kordoc 4.0.7 → 4.1.0

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 (123) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +837 -806
  3. package/dist/{-6HWEFXVV.js → -AS4IABL2.js} +24 -11
  4. package/dist/{chunk-BKWHX3RC.js → chunk-37HJHCPA.js} +24 -15
  5. package/dist/chunk-37HJHCPA.js.map +1 -0
  6. package/dist/{chunk-U25XGCNH.cjs → chunk-37VKMUST.cjs} +174 -25
  7. package/dist/chunk-37VKMUST.cjs.map +1 -0
  8. package/dist/{chunk-5OKHAJ62.js → chunk-4KI23VHA.js} +162 -13
  9. package/dist/chunk-4KI23VHA.js.map +1 -0
  10. package/dist/{chunk-6GBLFQMA.js → chunk-5RPYBC2Q.js} +1 -1
  11. package/dist/chunk-5RPYBC2Q.js.map +1 -0
  12. package/dist/{chunk-NGSHDBQR.js → chunk-AX2R5Q2N.js} +7 -4
  13. package/dist/chunk-AX2R5Q2N.js.map +1 -0
  14. package/dist/{chunk-X34YWRL5.cjs → chunk-DCZVOIEO.cjs} +1 -1
  15. package/dist/chunk-DCZVOIEO.cjs.map +1 -0
  16. package/dist/chunk-DZIXKL7E.js +145 -0
  17. package/dist/chunk-DZIXKL7E.js.map +1 -0
  18. package/dist/chunk-F2ZU3IZG.js +82 -0
  19. package/dist/chunk-F2ZU3IZG.js.map +1 -0
  20. package/dist/{chunk-AIQ3ISQU.js → chunk-GE43BE46.js} +1 -1
  21. package/dist/chunk-GE43BE46.js.map +1 -0
  22. package/dist/{chunk-Q7EN4EUJ.js → chunk-GQVSHGG4.js} +12 -4
  23. package/dist/chunk-GQVSHGG4.js.map +1 -0
  24. package/dist/chunk-GS7T56RP.cjs.map +1 -1
  25. package/dist/{chunk-TJJTLM76.js → chunk-L2XQQTOY.js} +1422 -1121
  26. package/dist/chunk-L2XQQTOY.js.map +1 -0
  27. package/dist/{chunk-3TBUDJDE.js → chunk-MOL7MDBG.js} +1 -1
  28. package/dist/chunk-MOL7MDBG.js.map +1 -0
  29. package/dist/{chunk-CNM75ZSX.js → chunk-O5VS7RIR.js} +2 -2
  30. package/dist/chunk-O5VS7RIR.js.map +1 -0
  31. package/dist/{chunk-2SN3CKME.js → chunk-SSTK6IKK.js} +18 -21
  32. package/dist/chunk-SSTK6IKK.js.map +1 -0
  33. package/dist/chunk-UOO7LSDG.js +152 -0
  34. package/dist/chunk-UOO7LSDG.js.map +1 -0
  35. package/dist/{chunk-ONXVBURQ.js → chunk-WO52HVQJ.js} +2 -2
  36. package/dist/chunk-WO52HVQJ.js.map +1 -0
  37. package/dist/{chunk-CFQLK3LM.js → chunk-YOO6ET6M.js} +7 -3
  38. package/dist/chunk-YOO6ET6M.js.map +1 -0
  39. package/dist/chunks-WVMGF7JL.js +10 -0
  40. package/dist/cli.js +173 -38
  41. package/dist/cli.js.map +1 -1
  42. package/dist/{detect-NTR2FUEG.js → detect-YEZX2XCF.js} +2 -2
  43. package/dist/{formula-ZYUDQHD3.cjs → formula-5NKVS2LR.cjs} +1 -1
  44. package/dist/formula-5NKVS2LR.cjs.map +1 -0
  45. package/dist/{formula-KOQV353G.js → formula-JCNF43NE.js} +1 -1
  46. package/dist/formula-JCNF43NE.js.map +1 -0
  47. package/dist/{formula-MEDGYQXS.js → formula-RXVSQPXI.js} +1 -1
  48. package/dist/formula-RXVSQPXI.js.map +1 -0
  49. package/dist/index.cjs +1932 -1397
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +106 -2
  52. package/dist/index.d.ts +106 -2
  53. package/dist/index.js +1626 -1091
  54. package/dist/index.js.map +1 -1
  55. package/dist/mcp.js +275 -67
  56. package/dist/mcp.js.map +1 -1
  57. package/dist/page-range-737B4EZW.js +8 -0
  58. package/dist/page-range-LNMXJU6W.js +7 -0
  59. package/dist/page-range-P7SDW6LR.cjs +8 -0
  60. package/dist/page-range-P7SDW6LR.cjs.map +1 -0
  61. package/dist/{parser-E6U5TNAU.cjs → parser-N4A7UGDK.cjs} +314 -56
  62. package/dist/parser-N4A7UGDK.cjs.map +1 -0
  63. package/dist/{parser-5EHWYJMC.js → parser-PEOYBXBO.js} +287 -27
  64. package/dist/parser-PEOYBXBO.js.map +1 -0
  65. package/dist/{parser-UGB3NIVH.js → parser-XX4QTDFQ.js} +283 -25
  66. package/dist/parser-XX4QTDFQ.js.map +1 -0
  67. package/dist/{profile-io-CAKRPQRD.js → profile-io-PLDQFPJA.js} +3 -3
  68. package/dist/{provider-5K7O3Z2O.cjs → provider-4FAYHJ6N.cjs} +3 -3
  69. package/dist/provider-4FAYHJ6N.cjs.map +1 -0
  70. package/dist/{provider-H4WWSPOV.js → provider-C6IOGIS5.js} +3 -3
  71. package/dist/provider-C6IOGIS5.js.map +1 -0
  72. package/dist/{provider-7H4CPZYS.js → provider-UIBW2MIH.js} +3 -3
  73. package/dist/provider-UIBW2MIH.js.map +1 -0
  74. package/dist/rasterize-WWNQLKYW.js +40 -0
  75. package/dist/rasterize-WWNQLKYW.js.map +1 -0
  76. package/dist/redact-7ILEAUTS.js +12 -0
  77. package/dist/render-T7S6H6AN.js +10 -0
  78. package/dist/render-T7S6H6AN.js.map +1 -0
  79. package/dist/seal-7PTL6MXH.js +10 -0
  80. package/dist/seal-7PTL6MXH.js.map +1 -0
  81. package/dist/{setup-QSJ2INNS.js → setup-Q2PRE7UA.js} +5 -3
  82. package/dist/setup-Q2PRE7UA.js.map +1 -0
  83. package/dist/{watch-RMX427YR.js → watch-SFHXZALC.js} +78 -24
  84. package/dist/watch-SFHXZALC.js.map +1 -0
  85. package/package.json +97 -97
  86. package/dist/chunk-2SN3CKME.js.map +0 -1
  87. package/dist/chunk-3TBUDJDE.js.map +0 -1
  88. package/dist/chunk-5OKHAJ62.js.map +0 -1
  89. package/dist/chunk-6GBLFQMA.js.map +0 -1
  90. package/dist/chunk-AIQ3ISQU.js.map +0 -1
  91. package/dist/chunk-BKWHX3RC.js.map +0 -1
  92. package/dist/chunk-CFQLK3LM.js.map +0 -1
  93. package/dist/chunk-CNM75ZSX.js.map +0 -1
  94. package/dist/chunk-NGSHDBQR.js.map +0 -1
  95. package/dist/chunk-ONXVBURQ.js.map +0 -1
  96. package/dist/chunk-Q7EN4EUJ.js.map +0 -1
  97. package/dist/chunk-TJJTLM76.js.map +0 -1
  98. package/dist/chunk-U25XGCNH.cjs.map +0 -1
  99. package/dist/chunk-X34YWRL5.cjs.map +0 -1
  100. package/dist/formula-KOQV353G.js.map +0 -1
  101. package/dist/formula-MEDGYQXS.js.map +0 -1
  102. package/dist/formula-ZYUDQHD3.cjs.map +0 -1
  103. package/dist/page-range-CIRRJPXJ.js +0 -7
  104. package/dist/page-range-OF5I4PQY.js +0 -8
  105. package/dist/page-range-TPIRSA3J.cjs +0 -8
  106. package/dist/page-range-TPIRSA3J.cjs.map +0 -1
  107. package/dist/parser-5EHWYJMC.js.map +0 -1
  108. package/dist/parser-E6U5TNAU.cjs.map +0 -1
  109. package/dist/parser-UGB3NIVH.js.map +0 -1
  110. package/dist/provider-5K7O3Z2O.cjs.map +0 -1
  111. package/dist/provider-7H4CPZYS.js.map +0 -1
  112. package/dist/provider-H4WWSPOV.js.map +0 -1
  113. package/dist/render-P6663O4I.js +0 -10
  114. package/dist/seal-7PPTXKKV.js +0 -10
  115. package/dist/setup-QSJ2INNS.js.map +0 -1
  116. package/dist/watch-RMX427YR.js.map +0 -1
  117. /package/dist/{-6HWEFXVV.js.map → -AS4IABL2.js.map} +0 -0
  118. /package/dist/{detect-NTR2FUEG.js.map → chunks-WVMGF7JL.js.map} +0 -0
  119. /package/dist/{page-range-CIRRJPXJ.js.map → detect-YEZX2XCF.js.map} +0 -0
  120. /package/dist/{page-range-OF5I4PQY.js.map → page-range-737B4EZW.js.map} +0 -0
  121. /package/dist/{profile-io-CAKRPQRD.js.map → page-range-LNMXJU6W.js.map} +0 -0
  122. /package/dist/{render-P6663O4I.js.map → profile-io-PLDQFPJA.js.map} +0 -0
  123. /package/dist/{seal-7PPTXKKV.js.map → redact-7ILEAUTS.js.map} +0 -0
package/dist/index.js CHANGED
@@ -13,17 +13,19 @@ import {
13
13
  convertTableToText,
14
14
  dedupeRunningHeaders,
15
15
  flattenLayoutTables,
16
+ inlineImagesIntoMarkdown,
16
17
  isPathTraversal,
17
18
  mapPuaText,
18
19
  normalizeSectionHref,
19
20
  precheckZipSize,
21
+ sanitizeError,
20
22
  sanitizeHref,
21
23
  stripDtd,
22
24
  toArrayBuffer
23
- } from "./chunk-5OKHAJ62.js";
25
+ } from "./chunk-4KI23VHA.js";
24
26
  import {
25
27
  parsePageRange
26
- } from "./chunk-AIQ3ISQU.js";
28
+ } from "./chunk-GE43BE46.js";
27
29
 
28
30
  // src/index.ts
29
31
  import { readFile } from "fs/promises";
@@ -47,8 +49,11 @@ function parseLenientCfb(data) {
47
49
  const miniSectorSizeShift = data.readUInt16LE(32);
48
50
  if (miniSectorSizeShift > 16) throw new Error("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBBF8\uB2C8 \uC139\uD130 \uD06C\uAE30 \uC2DC\uD504\uD2B8: " + miniSectorSizeShift);
49
51
  const miniSectorSize = 1 << miniSectorSizeShift;
50
- const fatSectorCount = data.readUInt32LE(44);
51
- if (fatSectorCount > 1e4) throw new Error("FAT \uC139\uD130 \uC218\uAC00 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4: " + fatSectorCount);
52
+ const headerFatSectorCount = data.readUInt32LE(44);
53
+ if (headerFatSectorCount > 1e4) throw new Error("FAT \uC139\uD130 \uC218\uAC00 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4: " + headerFatSectorCount);
54
+ const maxRealSectors = Math.ceil(Math.max(0, data.length - 512) / sectorSize);
55
+ const maxFatSectors = Math.max(1, Math.ceil(maxRealSectors / (sectorSize / 4)));
56
+ const fatSectorCount = Math.min(headerFatSectorCount, maxFatSectors);
52
57
  const firstDirSector = data.readUInt32LE(48);
53
58
  const miniStreamCutoff = data.readUInt32LE(56);
54
59
  const firstMiniFatSector = data.readUInt32LE(60);
@@ -75,6 +80,7 @@ function parseLenientCfb(data) {
75
80
  if (visitedDifat.has(difatSector)) break;
76
81
  visitedDifat.add(difatSector);
77
82
  const buf = readSectorData(difatSector);
83
+ if (buf.length < sectorSize) break;
78
84
  const entriesPerSector = sectorSize / 4 - 1;
79
85
  for (let i = 0; i < entriesPerSector && fatSectors.length < fatSectorCount; i++) {
80
86
  const sid = buf.readUInt32LE(i * 4);
@@ -393,6 +399,12 @@ function comResultToParseResult(pages, pageCount, warnings) {
393
399
  import { DOMParser } from "@xmldom/xmldom";
394
400
  var MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024;
395
401
  var MAX_ZIP_ENTRIES = 500;
402
+ var ZipBombError = class extends KordocError {
403
+ constructor(message) {
404
+ super(message);
405
+ this.name = "ZipBombError";
406
+ }
407
+ };
396
408
  function clampSpan(val, max) {
397
409
  return Math.max(1, Math.min(val, max));
398
410
  }
@@ -428,14 +440,15 @@ function findChildByLocalName(parent, name) {
428
440
  }
429
441
  return null;
430
442
  }
431
- function extractTextFromNode(node) {
443
+ function extractTextFromNode(node, depth = 0) {
432
444
  let result = "";
445
+ if (depth > MAX_XML_DEPTH) return result;
433
446
  const children = node.childNodes;
434
447
  if (!children) return result;
435
448
  for (let i = 0; i < children.length; i++) {
436
449
  const child = children[i];
437
450
  if (child.nodeType === 3) result += child.textContent || "";
438
- else if (child.nodeType === 1) result += extractTextFromNode(child);
451
+ else if (child.nodeType === 1) result += extractTextFromNode(child, depth + 1);
439
452
  }
440
453
  return result.trim();
441
454
  }
@@ -1319,7 +1332,7 @@ function simulateWrap(text, firstWidth, contWidth, height, ratioPct, mode = "kee
1319
1332
  for (const ch of text.slice(from, to)) w += charW2(ch);
1320
1333
  return w;
1321
1334
  };
1322
- const units = text.match(mode === "keep" ? / +|[^ ]+/g : / +|[^ ]/g) ?? [];
1335
+ const units = text.match(mode === "keep" ? / +|[^ ]+/gu : / +|[^ ]/gu) ?? [];
1323
1336
  const starts = [0];
1324
1337
  let lineW = 0;
1325
1338
  let avail = firstWidth;
@@ -1788,6 +1801,7 @@ function toRoman(n) {
1788
1801
  return out;
1789
1802
  }
1790
1803
  function formatHeadNumber(n, numFormat) {
1804
+ if (n === 0 && numFormat === "DIGIT") return "0";
1791
1805
  if (n <= 0) n = 1;
1792
1806
  switch (numFormat) {
1793
1807
  case "DIGIT":
@@ -1845,25 +1859,25 @@ function resolveParaHeading(paraEl, ctx) {
1845
1859
  if (!numDef) return headingLevel ? { headingLevel } : null;
1846
1860
  let counters = ctx.shared.numState.get(numId);
1847
1861
  if (!counters) {
1848
- counters = new Array(11).fill(0);
1862
+ counters = new Array(11).fill(-1);
1849
1863
  ctx.shared.numState.set(numId, counters);
1850
1864
  }
1851
1865
  const head = numDef.heads.get(level);
1852
- counters[level] = counters[level] === 0 ? head?.start ?? 1 : counters[level] + 1;
1853
- for (let l = level + 1; l <= 10; l++) counters[l] = 0;
1866
+ counters[level] = counters[level] < 0 ? head?.start ?? 1 : counters[level] + 1;
1867
+ for (let l = level + 1; l <= 10; l++) counters[l] = -1;
1854
1868
  const fmtText = head ? head.text.trim() : `^${level}.`;
1855
1869
  const prefix = fmtText.replace(/\^(10|[1-9])/g, (_, d) => {
1856
1870
  const lv = parseInt(d, 10);
1857
1871
  const refHead = numDef.heads.get(lv);
1858
- const n = counters[lv] || refHead?.start || 1;
1872
+ const n = counters[lv] >= 0 ? counters[lv] : refHead?.start ?? 1;
1859
1873
  return formatHeadNumber(n, refHead?.numFormat || "DIGIT");
1860
1874
  });
1861
1875
  return { prefix: prefix || void 0, headingLevel };
1862
1876
  }
1863
1877
 
1864
1878
  // src/hwpx/table-build.ts
1865
- function buildTableWithCellMeta(state) {
1866
- const table2 = buildTable(state.rows);
1879
+ function buildTableWithCellMeta(state, keepAnchoredEmptyCols) {
1880
+ const table2 = buildTable(state.rows, { keepAnchoredEmptyCols });
1867
1881
  if (state.caption) table2.caption = state.caption;
1868
1882
  const anchors = [];
1869
1883
  {
@@ -1927,7 +1941,7 @@ function completeTable(newTable, tableStack, blocks, ctx) {
1927
1941
  if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
1928
1942
  return parentTable;
1929
1943
  }
1930
- const ir = buildTableWithCellMeta(newTable);
1944
+ const ir = buildTableWithCellMeta(newTable, ctx.shared.keepTrailingEmptyCols);
1931
1945
  const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
1932
1946
  if (parentTable?.cell) {
1933
1947
  const cell2 = parentTable.cell;
@@ -1961,7 +1975,8 @@ function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
1961
1975
  walkSection(doc.documentElement, blocks, null, [], ctx);
1962
1976
  return blocks;
1963
1977
  }
1964
- function extractImageRef(el) {
1978
+ function extractImageRef(el, depth = 0) {
1979
+ if (depth > MAX_XML_DEPTH) return null;
1965
1980
  const children = el.childNodes;
1966
1981
  if (!children) return null;
1967
1982
  for (let i = 0; i < children.length; i++) {
@@ -1972,7 +1987,7 @@ function extractImageRef(el) {
1972
1987
  const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
1973
1988
  if (ref) return ref;
1974
1989
  }
1975
- const nested = extractImageRef(child);
1990
+ const nested = extractImageRef(child, depth + 1);
1976
1991
  if (nested) return nested;
1977
1992
  }
1978
1993
  const directRef = el.getAttribute("binaryItemIDRef") || "";
@@ -2079,7 +2094,7 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
2079
2094
  ;
2080
2095
  (cell2.blocks ??= []).push(cellBlock);
2081
2096
  } else if (!tableCtx) {
2082
- if (/^─{10,}$/.test(text)) {
2097
+ if (ctx.shared.kordocLayout && /^─{10,}$/.test(text)) {
2083
2098
  blocks.push({ type: "separator", pageNumber: ctx.sectionNum });
2084
2099
  tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
2085
2100
  break;
@@ -2355,8 +2370,8 @@ function extractDrawTextBlocks(drawTextNode, blocks, ctx) {
2355
2370
  } else {
2356
2371
  const info = extractParagraphInfo(child, ctx.styleMap, ctx);
2357
2372
  let text = info.text.trim();
2373
+ const ph = resolveParaHeading(child, ctx);
2358
2374
  if (text) {
2359
- const ph = resolveParaHeading(child, ctx);
2360
2375
  if (ph?.prefix) text = ph.prefix + " " + text;
2361
2376
  const block = { type: "paragraph", text, style: info.style ?? void 0, pageNumber: ctx.sectionNum };
2362
2377
  if (info.href) block.href = info.href;
@@ -2469,7 +2484,8 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2469
2484
  }
2470
2485
  }
2471
2486
  };
2472
- const walk = (node) => {
2487
+ const walk = (node, depth = 0) => {
2488
+ if (depth > MAX_XML_DEPTH) return;
2473
2489
  const children = node.childNodes;
2474
2490
  if (!children) return;
2475
2491
  for (let i = 0; i < children.length; i++) {
@@ -2490,7 +2506,7 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2490
2506
  const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
2491
2507
  switch (tag) {
2492
2508
  case "t":
2493
- walk(child);
2509
+ walk(child, depth + 1);
2494
2510
  break;
2495
2511
  // 자식 순회 (tab 등 하위 요소 처리)
2496
2512
  case "tab": {
@@ -2523,7 +2539,7 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2523
2539
  const safe = sanitizeHref(url);
2524
2540
  if (safe) href = safe;
2525
2541
  }
2526
- walk(child);
2542
+ walk(child, depth + 1);
2527
2543
  break;
2528
2544
  }
2529
2545
  // 각주/미주
@@ -2594,11 +2610,11 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2594
2610
  case "r": {
2595
2611
  const runCharPr = child.getAttribute("charPrIDRef");
2596
2612
  if (runCharPr && !charPrId) charPrId = runCharPr;
2597
- walk(child);
2613
+ walk(child, depth + 1);
2598
2614
  break;
2599
2615
  }
2600
2616
  default:
2601
- walk(child);
2617
+ walk(child, depth + 1);
2602
2618
  break;
2603
2619
  }
2604
2620
  }
@@ -2609,7 +2625,7 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2609
2625
  let cleanText = text.replace(/[ \t]+/g, " ").trim();
2610
2626
  if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
2611
2627
  cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
2612
- cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
2628
+ cleanText = cleanText.replace(/^(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?$/gm, "").trim();
2613
2629
  let style;
2614
2630
  if (styleMap && charPrId) {
2615
2631
  const charProp = styleMap.charProperties.get(charPrId);
@@ -2711,628 +2727,805 @@ function gongmunDepthFromIndent(para2, left, ctx) {
2711
2727
  return depth >= 1 && depth < 8 ? depth : null;
2712
2728
  }
2713
2729
 
2714
- // src/hwpx/images.ts
2715
- function imageExtToMime(ext) {
2716
- switch (ext.toLowerCase()) {
2717
- case "jpg":
2718
- case "jpeg":
2719
- return "image/jpeg";
2720
- case "png":
2721
- return "image/png";
2722
- case "gif":
2723
- return "image/gif";
2724
- case "bmp":
2725
- return "image/bmp";
2726
- case "tif":
2727
- case "tiff":
2728
- return "image/tiff";
2729
- case "wmf":
2730
- return "image/wmf";
2731
- case "emf":
2732
- return "image/emf";
2733
- case "svg":
2734
- return "image/svg+xml";
2735
- default:
2736
- return "application/octet-stream";
2730
+ // src/hwp5/record.ts
2731
+ import { inflateRawSync, inflateSync } from "zlib";
2732
+ var TAG_PARA_HEADER = 66;
2733
+ var TAG_PARA_TEXT = 67;
2734
+ var TAG_CHAR_SHAPE = 68;
2735
+ var TAG_CTRL_HEADER = 71;
2736
+ var TAG_LIST_HEADER = 72;
2737
+ var TAG_TABLE = 77;
2738
+ var TAG_SHAPE_COMPONENT = 76;
2739
+ var TAG_SHAPE_COMPONENT_PICTURE = 85;
2740
+ var TAG_SHAPE_COMPONENT_CONTAINER = 86;
2741
+ var TAG_EQEDIT = 88;
2742
+ var TAG_BIN_DATA = 18;
2743
+ var TAG_DOC_CHAR_SHAPE = 21;
2744
+ var TAG_NUMBERING = 23;
2745
+ var TAG_BULLET = 24;
2746
+ var TAG_DOC_PARA_SHAPE = 25;
2747
+ var TAG_DOC_STYLE = 26;
2748
+ var CHAR_LINE = 0;
2749
+ var CHAR_SECTION_BREAK = 10;
2750
+ var CHAR_PARA = 13;
2751
+ var CHAR_TAB = 9;
2752
+ var CHAR_HYPHEN = 24;
2753
+ var CHAR_NBSP = 30;
2754
+ var CHAR_FIXED_WIDTH = 31;
2755
+ var FLAG_COMPRESSED = 1 << 0;
2756
+ var FLAG_ENCRYPTED = 1 << 1;
2757
+ var FLAG_DISTRIBUTION = 1 << 2;
2758
+ var FLAG_DRM = 1 << 4;
2759
+ var MAX_RECORDS = 5e5;
2760
+ function readRecords(data) {
2761
+ const records = [];
2762
+ let offset = 0;
2763
+ while (offset + 4 <= data.length && records.length < MAX_RECORDS) {
2764
+ const header = data.readUInt32LE(offset);
2765
+ offset += 4;
2766
+ const tagId = header & 1023;
2767
+ const level = header >> 10 & 1023;
2768
+ let size = header >> 20 & 4095;
2769
+ if (size === 4095) {
2770
+ if (offset + 4 > data.length) break;
2771
+ size = data.readUInt32LE(offset);
2772
+ offset += 4;
2773
+ }
2774
+ if (offset + size > data.length) break;
2775
+ records.push({ tagId, level, size, data: data.subarray(offset, offset + size) });
2776
+ offset += size;
2737
2777
  }
2778
+ return records;
2738
2779
  }
2739
- function mimeToExt(mime) {
2740
- if (mime.includes("jpeg")) return "jpg";
2741
- if (mime.includes("png")) return "png";
2742
- if (mime.includes("gif")) return "gif";
2743
- if (mime.includes("bmp")) return "bmp";
2744
- if (mime.includes("tiff")) return "tif";
2745
- if (mime.includes("wmf")) return "wmf";
2746
- if (mime.includes("emf")) return "emf";
2747
- if (mime.includes("svg")) return "svg";
2748
- return "bin";
2749
- }
2750
- function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
2751
- if (depth > MAX_XML_DEPTH) return;
2752
- for (const block of blocks) {
2753
- if (block.type === "image") {
2754
- out.push({ block, ownerCell });
2755
- } else if (block.type === "table" && block.table) {
2756
- for (const row of block.table.cells) {
2757
- for (const cell2 of row) {
2758
- if (cell2.blocks?.length) collectImageBlocks(cell2.blocks, out, cell2, depth + 1);
2759
- }
2760
- }
2780
+ var MAX_DECOMPRESS_SIZE2 = 100 * 1024 * 1024;
2781
+ function decompressStream(data) {
2782
+ const opts = { maxOutputLength: MAX_DECOMPRESS_SIZE2 };
2783
+ if (data.length >= 2 && data[0] === 120) {
2784
+ try {
2785
+ return inflateSync(data, opts);
2786
+ } catch {
2761
2787
  }
2762
2788
  }
2789
+ return inflateRawSync(data, opts);
2763
2790
  }
2764
- async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
2765
- const images = [];
2766
- let imageIndex = 0;
2767
- const imageBlocks = [];
2768
- collectImageBlocks(blocks, imageBlocks);
2769
- const resolved = /* @__PURE__ */ new Map();
2770
- for (const { block, ownerCell } of imageBlocks) {
2771
- if (block.type !== "image" || !block.text) continue;
2772
- const ref = block.text;
2773
- let img = resolved.get(ref);
2774
- if (img === void 0) {
2775
- img = null;
2776
- const candidates = [
2777
- `BinData/${ref}`,
2778
- `Contents/BinData/${ref}`,
2779
- ref
2780
- // 절대 경로일 수도 있음
2781
- ];
2782
- let resolvedPath = null;
2783
- if (!ref.includes(".")) {
2784
- const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
2785
- for (const prefix of prefixes) {
2786
- const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
2787
- if (match.length > 0) {
2788
- resolvedPath = match[0].name;
2789
- break;
2790
- }
2791
+ function parseFileHeader(data) {
2792
+ if (data.length < 40) throw new KordocError("FileHeader\uAC00 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (\uCD5C\uC18C 40\uBC14\uC774\uD2B8)");
2793
+ const sig = data.subarray(0, 32).toString("utf8").replace(/\0+$/, "");
2794
+ return {
2795
+ signature: sig,
2796
+ versionMajor: data[35],
2797
+ flags: data.readUInt32LE(36)
2798
+ };
2799
+ }
2800
+ function readHwpString(data, offset) {
2801
+ if (offset + 2 > data.length) return { value: "", next: data.length };
2802
+ const len = data.readUInt16LE(offset);
2803
+ const start = offset + 2;
2804
+ const end = start + len * 2;
2805
+ if (len === 0 || end > data.length) return { value: "", next: start };
2806
+ return { value: data.subarray(start, end).toString("utf16le"), next: end };
2807
+ }
2808
+ function parseDocInfo(records) {
2809
+ const charShapes = [];
2810
+ const paraShapes = [];
2811
+ const styles = [];
2812
+ const binData = [];
2813
+ const numberings = [];
2814
+ const bullets = [];
2815
+ for (const rec of records) {
2816
+ if (rec.tagId === TAG_DOC_PARA_SHAPE && rec.data.length >= 4) {
2817
+ const attr1 = rec.data.readUInt32LE(0);
2818
+ const headType = attr1 >>> 23 & 3;
2819
+ const paraLevel = attr1 >>> 25 & 7;
2820
+ const numberingId = rec.data.length >= 32 ? rec.data.readUInt16LE(30) : 0;
2821
+ paraShapes.push({ headType, paraLevel, numberingId });
2822
+ }
2823
+ if (rec.tagId === TAG_BIN_DATA && rec.data.length >= 2) {
2824
+ const attr = rec.data.readUInt16LE(0);
2825
+ const typeBits = attr & 15;
2826
+ if (typeBits === 0) {
2827
+ binData.push({ kind: "link", storageId: 0, extension: "" });
2828
+ } else {
2829
+ const storageId = rec.data.length >= 4 ? rec.data.readUInt16LE(2) : 0;
2830
+ const { value: extension } = readHwpString(rec.data, 4);
2831
+ binData.push({ kind: typeBits === 2 ? "storage" : "embed", storageId, extension });
2832
+ }
2833
+ }
2834
+ if (rec.tagId === TAG_NUMBERING && rec.data.length >= 14) {
2835
+ const levelFormats = [];
2836
+ const numberFormats = [];
2837
+ const startNumbers = [1, 1, 1, 1, 1, 1, 1];
2838
+ let offset = 0;
2839
+ for (let level = 0; level < 7; level++) {
2840
+ if (offset + 12 > rec.data.length) {
2841
+ levelFormats.push("");
2842
+ numberFormats.push(0);
2843
+ continue;
2791
2844
  }
2845
+ const attr = rec.data.readUInt32LE(offset);
2846
+ numberFormats.push(attr >>> 5 & 15);
2847
+ offset += 12;
2848
+ const { value, next } = readHwpString(rec.data, offset);
2849
+ levelFormats.push(value);
2850
+ offset = next;
2792
2851
  }
2793
- const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
2794
- for (const path of allCandidates) {
2795
- if (isPathTraversal(path)) continue;
2796
- const file = zip.file(path);
2797
- if (!file) continue;
2798
- try {
2799
- const data = await file.async("uint8array");
2800
- decompressed.total += data.length;
2801
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2802
- const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
2803
- const mimeType = imageExtToMime(ext);
2804
- imageIndex++;
2805
- const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
2806
- img = { filename, data, mimeType };
2807
- images.push(img);
2808
- break;
2809
- } catch (err) {
2810
- if (err instanceof KordocError) throw err;
2852
+ let baseStart = 1;
2853
+ if (offset + 2 <= rec.data.length) {
2854
+ baseStart = rec.data.readUInt16LE(offset) || 1;
2855
+ offset += 2;
2856
+ }
2857
+ for (let level = 0; level < 7; level++) {
2858
+ if (offset + 4 <= rec.data.length) {
2859
+ startNumbers[level] = rec.data.readUInt32LE(offset) || 1;
2860
+ offset += 4;
2861
+ } else {
2862
+ startNumbers[level] = baseStart;
2811
2863
  }
2812
2864
  }
2813
- if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
2814
- resolved.set(ref, img);
2865
+ numberings.push({ levelFormats, numberFormats, startNumbers });
2815
2866
  }
2816
- if (!img) {
2817
- block.type = "paragraph";
2818
- block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
2819
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\uC774\uBBF8\uC9C0: ${ref}]`);
2820
- continue;
2867
+ if (rec.tagId === TAG_BULLET && rec.data.length >= 14) {
2868
+ const code = rec.data.readUInt16LE(12);
2869
+ bullets.push({ char: code > 0 ? String.fromCharCode(code) : "\u2022" });
2821
2870
  }
2822
- block.text = img.filename;
2823
- block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
2824
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `![image](${img.filename})`);
2825
- }
2826
- return images;
2827
- }
2828
-
2829
- // src/hwpx/metadata.ts
2830
- import JSZip2 from "jszip";
2831
-
2832
- // src/hwpx/zip-sections.ts
2833
- import { inflateRawSync } from "zlib";
2834
- function extractFromBrokenZip(buffer) {
2835
- const data = new Uint8Array(buffer);
2836
- const view = new DataView(buffer);
2837
- let pos = 0;
2838
- const blocks = [];
2839
- const warnings = [
2840
- { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
2841
- ];
2842
- let totalDecompressed = 0;
2843
- let entryCount = 0;
2844
- let sectionNum = 0;
2845
- const shared = createSectionShared();
2846
- while (pos < data.length - 30) {
2847
- if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
2848
- pos++;
2849
- while (pos < data.length - 30) {
2850
- if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
2851
- pos++;
2871
+ if (rec.tagId === TAG_DOC_CHAR_SHAPE && rec.data.length >= 18) {
2872
+ if (rec.data.length >= 50) {
2873
+ const fontSize = rec.data.readUInt32LE(42);
2874
+ const attrFlags = rec.data.readUInt32LE(46);
2875
+ charShapes.push({ fontSize, attrFlags });
2876
+ } else {
2877
+ charShapes.push({ fontSize: 0, attrFlags: 0 });
2852
2878
  }
2853
- continue;
2854
2879
  }
2855
- if (++entryCount > MAX_ZIP_ENTRIES) break;
2856
- const method = view.getUint16(pos + 8, true);
2857
- const compSize = view.getUint32(pos + 18, true);
2858
- const nameLen = view.getUint16(pos + 26, true);
2859
- const extraLen = view.getUint16(pos + 28, true);
2860
- if (nameLen > 1024 || extraLen > 65535) {
2861
- pos += 30 + nameLen + extraLen;
2862
- continue;
2863
- }
2864
- const fileStart = pos + 30 + nameLen + extraLen;
2865
- if (fileStart + compSize > data.length) break;
2866
- if (compSize === 0 && method !== 0) {
2867
- pos = fileStart;
2868
- continue;
2869
- }
2870
- const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
2871
- const name = new TextDecoder().decode(nameBytes);
2872
- if (isPathTraversal(name)) {
2873
- pos = fileStart + compSize;
2874
- continue;
2875
- }
2876
- const fileData = data.slice(fileStart, fileStart + compSize);
2877
- pos = fileStart + compSize;
2878
- if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
2879
- try {
2880
- let content;
2881
- if (method === 0) {
2882
- content = new TextDecoder().decode(fileData);
2883
- } else if (method === 8) {
2884
- const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
2885
- content = new TextDecoder().decode(decompressed);
2886
- } else {
2887
- continue;
2880
+ if (rec.tagId === TAG_DOC_STYLE && rec.data.length >= 8) {
2881
+ try {
2882
+ let offset = 0;
2883
+ const nameLen = rec.data.readUInt16LE(offset);
2884
+ offset += 2;
2885
+ const nameBytes = nameLen * 2;
2886
+ const name = nameBytes > 0 && offset + nameBytes <= rec.data.length ? rec.data.subarray(offset, offset + nameBytes).toString("utf16le") : "";
2887
+ offset += nameBytes;
2888
+ let nameKo = "";
2889
+ if (offset + 2 <= rec.data.length) {
2890
+ const nameKoLen = rec.data.readUInt16LE(offset);
2891
+ offset += 2;
2892
+ const nameKoBytes = nameKoLen * 2;
2893
+ if (nameKoBytes > 0 && offset + nameKoBytes <= rec.data.length) {
2894
+ nameKo = rec.data.subarray(offset, offset + nameKoBytes).toString("utf16le");
2895
+ }
2896
+ offset += nameKoBytes;
2897
+ }
2898
+ const type = offset < rec.data.length ? rec.data.readUInt8(offset) : 0;
2899
+ offset += 1;
2900
+ offset += 1;
2901
+ offset += 2;
2902
+ const paraShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2903
+ offset += 2;
2904
+ const charShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2905
+ styles.push({ name, nameKo, charShapeId, paraShapeId, type });
2906
+ } catch {
2888
2907
  }
2889
- totalDecompressed += content.length * 2;
2890
- if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
2891
- sectionNum++;
2892
- blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
2893
- } catch {
2894
- continue;
2895
2908
  }
2896
2909
  }
2897
- 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");
2898
- applyPageText(blocks, shared);
2899
- const markdown = blocksToMarkdown(blocks);
2900
- return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
2910
+ return { charShapes, paraShapes, styles, binData, numberings, bullets };
2901
2911
  }
2902
- async function readKordocLayout(zip) {
2903
- const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
2904
- if (!file) return null;
2905
- try {
2906
- const xml = await file.async("text");
2907
- if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
2908
- return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
2909
- } catch {
2910
- return null;
2911
- }
2912
+ function createParaTextState() {
2913
+ return { text: "", ctrlIdx: 0, fieldStack: [], fieldRanges: [] };
2912
2914
  }
2913
- async function resolveSectionPaths(zip) {
2914
- const manifestPaths = ["Contents/content.hpf", "content.hpf"];
2915
- for (const mp of manifestPaths) {
2916
- const mpLower = mp.toLowerCase();
2917
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
2918
- if (!file) continue;
2919
- const xml = await file.async("text");
2920
- const paths = parseSectionPathsFromManifest(xml);
2921
- if (paths.length > 0) return paths;
2922
- }
2923
- const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
2924
- return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
2915
+ function isExtendedOnlyCtrlChar(ch) {
2916
+ return ch >= 1 && ch <= 3 || ch >= 11 && ch <= 12 || ch >= 14 && ch <= 18 || ch >= 21 && ch <= 23;
2925
2917
  }
2926
- function parseSectionPathsFromManifest(xml) {
2927
- const parser = createXmlParser();
2928
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2929
- const items = doc.getElementsByTagName("opf:item");
2930
- const spine = doc.getElementsByTagName("opf:itemref");
2931
- const idToHref = /* @__PURE__ */ new Map();
2932
- for (let i = 0; i < items.length; i++) {
2933
- const item = items[i];
2934
- const id = item.getAttribute("id") || "";
2935
- const href = normalizeSectionHref(item.getAttribute("href") || "");
2936
- if (id && href) idToHref.set(id, href);
2937
- }
2938
- if (spine.length > 0) {
2939
- const ordered = [];
2940
- for (let i = 0; i < spine.length; i++) {
2941
- const href = idToHref.get(spine[i].getAttribute("idref") || "");
2942
- if (href) ordered.push(href);
2918
+ function appendParaText(state, data, resolveControl) {
2919
+ let result = "";
2920
+ let i = 0;
2921
+ const base = state.text.length;
2922
+ const resolveAt = (byteOffset, extended) => {
2923
+ const ctrlId = data.readUInt32LE(byteOffset);
2924
+ const idx = extended ? state.ctrlIdx : -1;
2925
+ const replacement = resolveControl?.(idx, ctrlId);
2926
+ if (replacement) result += replacement;
2927
+ if (extended) state.ctrlIdx++;
2928
+ };
2929
+ while (i + 1 < data.length) {
2930
+ const ch = data.readUInt16LE(i);
2931
+ i += 2;
2932
+ switch (ch) {
2933
+ // ── char 타입 (2바이트만, 확장 데이터 없음) ──
2934
+ case CHAR_LINE:
2935
+ result += "\n";
2936
+ break;
2937
+ case CHAR_SECTION_BREAK: {
2938
+ if (i + 16 <= data.length && data.readUInt16LE(i) === 11) {
2939
+ resolveAt(i + 2, true);
2940
+ i += 16;
2941
+ break;
2942
+ }
2943
+ result += "\n";
2944
+ break;
2945
+ }
2946
+ case CHAR_PARA:
2947
+ break;
2948
+ // 문단 끝
2949
+ case CHAR_HYPHEN:
2950
+ result += "-";
2951
+ break;
2952
+ case CHAR_NBSP:
2953
+ result += "\xA0";
2954
+ break;
2955
+ // 진짜 NBSP
2956
+ case CHAR_FIXED_WIDTH:
2957
+ result += " ";
2958
+ break;
2959
+ // 고정폭 공백
2960
+ // ── inline 타입 (2바이트 + 14바이트 확장) ──
2961
+ case CHAR_TAB:
2962
+ result += " ";
2963
+ if (i + 14 <= data.length) i += 14;
2964
+ break;
2965
+ default:
2966
+ if (ch >= 1 && ch <= 31) {
2967
+ const isExtended = isExtendedOnlyCtrlChar(ch);
2968
+ const isInline = ch >= 4 && ch <= 9 || ch >= 19 && ch <= 20;
2969
+ if ((isExtended || isInline) && i + 14 <= data.length) {
2970
+ if (ch === 3) {
2971
+ state.fieldStack.push({ start: base + result.length, ctrlIdx: state.ctrlIdx });
2972
+ } else if (ch === 4) {
2973
+ const open = state.fieldStack.pop();
2974
+ if (open) {
2975
+ state.fieldRanges.push({ start: open.start, end: base + result.length, ctrlIdx: open.ctrlIdx });
2976
+ }
2977
+ }
2978
+ resolveAt(i, isExtended);
2979
+ i += 14;
2980
+ }
2981
+ } else if (ch >= 32) {
2982
+ if (ch >= 55296 && ch <= 56319 && i + 1 < data.length) {
2983
+ const lo = data.readUInt16LE(i);
2984
+ if (lo >= 56320 && lo <= 57343) {
2985
+ i += 2;
2986
+ const codePoint = (ch - 55296 << 10) + (lo - 56320) + 65536;
2987
+ result += String.fromCodePoint(codePoint);
2988
+ break;
2989
+ }
2990
+ }
2991
+ result += String.fromCharCode(ch);
2992
+ }
2993
+ break;
2943
2994
  }
2944
- if (ordered.length > 0) return ordered;
2945
2995
  }
2946
- return Array.from(idToHref.values()).sort(compareSectionPaths);
2996
+ state.text += result;
2997
+ }
2998
+ function extractEquationText(data) {
2999
+ if (data.length < 6) return null;
3000
+ const scriptLength = data.readUInt16LE(4);
3001
+ const scriptStart = 6;
3002
+ const scriptEnd = scriptStart + scriptLength * 2;
3003
+ if (scriptLength <= 0 || scriptEnd > data.length) return null;
3004
+ const equation = data.subarray(scriptStart, scriptEnd).toString("utf16le").replace(/\0+/g, "").trim();
3005
+ return equation || null;
2947
3006
  }
2948
3007
 
2949
- // src/hwpx/metadata.ts
2950
- async function extractHwpxMetadata(zip, metadata, decompressed) {
3008
+ // src/hwp5/images.ts
3009
+ function detectImageMime(data) {
3010
+ if (data.length < 4) return null;
3011
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
3012
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
3013
+ if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
3014
+ if (data[0] === 66 && data[1] === 77) return "image/bmp";
3015
+ if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
3016
+ if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
3017
+ return null;
3018
+ }
3019
+ function normalizeBinPayload(data) {
3020
+ if (detectImageMime(data)) return data;
2951
3021
  try {
2952
- const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
2953
- for (const mp of metaPaths) {
2954
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
2955
- if (!file) continue;
2956
- const xml = await file.async("text");
2957
- if (decompressed) {
2958
- decompressed.total += xml.length * 2;
2959
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2960
- }
2961
- parseDublinCoreMetadata(xml, metadata);
2962
- if (metadata.title || metadata.author) return;
2963
- }
3022
+ const inflated = decompressStream(data);
3023
+ if (inflated.length > 0) return inflated;
2964
3024
  } catch {
2965
3025
  }
3026
+ return data;
2966
3027
  }
2967
- function parseDublinCoreMetadata(xml, metadata) {
2968
- const parser = createXmlParser();
2969
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2970
- if (!doc.documentElement) return;
2971
- const getText = (tagNames) => {
2972
- for (const tag of tagNames) {
2973
- const els = doc.getElementsByTagName(tag);
2974
- if (els.length > 0) {
2975
- const text = els[0].textContent?.trim();
2976
- if (text) return text;
3028
+ var BIN_ENTRY_RE = /(?:^|\/)BIN([0-9A-Fa-f]{4,8})(?:\.[^./\\]*)?$/;
3029
+ function collectImageBlocks(blocks, out) {
3030
+ for (const b of blocks) {
3031
+ if (b.type === "image") out.push(b);
3032
+ if (b.table) {
3033
+ for (const row of b.table.cells) {
3034
+ for (const cell2 of row) {
3035
+ if (cell2.blocks) collectImageBlocks(cell2.blocks, out);
3036
+ }
2977
3037
  }
2978
3038
  }
2979
- return void 0;
2980
- };
2981
- metadata.title = metadata.title || getText(["dc:title", "title"]);
2982
- metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
2983
- metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
2984
- metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
2985
- metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
2986
- const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
2987
- if (keywords && !metadata.keywords) {
2988
- metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
3039
+ if (b.children) collectImageBlocks(b.children, out);
2989
3040
  }
2990
3041
  }
2991
-
2992
- // src/hwpx/parser.ts
2993
- async function parseHwpxDocument(buffer, options) {
2994
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
2995
- let zip;
2996
- try {
2997
- zip = await JSZip3.loadAsync(buffer);
2998
- } catch {
2999
- return extractFromBrokenZip(buffer);
3000
- }
3001
- const actualEntryCount = Object.keys(zip.files).length;
3002
- if (actualEntryCount > MAX_ZIP_ENTRIES) {
3003
- throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3042
+ function forEachTableCell(blocks, fn) {
3043
+ for (const b of blocks) {
3044
+ if (b.table) {
3045
+ for (const row of b.table.cells) {
3046
+ for (const cell2 of row) {
3047
+ fn(cell2);
3048
+ if (cell2.blocks) forEachTableCell(cell2.blocks, fn);
3049
+ }
3050
+ }
3051
+ }
3052
+ if (b.children) forEachTableCell(b.children, fn);
3004
3053
  }
3005
- const manifestFile = zip.file("META-INF/manifest.xml");
3006
- if (manifestFile) {
3007
- const manifestXml = await manifestFile.async("text");
3008
- if (isEncryptedHwpx(manifestXml)) {
3009
- if (isComFallbackAvailable() && options?.filePath) {
3010
- const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
3011
- if (pages.some((p) => p && p.trim().length > 0)) {
3012
- return comResultToParseResult(pages, pageCount, warnings2);
3054
+ }
3055
+ var CELL_IMAGE_SENTINEL_RE = /!\[image\]\(hwp5bin:(\d+)\)/g;
3056
+ function resolveCellImageSentinels(blocks, renamed) {
3057
+ forEachTableCell(blocks, (cell2) => {
3058
+ if (!cell2.text.includes("hwp5bin:")) return;
3059
+ cell2.text = cell2.text.replace(CELL_IMAGE_SENTINEL_RE, (_m, idStr) => {
3060
+ const filename = renamed.get(Number(idStr));
3061
+ return filename ? `![image](${filename})` : "[\uC774\uBBF8\uC9C0]";
3062
+ });
3063
+ });
3064
+ }
3065
+ function resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced) {
3066
+ const imageBlocks = [];
3067
+ collectImageBlocks(blocks, imageBlocks);
3068
+ const images = [];
3069
+ const renamed = /* @__PURE__ */ new Map();
3070
+ const resolved = /* @__PURE__ */ new Map();
3071
+ let imageIndex = 0;
3072
+ for (const block of imageBlocks) {
3073
+ if (!block.text) continue;
3074
+ const storageId = parseInt(block.text, 10);
3075
+ if (isNaN(storageId)) continue;
3076
+ let img = resolved.get(storageId);
3077
+ if (img === void 0) {
3078
+ const bin = binDataMap.get(storageId);
3079
+ if (!bin) {
3080
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
3081
+ resolved.set(storageId, null);
3082
+ } else {
3083
+ const mime = detectImageMime(bin.data);
3084
+ if (!mime) {
3085
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
3086
+ resolved.set(storageId, null);
3087
+ } else {
3088
+ imageIndex++;
3089
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
3090
+ img = { filename: `image_${String(imageIndex).padStart(3, "0")}.${ext}`, data: new Uint8Array(bin.data), mime };
3091
+ resolved.set(storageId, img);
3092
+ images.push({ filename: img.filename, data: img.data, mimeType: img.mime });
3093
+ renamed.set(storageId, img.filename);
3013
3094
  }
3014
3095
  }
3015
- 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.");
3096
+ img = resolved.get(storageId);
3097
+ }
3098
+ if (!img) {
3099
+ const bin = binDataMap.get(storageId);
3100
+ block.type = "paragraph";
3101
+ block.text = bin ? `[\uC774\uBBF8\uC9C0: ${bin.name}]` : `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
3102
+ continue;
3016
3103
  }
3104
+ block.text = img.filename;
3105
+ block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
3017
3106
  }
3018
- const decompressed = { total: 0 };
3019
- const metadata = {};
3020
- await extractHwpxMetadata(zip, metadata, decompressed);
3021
- const styleMap = await extractHwpxStyles(zip, decompressed);
3022
- const warnings = [];
3023
- const sectionPaths = await resolveSectionPaths(zip);
3024
- if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
3025
- metadata.pageCount = sectionPaths.length;
3026
- const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
3027
- const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
3028
- const blocks = [];
3029
- const shared = createSectionShared();
3030
- shared.kordocLayout = await readKordocLayout(zip);
3031
- let parsedSections = 0;
3032
- for (let si = 0; si < sectionPaths.length; si++) {
3033
- if (pageFilter && !pageFilter.has(si + 1)) continue;
3034
- const file = zip.file(sectionPaths[si]);
3035
- if (!file) continue;
3036
- try {
3037
- const xml = await file.async("text");
3038
- decompressed.total += xml.length * 2;
3039
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3040
- blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
3041
- parsedSections++;
3042
- options?.onProgress?.(parsedSections, totalTarget);
3043
- } catch (secErr) {
3044
- if (secErr instanceof KordocError) throw secErr;
3045
- 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" });
3107
+ if (sweepUnreferenced) {
3108
+ for (const [storageId, bin] of [...binDataMap.entries()].sort((a, b) => a[0] - b[0])) {
3109
+ if (resolved.has(storageId)) continue;
3110
+ const mime = detectImageMime(bin.data);
3111
+ if (!mime) continue;
3112
+ imageIndex++;
3113
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
3114
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
3115
+ const data = new Uint8Array(bin.data);
3116
+ images.push({ filename, data, mimeType: mime });
3117
+ blocks.push({ type: "image", text: filename, imageData: { data, mimeType: mime, filename: bin.name } });
3046
3118
  }
3047
3119
  }
3048
- applyPageText(blocks, shared);
3049
- const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
3050
- detectHwpxHeadings(blocks, styleMap);
3051
- const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
3052
- const markdown = blocksToMarkdown(blocks);
3053
- 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 };
3120
+ resolveCellImageSentinels(blocks, renamed);
3121
+ return images;
3054
3122
  }
3055
-
3056
- // src/hwp5/record.ts
3057
- import { inflateRawSync as inflateRawSync2, inflateSync } from "zlib";
3058
- var TAG_PARA_HEADER = 66;
3059
- var TAG_PARA_TEXT = 67;
3060
- var TAG_CHAR_SHAPE = 68;
3061
- var TAG_CTRL_HEADER = 71;
3062
- var TAG_LIST_HEADER = 72;
3063
- var TAG_TABLE = 77;
3064
- var TAG_SHAPE_COMPONENT = 76;
3065
- var TAG_SHAPE_COMPONENT_PICTURE = 85;
3066
- var TAG_SHAPE_COMPONENT_CONTAINER = 86;
3067
- var TAG_EQEDIT = 88;
3068
- var TAG_BIN_DATA = 18;
3069
- var TAG_DOC_CHAR_SHAPE = 21;
3070
- var TAG_NUMBERING = 23;
3071
- var TAG_BULLET = 24;
3072
- var TAG_DOC_PARA_SHAPE = 25;
3073
- var TAG_DOC_STYLE = 26;
3074
- var CHAR_LINE = 0;
3075
- var CHAR_SECTION_BREAK = 10;
3076
- var CHAR_PARA = 13;
3077
- var CHAR_TAB = 9;
3078
- var CHAR_HYPHEN = 30;
3079
- var CHAR_NBSP = 31;
3080
- var CHAR_FIXED_NBSP = 24;
3081
- var CHAR_FIXED_WIDTH = 25;
3082
- var FLAG_COMPRESSED = 1 << 0;
3083
- var FLAG_ENCRYPTED = 1 << 1;
3084
- var FLAG_DISTRIBUTION = 1 << 2;
3085
- var FLAG_DRM = 1 << 4;
3086
- var MAX_RECORDS = 5e5;
3087
- function readRecords(data) {
3088
- const records = [];
3089
- let offset = 0;
3090
- while (offset + 4 <= data.length && records.length < MAX_RECORDS) {
3091
- const header = data.readUInt32LE(offset);
3092
- offset += 4;
3093
- const tagId = header & 1023;
3094
- const level = header >> 10 & 1023;
3095
- let size = header >> 20 & 4095;
3096
- if (size === 4095) {
3097
- if (offset + 4 > data.length) break;
3098
- size = data.readUInt32LE(offset);
3099
- offset += 4;
3123
+ function extractHwp5Images(fileIndex, blocks, warnings, sweepUnreferenced) {
3124
+ const binDataMap = /* @__PURE__ */ new Map();
3125
+ if (fileIndex) {
3126
+ for (const entry of fileIndex) {
3127
+ if (!entry?.name || !entry.content) continue;
3128
+ const match = entry.name.match(BIN_ENTRY_RE);
3129
+ if (!match) continue;
3130
+ const idx = parseInt(match[1], 16);
3131
+ const data = normalizeBinPayload(Buffer.from(entry.content));
3132
+ binDataMap.set(idx, { data, name: entry.name });
3100
3133
  }
3101
- if (offset + size > data.length) break;
3102
- records.push({ tagId, level, size, data: data.subarray(offset, offset + size) });
3103
- offset += size;
3104
3134
  }
3105
- return records;
3135
+ if (binDataMap.size === 0) {
3136
+ resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3137
+ return [];
3138
+ }
3139
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
3106
3140
  }
3107
- var MAX_DECOMPRESS_SIZE2 = 100 * 1024 * 1024;
3108
- function decompressStream(data) {
3109
- const opts = { maxOutputLength: MAX_DECOMPRESS_SIZE2 };
3110
- if (data.length >= 2 && data[0] === 120) {
3111
- try {
3112
- return inflateSync(data, opts);
3113
- } catch {
3114
- }
3141
+ function extractHwp5ImagesLenient(lcfb, blocks, warnings, sweepUnreferenced) {
3142
+ const binDataMap = /* @__PURE__ */ new Map();
3143
+ const binRe = /^BIN([0-9A-Fa-f]{4,8})(?:\.|$)/;
3144
+ for (const e of lcfb.entries()) {
3145
+ const match = e.name.match(binRe);
3146
+ if (!match) continue;
3147
+ const idx = parseInt(match[1], 16);
3148
+ const raw = lcfb.findStream(e.name);
3149
+ if (!raw) continue;
3150
+ binDataMap.set(idx, { data: normalizeBinPayload(raw), name: e.name });
3151
+ }
3152
+ if (binDataMap.size === 0) {
3153
+ resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3154
+ return [];
3115
3155
  }
3116
- return inflateRawSync2(data, opts);
3156
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
3117
3157
  }
3118
- function parseFileHeader(data) {
3119
- if (data.length < 40) throw new KordocError("FileHeader\uAC00 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (\uCD5C\uC18C 40\uBC14\uC774\uD2B8)");
3120
- const sig = data.subarray(0, 32).toString("utf8").replace(/\0+$/, "");
3121
- return {
3122
- signature: sig,
3123
- versionMajor: data[35],
3124
- flags: data.readUInt32LE(36)
3125
- };
3158
+
3159
+ // src/hwpx/images.ts
3160
+ function imageExtToMime(ext) {
3161
+ switch (ext.toLowerCase()) {
3162
+ case "jpg":
3163
+ case "jpeg":
3164
+ return "image/jpeg";
3165
+ case "png":
3166
+ return "image/png";
3167
+ case "gif":
3168
+ return "image/gif";
3169
+ case "bmp":
3170
+ return "image/bmp";
3171
+ case "tif":
3172
+ case "tiff":
3173
+ return "image/tiff";
3174
+ case "wmf":
3175
+ return "image/wmf";
3176
+ case "emf":
3177
+ return "image/emf";
3178
+ case "svg":
3179
+ return "image/svg+xml";
3180
+ default:
3181
+ return "application/octet-stream";
3182
+ }
3126
3183
  }
3127
- function readHwpString(data, offset) {
3128
- if (offset + 2 > data.length) return { value: "", next: data.length };
3129
- const len = data.readUInt16LE(offset);
3130
- const start = offset + 2;
3131
- const end = start + len * 2;
3132
- if (len === 0 || end > data.length) return { value: "", next: start };
3133
- return { value: data.subarray(start, end).toString("utf16le"), next: end };
3184
+ function mimeToExt(mime) {
3185
+ if (mime.includes("jpeg")) return "jpg";
3186
+ if (mime.includes("png")) return "png";
3187
+ if (mime.includes("gif")) return "gif";
3188
+ if (mime.includes("bmp")) return "bmp";
3189
+ if (mime.includes("tiff")) return "tif";
3190
+ if (mime.includes("wmf")) return "wmf";
3191
+ if (mime.includes("emf")) return "emf";
3192
+ if (mime.includes("svg")) return "svg";
3193
+ return "bin";
3134
3194
  }
3135
- function parseDocInfo(records) {
3136
- const charShapes = [];
3137
- const paraShapes = [];
3138
- const styles = [];
3139
- const binData = [];
3140
- const numberings = [];
3141
- const bullets = [];
3142
- for (const rec of records) {
3143
- if (rec.tagId === TAG_DOC_PARA_SHAPE && rec.data.length >= 4) {
3144
- const attr1 = rec.data.readUInt32LE(0);
3145
- const headType = attr1 >>> 23 & 3;
3146
- const paraLevel = attr1 >>> 25 & 7;
3147
- const numberingId = rec.data.length >= 32 ? rec.data.readUInt16LE(30) : 0;
3148
- paraShapes.push({ headType, paraLevel, numberingId });
3149
- }
3150
- if (rec.tagId === TAG_BIN_DATA && rec.data.length >= 2) {
3151
- const attr = rec.data.readUInt16LE(0);
3152
- const typeBits = attr & 15;
3153
- if (typeBits === 0) {
3154
- binData.push({ kind: "link", storageId: 0, extension: "" });
3155
- } else {
3156
- const storageId = rec.data.length >= 4 ? rec.data.readUInt16LE(2) : 0;
3157
- const { value: extension } = readHwpString(rec.data, 4);
3158
- binData.push({ kind: typeBits === 2 ? "storage" : "embed", storageId, extension });
3195
+ function collectImageBlocks2(blocks, out, ownerCell, depth = 0) {
3196
+ if (depth > MAX_XML_DEPTH) return;
3197
+ for (const block of blocks) {
3198
+ if (block.type === "image") {
3199
+ out.push({ block, ownerCell });
3200
+ } else if (block.type === "table" && block.table) {
3201
+ for (const row of block.table.cells) {
3202
+ for (const cell2 of row) {
3203
+ if (cell2.blocks?.length) collectImageBlocks2(cell2.blocks, out, cell2, depth + 1);
3204
+ }
3159
3205
  }
3160
3206
  }
3161
- if (rec.tagId === TAG_NUMBERING && rec.data.length >= 14) {
3162
- const levelFormats = [];
3163
- const numberFormats = [];
3164
- const startNumbers = [1, 1, 1, 1, 1, 1, 1];
3165
- let offset = 0;
3166
- for (let level = 0; level < 7; level++) {
3167
- if (offset + 12 > rec.data.length) {
3168
- levelFormats.push("");
3169
- numberFormats.push(0);
3170
- continue;
3207
+ }
3208
+ }
3209
+ async function extractImagesFromZip(zip, blocks, decompressed, warnings, sweepUnreferenced) {
3210
+ const images = [];
3211
+ let imageIndex = 0;
3212
+ const usedPaths = /* @__PURE__ */ new Set();
3213
+ const imageBlocks = [];
3214
+ collectImageBlocks2(blocks, imageBlocks);
3215
+ const resolved = /* @__PURE__ */ new Map();
3216
+ for (const { block, ownerCell } of imageBlocks) {
3217
+ if (block.type !== "image" || !block.text) continue;
3218
+ const ref = block.text;
3219
+ let img = resolved.get(ref);
3220
+ if (img === void 0) {
3221
+ img = null;
3222
+ const candidates = [
3223
+ `BinData/${ref}`,
3224
+ `Contents/BinData/${ref}`,
3225
+ ref
3226
+ // 절대 경로일 수도 있음
3227
+ ];
3228
+ let resolvedPath = null;
3229
+ if (!ref.includes(".")) {
3230
+ const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
3231
+ for (const prefix of prefixes) {
3232
+ const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
3233
+ if (match.length > 0) {
3234
+ resolvedPath = match[0].name;
3235
+ break;
3236
+ }
3171
3237
  }
3172
- const attr = rec.data.readUInt32LE(offset);
3173
- numberFormats.push(attr >>> 5 & 15);
3174
- offset += 12;
3175
- const { value, next } = readHwpString(rec.data, offset);
3176
- levelFormats.push(value);
3177
- offset = next;
3178
- }
3179
- let baseStart = 1;
3180
- if (offset + 2 <= rec.data.length) {
3181
- baseStart = rec.data.readUInt16LE(offset) || 1;
3182
- offset += 2;
3183
3238
  }
3184
- for (let level = 0; level < 7; level++) {
3185
- if (offset + 4 <= rec.data.length) {
3186
- startNumbers[level] = rec.data.readUInt32LE(offset) || 1;
3187
- offset += 4;
3188
- } else {
3189
- startNumbers[level] = baseStart;
3239
+ const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
3240
+ for (const path of allCandidates) {
3241
+ if (isPathTraversal(path)) continue;
3242
+ const file = zip.file(path);
3243
+ if (!file) continue;
3244
+ try {
3245
+ const data = await file.async("uint8array");
3246
+ decompressed.total += data.length;
3247
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3248
+ const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
3249
+ const mimeType = imageExtToMime(ext);
3250
+ imageIndex++;
3251
+ const filename2 = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
3252
+ img = { filename: filename2, data, mimeType };
3253
+ images.push(img);
3254
+ usedPaths.add(path);
3255
+ break;
3256
+ } catch (err) {
3257
+ if (err instanceof KordocError) throw err;
3190
3258
  }
3191
3259
  }
3192
- numberings.push({ levelFormats, numberFormats, startNumbers });
3193
- }
3194
- if (rec.tagId === TAG_BULLET && rec.data.length >= 14) {
3195
- const code = rec.data.readUInt16LE(12);
3196
- bullets.push({ char: code > 0 ? String.fromCharCode(code) : "\u2022" });
3260
+ if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
3261
+ resolved.set(ref, img);
3197
3262
  }
3198
- if (rec.tagId === TAG_DOC_CHAR_SHAPE && rec.data.length >= 18) {
3199
- if (rec.data.length >= 50) {
3200
- const fontSize = rec.data.readUInt32LE(42);
3201
- const attrFlags = rec.data.readUInt32LE(46);
3202
- charShapes.push({ fontSize, attrFlags });
3203
- } else {
3204
- charShapes.push({ fontSize: 0, attrFlags: 0 });
3205
- }
3263
+ if (!img) {
3264
+ block.type = "paragraph";
3265
+ block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
3266
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, () => `[\uC774\uBBF8\uC9C0: ${ref}]`);
3267
+ continue;
3206
3268
  }
3207
- if (rec.tagId === TAG_DOC_STYLE && rec.data.length >= 8) {
3269
+ const filename = img.filename;
3270
+ block.text = filename;
3271
+ block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
3272
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, () => `![image](${filename})`);
3273
+ }
3274
+ if (sweepUnreferenced) {
3275
+ const binEntries = zip.file(/(?:^|\/)BinData\//i);
3276
+ for (const file of binEntries) {
3277
+ if (file.dir || usedPaths.has(file.name) || isPathTraversal(file.name)) continue;
3208
3278
  try {
3209
- let offset = 0;
3210
- const nameLen = rec.data.readUInt16LE(offset);
3211
- offset += 2;
3212
- const nameBytes = nameLen * 2;
3213
- const name = nameBytes > 0 && offset + nameBytes <= rec.data.length ? rec.data.subarray(offset, offset + nameBytes).toString("utf16le") : "";
3214
- offset += nameBytes;
3215
- let nameKo = "";
3216
- if (offset + 2 <= rec.data.length) {
3217
- const nameKoLen = rec.data.readUInt16LE(offset);
3218
- offset += 2;
3219
- const nameKoBytes = nameKoLen * 2;
3220
- if (nameKoBytes > 0 && offset + nameKoBytes <= rec.data.length) {
3221
- nameKo = rec.data.subarray(offset, offset + nameKoBytes).toString("utf16le");
3222
- }
3223
- offset += nameKoBytes;
3279
+ const data = await file.async("uint8array");
3280
+ decompressed.total += data.length;
3281
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3282
+ const ext = file.name.includes(".") ? file.name.split(".").pop() || "" : "";
3283
+ let mimeType = imageExtToMime(ext);
3284
+ if (mimeType === "application/octet-stream") {
3285
+ const sniffed = detectImageMime(data);
3286
+ if (!sniffed) continue;
3287
+ mimeType = sniffed;
3224
3288
  }
3225
- const type = offset < rec.data.length ? rec.data.readUInt8(offset) : 0;
3226
- offset += 1;
3227
- offset += 1;
3228
- offset += 2;
3229
- const paraShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
3230
- offset += 2;
3231
- const charShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
3232
- styles.push({ name, nameKo, charShapeId, paraShapeId, type });
3233
- } catch {
3289
+ imageIndex++;
3290
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
3291
+ const img = { filename, data, mimeType };
3292
+ images.push(img);
3293
+ blocks.push({ type: "image", text: filename, imageData: { data, mimeType, filename: file.name } });
3294
+ } catch (err) {
3295
+ if (err instanceof KordocError) throw err;
3296
+ warnings?.push({ message: `\uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC2E4\uD328: ${file.name}`, code: "SKIPPED_IMAGE" });
3234
3297
  }
3235
3298
  }
3236
3299
  }
3237
- return { charShapes, paraShapes, styles, binData, numberings, bullets };
3300
+ return images;
3238
3301
  }
3239
- function createParaTextState() {
3240
- return { text: "", ctrlIdx: 0, fieldStack: [], fieldRanges: [] };
3302
+
3303
+ // src/hwpx/metadata.ts
3304
+ import JSZip2 from "jszip";
3305
+
3306
+ // src/hwpx/zip-sections.ts
3307
+ import { inflateRawSync as inflateRawSync2 } from "zlib";
3308
+ function extractFromBrokenZip(buffer) {
3309
+ const data = new Uint8Array(buffer);
3310
+ const view = new DataView(buffer);
3311
+ let pos = 0;
3312
+ const blocks = [];
3313
+ const warnings = [
3314
+ { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
3315
+ ];
3316
+ let totalDecompressed = 0;
3317
+ let entryCount = 0;
3318
+ let sectionNum = 0;
3319
+ const shared = createSectionShared();
3320
+ while (pos < data.length - 30) {
3321
+ if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
3322
+ pos++;
3323
+ while (pos < data.length - 30) {
3324
+ if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
3325
+ pos++;
3326
+ }
3327
+ continue;
3328
+ }
3329
+ if (++entryCount > MAX_ZIP_ENTRIES) break;
3330
+ const method = view.getUint16(pos + 8, true);
3331
+ const compSize = view.getUint32(pos + 18, true);
3332
+ const nameLen = view.getUint16(pos + 26, true);
3333
+ const extraLen = view.getUint16(pos + 28, true);
3334
+ if (nameLen > 1024 || extraLen > 65535) {
3335
+ pos += 30 + nameLen + extraLen;
3336
+ continue;
3337
+ }
3338
+ const fileStart = pos + 30 + nameLen + extraLen;
3339
+ if (fileStart + compSize > data.length) break;
3340
+ if (compSize === 0 && method !== 0) {
3341
+ pos = fileStart;
3342
+ continue;
3343
+ }
3344
+ const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
3345
+ const name = new TextDecoder().decode(nameBytes);
3346
+ if (isPathTraversal(name)) {
3347
+ pos = fileStart + compSize;
3348
+ continue;
3349
+ }
3350
+ const fileData = data.slice(fileStart, fileStart + compSize);
3351
+ pos = fileStart + compSize;
3352
+ if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
3353
+ try {
3354
+ let content;
3355
+ if (method === 0) {
3356
+ content = new TextDecoder().decode(fileData);
3357
+ } else if (method === 8) {
3358
+ const decompressed = inflateRawSync2(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
3359
+ content = new TextDecoder().decode(decompressed);
3360
+ } else {
3361
+ continue;
3362
+ }
3363
+ totalDecompressed += content.length * 2;
3364
+ if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
3365
+ sectionNum++;
3366
+ blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
3367
+ } catch {
3368
+ continue;
3369
+ }
3370
+ }
3371
+ 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");
3372
+ applyPageText(blocks, shared);
3373
+ const markdown = blocksToMarkdown(blocks);
3374
+ return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
3241
3375
  }
3242
- function isExtendedOnlyCtrlChar(ch) {
3243
- return ch >= 1 && ch <= 3 || ch >= 11 && ch <= 12 || ch >= 14 && ch <= 18 || ch >= 21 && ch <= 23;
3376
+ async function readKordocLayout(zip) {
3377
+ const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
3378
+ if (!file) return null;
3379
+ try {
3380
+ const xml = await file.async("text");
3381
+ if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
3382
+ return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
3383
+ } catch {
3384
+ return null;
3385
+ }
3244
3386
  }
3245
- function appendParaText(state, data, resolveControl) {
3246
- let result = "";
3247
- let i = 0;
3248
- const base = state.text.length;
3249
- const resolveAt = (byteOffset, extended) => {
3250
- const ctrlId = data.readUInt32LE(byteOffset);
3251
- const idx = extended ? state.ctrlIdx : -1;
3252
- const replacement = resolveControl?.(idx, ctrlId);
3253
- if (replacement) result += replacement;
3254
- if (extended) state.ctrlIdx++;
3255
- };
3256
- while (i + 1 < data.length) {
3257
- const ch = data.readUInt16LE(i);
3258
- i += 2;
3259
- switch (ch) {
3260
- // ── char 타입 (2바이트만, 확장 데이터 없음) ──
3261
- case CHAR_LINE:
3262
- result += "\n";
3263
- break;
3264
- case CHAR_SECTION_BREAK: {
3265
- if (i + 16 <= data.length && data.readUInt16LE(i) === 11) {
3266
- resolveAt(i + 2, true);
3267
- i += 16;
3268
- break;
3269
- }
3270
- result += "\n";
3271
- break;
3387
+ async function resolveSectionPaths(zip) {
3388
+ const manifestPaths = ["Contents/content.hpf", "content.hpf"];
3389
+ for (const mp of manifestPaths) {
3390
+ const mpLower = mp.toLowerCase();
3391
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
3392
+ if (!file) continue;
3393
+ const xml = await file.async("text");
3394
+ const paths = parseSectionPathsFromManifest(xml);
3395
+ if (paths.length > 0) return paths;
3396
+ }
3397
+ const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
3398
+ return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
3399
+ }
3400
+ function parseSectionPathsFromManifest(xml) {
3401
+ const parser = createXmlParser();
3402
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
3403
+ const items = doc.getElementsByTagName("opf:item");
3404
+ const spine = doc.getElementsByTagName("opf:itemref");
3405
+ const idToHref = /* @__PURE__ */ new Map();
3406
+ for (let i = 0; i < items.length; i++) {
3407
+ const item = items[i];
3408
+ const id = item.getAttribute("id") || "";
3409
+ const href = normalizeSectionHref(item.getAttribute("href") || "");
3410
+ if (id && href) idToHref.set(id, href);
3411
+ }
3412
+ if (spine.length > 0) {
3413
+ const ordered = [];
3414
+ for (let i = 0; i < spine.length; i++) {
3415
+ const href = idToHref.get(spine[i].getAttribute("idref") || "");
3416
+ if (href) ordered.push(href);
3417
+ }
3418
+ if (ordered.length > 0) return ordered;
3419
+ }
3420
+ return Array.from(idToHref.values()).sort(compareSectionPaths);
3421
+ }
3422
+
3423
+ // src/hwpx/metadata.ts
3424
+ async function extractHwpxMetadata(zip, metadata, decompressed) {
3425
+ try {
3426
+ const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
3427
+ for (const mp of metaPaths) {
3428
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
3429
+ if (!file) continue;
3430
+ const xml = await file.async("text");
3431
+ if (decompressed) {
3432
+ decompressed.total += xml.length * 2;
3433
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3272
3434
  }
3273
- case CHAR_PARA:
3274
- break;
3275
- // 문단 끝
3276
- case CHAR_HYPHEN:
3277
- result += "-";
3278
- break;
3279
- case CHAR_NBSP:
3280
- result += " ";
3281
- break;
3282
- case CHAR_FIXED_NBSP:
3283
- result += "\xA0";
3284
- break;
3285
- // 진짜 NBSP
3286
- case CHAR_FIXED_WIDTH:
3287
- result += " ";
3288
- break;
3289
- // 고정폭 공백
3290
- // ── inline 타입 (2바이트 + 14바이트 확장) ──
3291
- case CHAR_TAB:
3292
- result += " ";
3293
- if (i + 14 <= data.length) i += 14;
3294
- break;
3295
- default:
3296
- if (ch >= 1 && ch <= 31) {
3297
- const isExtended = isExtendedOnlyCtrlChar(ch);
3298
- const isInline = ch >= 4 && ch <= 9 || ch >= 19 && ch <= 20;
3299
- if ((isExtended || isInline) && i + 14 <= data.length) {
3300
- if (ch === 3) {
3301
- state.fieldStack.push({ start: base + result.length, ctrlIdx: state.ctrlIdx });
3302
- } else if (ch === 4) {
3303
- const open = state.fieldStack.pop();
3304
- if (open) {
3305
- state.fieldRanges.push({ start: open.start, end: base + result.length, ctrlIdx: open.ctrlIdx });
3306
- }
3307
- }
3308
- resolveAt(i, isExtended);
3309
- i += 14;
3310
- }
3311
- } else if (ch >= 32) {
3312
- if (ch >= 55296 && ch <= 56319 && i + 1 < data.length) {
3313
- const lo = data.readUInt16LE(i);
3314
- if (lo >= 56320 && lo <= 57343) {
3315
- i += 2;
3316
- const codePoint = (ch - 55296 << 10) + (lo - 56320) + 65536;
3317
- result += String.fromCodePoint(codePoint);
3318
- break;
3319
- }
3320
- }
3321
- result += String.fromCharCode(ch);
3435
+ parseDublinCoreMetadata(xml, metadata);
3436
+ if (metadata.title || metadata.author) return;
3437
+ }
3438
+ } catch {
3439
+ }
3440
+ }
3441
+ function parseDublinCoreMetadata(xml, metadata) {
3442
+ const parser = createXmlParser();
3443
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
3444
+ if (!doc.documentElement) return;
3445
+ const getText = (tagNames) => {
3446
+ for (const tag of tagNames) {
3447
+ const els = doc.getElementsByTagName(tag);
3448
+ if (els.length > 0) {
3449
+ const text = els[0].textContent?.trim();
3450
+ if (text) return text;
3451
+ }
3452
+ }
3453
+ return void 0;
3454
+ };
3455
+ metadata.title = metadata.title || getText(["dc:title", "title"]);
3456
+ metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
3457
+ metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
3458
+ metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
3459
+ metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
3460
+ const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
3461
+ if (keywords && !metadata.keywords) {
3462
+ metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
3463
+ }
3464
+ }
3465
+
3466
+ // src/hwpx/parser.ts
3467
+ async function parseHwpxDocument(buffer, options) {
3468
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
3469
+ let zip;
3470
+ try {
3471
+ zip = await JSZip3.loadAsync(buffer);
3472
+ } catch {
3473
+ return extractFromBrokenZip(buffer);
3474
+ }
3475
+ const actualEntryCount = Object.keys(zip.files).length;
3476
+ if (actualEntryCount > MAX_ZIP_ENTRIES) {
3477
+ throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3478
+ }
3479
+ const manifestFile = zip.file("META-INF/manifest.xml");
3480
+ if (manifestFile) {
3481
+ const manifestXml = await manifestFile.async("text");
3482
+ if (isEncryptedHwpx(manifestXml)) {
3483
+ if (isComFallbackAvailable() && options?.filePath) {
3484
+ const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
3485
+ if (pages.some((p) => p && p.trim().length > 0)) {
3486
+ return comResultToParseResult(pages, pageCount, warnings2);
3322
3487
  }
3323
- break;
3488
+ }
3489
+ 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.");
3324
3490
  }
3325
3491
  }
3326
- state.text += result;
3327
- }
3328
- function extractEquationText(data) {
3329
- if (data.length < 6) return null;
3330
- const scriptLength = data.readUInt16LE(4);
3331
- const scriptStart = 6;
3332
- const scriptEnd = scriptStart + scriptLength * 2;
3333
- if (scriptLength <= 0 || scriptEnd > data.length) return null;
3334
- const equation = data.subarray(scriptStart, scriptEnd).toString("utf16le").replace(/\0+/g, "").trim();
3335
- return equation || null;
3492
+ const decompressed = { total: 0 };
3493
+ const metadata = {};
3494
+ await extractHwpxMetadata(zip, metadata, decompressed);
3495
+ const styleMap = await extractHwpxStyles(zip, decompressed);
3496
+ const warnings = [];
3497
+ const sectionPaths = await resolveSectionPaths(zip);
3498
+ if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
3499
+ metadata.pageCount = sectionPaths.length;
3500
+ const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
3501
+ const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
3502
+ const blocks = [];
3503
+ const shared = createSectionShared();
3504
+ shared.kordocLayout = await readKordocLayout(zip);
3505
+ shared.keepTrailingEmptyCols = options?.keepTrailingEmptyCols;
3506
+ let parsedSections = 0;
3507
+ for (let si = 0; si < sectionPaths.length; si++) {
3508
+ if (pageFilter && !pageFilter.has(si + 1)) continue;
3509
+ const file = zip.file(sectionPaths[si]);
3510
+ if (!file) continue;
3511
+ try {
3512
+ const xml = await file.async("text");
3513
+ decompressed.total += xml.length * 2;
3514
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new ZipBombError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3515
+ blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
3516
+ parsedSections++;
3517
+ options?.onProgress?.(parsedSections, totalTarget);
3518
+ } catch (secErr) {
3519
+ if (secErr instanceof ZipBombError) throw secErr;
3520
+ 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" });
3521
+ }
3522
+ }
3523
+ applyPageText(blocks, shared);
3524
+ const images = await extractImagesFromZip(zip, blocks, decompressed, warnings, !pageFilter);
3525
+ detectHwpxHeadings(blocks, styleMap);
3526
+ const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
3527
+ const markdown = blocksToMarkdown(blocks);
3528
+ 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 };
3336
3529
  }
3337
3530
 
3338
3531
  // src/hwp5/numbering.ts
@@ -3443,350 +3636,74 @@ function formatLatin(n, upper) {
3443
3636
  }
3444
3637
  return result;
3445
3638
  }
3446
- function formatEastAsianNumber(n, digits, units, zero) {
3447
- if (n === 0) return zero;
3448
- if (n < 0 || n > 99999) return String(n);
3449
- let result = "";
3450
- let num4 = n;
3451
- let unit = 0;
3452
- while (num4 > 0) {
3453
- const d = num4 % 10;
3454
- if (d > 0) {
3455
- const digitStr = d === 1 && unit > 0 ? "" : digits[d];
3456
- result = digitStr + units[unit] + result;
3457
- }
3458
- num4 = Math.floor(num4 / 10);
3459
- unit++;
3460
- }
3461
- return result;
3462
- }
3463
- var HANGUL_DIGITS = ["", "\uC77C", "\uC774", "\uC0BC", "\uC0AC", "\uC624", "\uC721", "\uCE60", "\uD314", "\uAD6C"];
3464
- var HANGUL_UNITS = ["", "\uC2ED", "\uBC31", "\uCC9C", "\uB9CC"];
3465
- var HANJA_DIGITS = ["", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u4E03", "\u516B", "\u4E5D"];
3466
- var HANJA_UNITS = ["", "\u5341", "\u767E", "\u5343", "\u842C"];
3467
- function formatNumber(n, fmt) {
3468
- switch (fmt) {
3469
- case "circled":
3470
- return fromTable(n, CIRCLED_DIGITS);
3471
- case "romanUpper":
3472
- return formatRoman(n, true);
3473
- case "romanLower":
3474
- return formatRoman(n, false);
3475
- case "latinUpper":
3476
- return formatLatin(n, true) || String(n);
3477
- case "latinLower":
3478
- return formatLatin(n, false) || String(n);
3479
- case "ganada":
3480
- return fromTable(n, GANADA);
3481
- case "circledGanada":
3482
- return fromTable(n, CIRCLED_GANADA);
3483
- case "jamo":
3484
- return fromTable(n, JAMO);
3485
- case "circledJamo":
3486
- return fromTable(n, CIRCLED_JAMO);
3487
- case "hangulNum":
3488
- return formatEastAsianNumber(n, HANGUL_DIGITS, HANGUL_UNITS, "\uC601");
3489
- case "hanjaNum":
3490
- return formatEastAsianNumber(n, HANJA_DIGITS, HANJA_UNITS, "\u96F6");
3491
- default:
3492
- return String(n);
3493
- }
3494
- }
3495
- function expandNumberingFormat(formatStr, counters, numbering) {
3496
- let result = "";
3497
- let i = 0;
3498
- while (i < formatStr.length) {
3499
- const ch = formatStr[i];
3500
- if (ch === "^" && i + 1 < formatStr.length && formatStr[i + 1] >= "1" && formatStr[i + 1] <= "7") {
3501
- const levelRef = formatStr.charCodeAt(i + 1) - 48;
3502
- const idx = levelRef - 1;
3503
- const counterVal = counters[idx] ?? 0;
3504
- const start = numbering.startNumbers[idx] ?? 1;
3505
- const num4 = counterVal > 0 ? start - 1 + counterVal : start;
3506
- result += formatNumber(num4, headFormatToNumFmt(numbering.numberFormats[idx] ?? 0));
3507
- i += 2;
3508
- continue;
3509
- }
3510
- result += ch;
3511
- i++;
3512
- }
3513
- return result;
3514
- }
3515
-
3516
- // src/hwp5/images.ts
3517
- function detectImageMime(data) {
3518
- if (data.length < 4) return null;
3519
- if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
3520
- if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
3521
- if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
3522
- if (data[0] === 66 && data[1] === 77) return "image/bmp";
3523
- if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
3524
- if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
3525
- return null;
3526
- }
3527
- function normalizeBinPayload(data) {
3528
- if (detectImageMime(data)) return data;
3529
- try {
3530
- const inflated = decompressStream(data);
3531
- if (inflated.length > 0) return inflated;
3532
- } catch {
3533
- }
3534
- return data;
3535
- }
3536
- var BIN_ENTRY_RE = /(?:^|\/)BIN([0-9A-Fa-f]{4,8})(?:\.[^./\\]*)?$/;
3537
- function collectImageBlocks2(blocks, out) {
3538
- for (const b of blocks) {
3539
- if (b.type === "image") out.push(b);
3540
- if (b.table) {
3541
- for (const row of b.table.cells) {
3542
- for (const cell2 of row) {
3543
- if (cell2.blocks) collectImageBlocks2(cell2.blocks, out);
3544
- }
3545
- }
3546
- }
3547
- if (b.children) collectImageBlocks2(b.children, out);
3548
- }
3549
- }
3550
- function forEachTableCell(blocks, fn) {
3551
- for (const b of blocks) {
3552
- if (b.table) {
3553
- for (const row of b.table.cells) {
3554
- for (const cell2 of row) {
3555
- fn(cell2);
3556
- if (cell2.blocks) forEachTableCell(cell2.blocks, fn);
3557
- }
3558
- }
3559
- }
3560
- if (b.children) forEachTableCell(b.children, fn);
3561
- }
3562
- }
3563
- var CELL_IMAGE_SENTINEL_RE = /!\[image\]\(hwp5bin:(\d+)\)/g;
3564
- function resolveCellImageSentinels(blocks, renamed) {
3565
- forEachTableCell(blocks, (cell2) => {
3566
- if (!cell2.text.includes("hwp5bin:")) return;
3567
- cell2.text = cell2.text.replace(CELL_IMAGE_SENTINEL_RE, (_m, idStr) => {
3568
- const filename = renamed.get(Number(idStr));
3569
- return filename ? `![image](${filename})` : "[\uC774\uBBF8\uC9C0]";
3570
- });
3571
- });
3572
- }
3573
- function resolveImageBlocks(binDataMap, blocks, warnings) {
3574
- const imageBlocks = [];
3575
- collectImageBlocks2(blocks, imageBlocks);
3576
- if (imageBlocks.length === 0) return [];
3577
- const images = [];
3578
- const renamed = /* @__PURE__ */ new Map();
3579
- const resolved = /* @__PURE__ */ new Map();
3580
- let imageIndex = 0;
3581
- for (const block of imageBlocks) {
3582
- if (!block.text) continue;
3583
- const storageId = parseInt(block.text, 10);
3584
- if (isNaN(storageId)) continue;
3585
- let img = resolved.get(storageId);
3586
- if (img === void 0) {
3587
- const bin = binDataMap.get(storageId);
3588
- if (!bin) {
3589
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
3590
- resolved.set(storageId, null);
3591
- } else {
3592
- const mime = detectImageMime(bin.data);
3593
- if (!mime) {
3594
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
3595
- resolved.set(storageId, null);
3596
- } else {
3597
- imageIndex++;
3598
- const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
3599
- img = { filename: `image_${String(imageIndex).padStart(3, "0")}.${ext}`, data: new Uint8Array(bin.data), mime };
3600
- resolved.set(storageId, img);
3601
- images.push({ filename: img.filename, data: img.data, mimeType: img.mime });
3602
- renamed.set(storageId, img.filename);
3603
- }
3604
- }
3605
- img = resolved.get(storageId);
3606
- }
3607
- if (!img) {
3608
- const bin = binDataMap.get(storageId);
3609
- block.type = "paragraph";
3610
- block.text = bin ? `[\uC774\uBBF8\uC9C0: ${bin.name}]` : `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
3611
- continue;
3612
- }
3613
- block.text = img.filename;
3614
- block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
3615
- }
3616
- resolveCellImageSentinels(blocks, renamed);
3617
- return images;
3618
- }
3619
- function extractHwp5Images(fileIndex, blocks, warnings) {
3620
- const binDataMap = /* @__PURE__ */ new Map();
3621
- if (fileIndex) {
3622
- for (const entry of fileIndex) {
3623
- if (!entry?.name || !entry.content) continue;
3624
- const match = entry.name.match(BIN_ENTRY_RE);
3625
- if (!match) continue;
3626
- const idx = parseInt(match[1], 16);
3627
- const data = normalizeBinPayload(Buffer.from(entry.content));
3628
- binDataMap.set(idx, { data, name: entry.name });
3629
- }
3630
- }
3631
- if (binDataMap.size === 0) {
3632
- resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3633
- return [];
3634
- }
3635
- return resolveImageBlocks(binDataMap, blocks, warnings);
3636
- }
3637
- function extractHwp5ImagesLenient(lcfb, blocks, warnings) {
3638
- const binDataMap = /* @__PURE__ */ new Map();
3639
- const binRe = /^BIN([0-9A-Fa-f]{4,8})(?:\.|$)/;
3640
- for (const e of lcfb.entries()) {
3641
- const match = e.name.match(binRe);
3642
- if (!match) continue;
3643
- const idx = parseInt(match[1], 16);
3644
- const raw = lcfb.findStream(e.name);
3645
- if (!raw) continue;
3646
- binDataMap.set(idx, { data: normalizeBinPayload(raw), name: e.name });
3647
- }
3648
- if (binDataMap.size === 0) {
3649
- resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3650
- return [];
3651
- }
3652
- return resolveImageBlocks(binDataMap, blocks, warnings);
3653
- }
3654
-
3655
- // src/image/transcode.ts
3656
- import { deflateSync } from "zlib";
3657
- var CRC_TABLE = (() => {
3658
- const table2 = new Uint32Array(256);
3659
- for (let n = 0; n < 256; n++) {
3660
- let c = n;
3661
- for (let k = 0; k < 8; k++) {
3662
- c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
3639
+ function formatEastAsianNumber(n, digits, units, zero) {
3640
+ if (n === 0) return zero;
3641
+ if (n < 0 || n > 99999) return String(n);
3642
+ let result = "";
3643
+ let num4 = n;
3644
+ let unit = 0;
3645
+ while (num4 > 0) {
3646
+ const d = num4 % 10;
3647
+ if (d > 0) {
3648
+ const digitStr = d === 1 && unit > 0 ? "" : digits[d];
3649
+ result = digitStr + units[unit] + result;
3663
3650
  }
3664
- table2[n] = c >>> 0;
3651
+ num4 = Math.floor(num4 / 10);
3652
+ unit++;
3665
3653
  }
3666
- return table2;
3667
- })();
3668
- function crc32(bytes) {
3669
- let c = 4294967295;
3670
- for (let i = 0; i < bytes.length; i++) {
3671
- c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
3672
- }
3673
- return (c ^ 4294967295) >>> 0;
3674
- }
3675
- var BI_RGB = 0;
3676
- var MAX_DIM = 32767;
3677
- var MAX_PIXELS = 36e6;
3678
- function bmpToPng(bmp) {
3679
- if (bmp.length < 54) return null;
3680
- if (bmp[0] !== 66 || bmp[1] !== 77) return null;
3681
- const dv = new DataView(bmp.buffer, bmp.byteOffset, bmp.byteLength);
3682
- const dataOffset = dv.getUint32(10, true);
3683
- const headerSize = dv.getUint32(14, true);
3684
- if (headerSize < 40) return null;
3685
- const width = dv.getInt32(18, true);
3686
- const rawHeight = dv.getInt32(22, true);
3687
- const bitCount = dv.getUint16(28, true);
3688
- const compression = dv.getUint32(30, true);
3689
- if (compression !== BI_RGB) return null;
3690
- if (bitCount !== 24 && bitCount !== 32) return null;
3691
- if (width <= 0 || rawHeight === 0) return null;
3692
- if (width > MAX_DIM || Math.abs(rawHeight) > MAX_DIM) return null;
3693
- const topDown = rawHeight < 0;
3694
- const height = Math.abs(rawHeight);
3695
- if (width * height > MAX_PIXELS) return null;
3696
- const bytesPerPixel = bitCount >> 3;
3697
- const rowStride = width * bytesPerPixel + 3 & ~3;
3698
- if (dataOffset + rowStride * height > bmp.length) return null;
3699
- const rgba = new Uint8Array(width * height * 4);
3700
- let anyAlpha = 0;
3701
- for (let y = 0; y < height; y++) {
3702
- const srcRow = topDown ? y : height - 1 - y;
3703
- let src = dataOffset + srcRow * rowStride;
3704
- let dst = y * width * 4;
3705
- for (let x = 0; x < width; x++) {
3706
- rgba[dst] = bmp[src + 2];
3707
- rgba[dst + 1] = bmp[src + 1];
3708
- rgba[dst + 2] = bmp[src];
3709
- const a = bitCount === 32 ? bmp[src + 3] : 255;
3710
- rgba[dst + 3] = a;
3711
- anyAlpha |= a;
3712
- src += bytesPerPixel;
3713
- dst += 4;
3714
- }
3715
- }
3716
- if (bitCount === 32 && anyAlpha === 0) {
3717
- for (let i = 3; i < rgba.length; i += 4) rgba[i] = 255;
3718
- }
3719
- return encodePng(width, height, rgba);
3720
- }
3721
- var PNG_SIGNATURE = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
3722
- function chunk(type, data) {
3723
- const body = new Uint8Array(4 + data.length);
3724
- body[0] = type.charCodeAt(0);
3725
- body[1] = type.charCodeAt(1);
3726
- body[2] = type.charCodeAt(2);
3727
- body[3] = type.charCodeAt(3);
3728
- body.set(data, 4);
3729
- const out = new Uint8Array(8 + data.length + 4);
3730
- const dv = new DataView(out.buffer);
3731
- dv.setUint32(0, data.length, false);
3732
- out.set(body, 4);
3733
- dv.setUint32(8 + data.length, crc32(body), false);
3734
- return out;
3654
+ return result;
3735
3655
  }
3736
- function encodePng(width, height, rgba) {
3737
- const ihdr = new Uint8Array(13);
3738
- const iv = new DataView(ihdr.buffer);
3739
- iv.setUint32(0, width, false);
3740
- iv.setUint32(4, height, false);
3741
- ihdr[8] = 8;
3742
- ihdr[9] = 6;
3743
- const stride = width * 4;
3744
- const raw = new Uint8Array((stride + 1) * height);
3745
- for (let y = 0; y < height; y++) {
3746
- const rowStart = y * (stride + 1);
3747
- raw[rowStart] = 0;
3748
- raw.set(rgba.subarray(y * stride, y * stride + stride), rowStart + 1);
3749
- }
3750
- const idat = deflateSync(raw);
3751
- const ihdrChunk = chunk("IHDR", ihdr);
3752
- const idatChunk = chunk("IDAT", idat);
3753
- const iendChunk = chunk("IEND", new Uint8Array(0));
3754
- const out = new Uint8Array(PNG_SIGNATURE.length + ihdrChunk.length + idatChunk.length + iendChunk.length);
3755
- let o = 0;
3756
- out.set(PNG_SIGNATURE, o);
3757
- o += PNG_SIGNATURE.length;
3758
- out.set(ihdrChunk, o);
3759
- o += ihdrChunk.length;
3760
- out.set(idatChunk, o);
3761
- o += idatChunk.length;
3762
- out.set(iendChunk, o);
3763
- return out;
3656
+ var HANGUL_DIGITS = ["", "\uC77C", "\uC774", "\uC0BC", "\uC0AC", "\uC624", "\uC721", "\uCE60", "\uD314", "\uAD6C"];
3657
+ var HANGUL_UNITS = ["", "\uC2ED", "\uBC31", "\uCC9C", "\uB9CC"];
3658
+ var HANJA_DIGITS = ["", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u4E03", "\u516B", "\u4E5D"];
3659
+ var HANJA_UNITS = ["", "\u5341", "\u767E", "\u5343", "\u842C"];
3660
+ function formatNumber(n, fmt) {
3661
+ switch (fmt) {
3662
+ case "circled":
3663
+ return fromTable(n, CIRCLED_DIGITS);
3664
+ case "romanUpper":
3665
+ return formatRoman(n, true);
3666
+ case "romanLower":
3667
+ return formatRoman(n, false);
3668
+ case "latinUpper":
3669
+ return formatLatin(n, true) || String(n);
3670
+ case "latinLower":
3671
+ return formatLatin(n, false) || String(n);
3672
+ case "ganada":
3673
+ return fromTable(n, GANADA);
3674
+ case "circledGanada":
3675
+ return fromTable(n, CIRCLED_GANADA);
3676
+ case "jamo":
3677
+ return fromTable(n, JAMO);
3678
+ case "circledJamo":
3679
+ return fromTable(n, CIRCLED_JAMO);
3680
+ case "hangulNum":
3681
+ return formatEastAsianNumber(n, HANGUL_DIGITS, HANGUL_UNITS, "\uC601");
3682
+ case "hanjaNum":
3683
+ return formatEastAsianNumber(n, HANJA_DIGITS, HANJA_UNITS, "\u96F6");
3684
+ default:
3685
+ return String(n);
3686
+ }
3764
3687
  }
3765
- function escapeRegExp(s) {
3766
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3767
- }
3768
- function inlineImagesIntoMarkdown(markdown, images, opts) {
3769
- const compress = opts?.compress !== false;
3770
- let out = markdown;
3771
- for (const img of images) {
3772
- let bytes = img.data;
3773
- let mime = img.mimeType;
3774
- if (compress && mime === "image/bmp") {
3775
- const png = bmpToPng(img.data);
3776
- if (png) {
3777
- bytes = png;
3778
- mime = "image/png";
3779
- }
3780
- }
3781
- const base64 = Buffer.from(bytes).toString("base64");
3782
- const dataUri = `data:${mime};base64,${base64}`;
3783
- const name = escapeRegExp(img.filename);
3784
- const mdRe = new RegExp(`!\\[image\\]\\((?:images/)?${name}\\)`, "g");
3785
- out = out.replace(mdRe, () => `![image](${dataUri})`);
3786
- const imgTagRe = new RegExp(`(<img\\b[^>]*\\bsrc=")(?:images/)?${name}(")`, "g");
3787
- out = out.replace(imgTagRe, (_m, pre, post) => `${pre}${dataUri}${post}`);
3688
+ function expandNumberingFormat(formatStr, counters, numbering) {
3689
+ let result = "";
3690
+ let i = 0;
3691
+ while (i < formatStr.length) {
3692
+ const ch = formatStr[i];
3693
+ if (ch === "^" && i + 1 < formatStr.length && formatStr[i + 1] >= "1" && formatStr[i + 1] <= "7") {
3694
+ const levelRef = formatStr.charCodeAt(i + 1) - 48;
3695
+ const idx = levelRef - 1;
3696
+ const counterVal = counters[idx] ?? 0;
3697
+ const start = numbering.startNumbers[idx] ?? 1;
3698
+ const num4 = counterVal > 0 ? start - 1 + counterVal : start;
3699
+ result += formatNumber(num4, headFormatToNumFmt(numbering.numberFormats[idx] ?? 0));
3700
+ i += 2;
3701
+ continue;
3702
+ }
3703
+ result += ch;
3704
+ i++;
3788
3705
  }
3789
- return out;
3706
+ return result;
3790
3707
  }
3791
3708
 
3792
3709
  // src/hwp5/aes.ts
@@ -4816,6 +4733,7 @@ function parseHwp5Document(buffer, options) {
4816
4733
  const totalTarget = pageFilter ? pageFilter.size : sections.length;
4817
4734
  const bodyBlocks = [];
4818
4735
  const doc = createHwp5DocState();
4736
+ doc.keepTrailingEmptyCols = options?.keepTrailingEmptyCols;
4819
4737
  let totalDecompressed = 0;
4820
4738
  let parsedSections = 0;
4821
4739
  for (let si = 0; si < sections.length; si++) {
@@ -4836,7 +4754,7 @@ function parseHwp5Document(buffer, options) {
4836
4754
  }
4837
4755
  }
4838
4756
  const blocks = [...doc.headerBlocks, ...bodyBlocks, ...doc.footerBlocks];
4839
- const images = cfb ? extractHwp5Images(cfb.FileIndex, blocks, warnings) : extractHwp5ImagesLenient(lenientCfb, blocks, warnings);
4757
+ const images = cfb ? extractHwp5Images(cfb.FileIndex, blocks, warnings, !pageFilter) : extractHwp5ImagesLenient(lenientCfb, blocks, warnings, !pageFilter);
4840
4758
  let flatBlocks = flattenLayoutTables(blocks);
4841
4759
  if (options?.dedupeRunningHeaders) {
4842
4760
  const deduped = dedupeRunningHeaders(flatBlocks);
@@ -5417,7 +5335,7 @@ function parseTableControl(ctrl, records, ctx) {
5417
5335
  return table3;
5418
5336
  }
5419
5337
  const cellRows = arrangeCells(rows, cols, cells);
5420
- const table2 = buildTable(cellRows);
5338
+ const table2 = buildTable(cellRows, { keepAnchoredEmptyCols: ctx.doc.keepTrailingEmptyCols });
5421
5339
  if (caption && table2.rows > 0) table2.caption = caption;
5422
5340
  return table2.rows > 0 ? table2 : null;
5423
5341
  }
@@ -17611,6 +17529,7 @@ function readHeader(reader) {
17611
17529
  }
17612
17530
 
17613
17531
  // src/hwp3/parser.ts
17532
+ var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
17614
17533
  var PARA_SHAPE_SIZE = 187;
17615
17534
  var LINE_INFO_SIZE = 14;
17616
17535
  var INLINE_CHAR_SHAPE_SIZE = 31;
@@ -17646,8 +17565,11 @@ function parseHwp3Document(buffer, _options) {
17646
17565
  const warnings = [];
17647
17566
  if (header.compressed !== 0) {
17648
17567
  try {
17649
- body = inflateRawSync3(tail);
17568
+ body = inflateRawSync3(tail, { maxOutputLength: MAX_DECOMPRESS_SIZE3 });
17650
17569
  } catch (err) {
17570
+ if (err?.code === "ERR_BUFFER_TOO_LARGE") {
17571
+ throw new Error(`HWP3 \uC555\uCD95 \uD574\uC81C \uACB0\uACFC\uAC00 \uCD5C\uB300 \uD5C8\uC6A9 \uD06C\uAE30(${MAX_DECOMPRESS_SIZE3 / 1024 / 1024}MB)\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4`);
17572
+ }
17651
17573
  const msg2 = err instanceof Error ? err.message : String(err);
17652
17574
  throw new Error(`HWP3 \uC555\uCD95 \uD574\uC81C \uC2E4\uD328: ${msg2}`);
17653
17575
  }
@@ -17844,7 +17766,7 @@ function isDistributionSentinel(markdown) {
17844
17766
  import JSZip4 from "jszip";
17845
17767
  import { DOMParser as DOMParser2 } from "@xmldom/xmldom";
17846
17768
  var MAX_SHEETS = 100;
17847
- var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
17769
+ var MAX_DECOMPRESS_SIZE4 = 100 * 1024 * 1024;
17848
17770
  var MAX_ROWS2 = 1e4;
17849
17771
  var MAX_COLS2 = 200;
17850
17772
  function cleanNumericValue(raw) {
@@ -17884,16 +17806,87 @@ function getTextContent(el) {
17884
17806
  function parseXml(text) {
17885
17807
  return new DOMParser2().parseFromString(stripDtd(text), "text/xml");
17886
17808
  }
17809
+ function collectRichText(root) {
17810
+ let out = "";
17811
+ const walk = (node) => {
17812
+ const children = node.childNodes;
17813
+ for (let i = 0; i < children.length; i++) {
17814
+ if (children[i].nodeType !== 1) continue;
17815
+ const el = children[i];
17816
+ const local = el.localName || el.tagName?.replace(/^[^:]+:/, "") || "";
17817
+ if (local === "rPh") continue;
17818
+ if (local === "t") out += el.textContent ?? "";
17819
+ else walk(el);
17820
+ }
17821
+ };
17822
+ walk(root);
17823
+ return out;
17824
+ }
17887
17825
  function parseSharedStrings(xml) {
17888
17826
  const doc = parseXml(xml);
17889
17827
  const strings = [];
17890
17828
  const siList = getElements(doc.documentElement, "si");
17891
17829
  for (const si of siList) {
17892
- const tElements = getElements(si, "t");
17893
- strings.push(tElements.map((t) => t.textContent ?? "").join(""));
17830
+ strings.push(collectRichText(si));
17894
17831
  }
17895
17832
  return strings;
17896
17833
  }
17834
+ var BUILTIN_DATE_FMT = /* @__PURE__ */ new Map([
17835
+ [14, "date"],
17836
+ [15, "date"],
17837
+ [16, "date"],
17838
+ [17, "date"],
17839
+ [18, "datetime"],
17840
+ [19, "datetime"],
17841
+ [20, "datetime"],
17842
+ [21, "datetime"],
17843
+ [22, "datetime"],
17844
+ [45, "datetime"],
17845
+ [46, "datetime"],
17846
+ [47, "datetime"]
17847
+ ]);
17848
+ function classifyDateFormat(code) {
17849
+ const stripped = code.replace(/"[^"]*"/g, "").replace(/\[[^\]]*\]/g, "").replace(/\\./g, "");
17850
+ if (!/[ymdh]/i.test(stripped)) return null;
17851
+ return /[hs]/i.test(stripped) ? "datetime" : "date";
17852
+ }
17853
+ function dateKindOfFmt(fmtId, customFormats) {
17854
+ const builtin = BUILTIN_DATE_FMT.get(fmtId);
17855
+ if (builtin) return builtin;
17856
+ const code = customFormats.get(fmtId);
17857
+ return code !== void 0 ? classifyDateFormat(code) : null;
17858
+ }
17859
+ function dateSerialToIso(serial, date1904, kind) {
17860
+ if (!isFinite(serial) || serial < 0) return null;
17861
+ let days = serial;
17862
+ if (date1904) days += 1462;
17863
+ else if (days < 60) days += 1;
17864
+ const ms = Math.round((days - 25569) * 864e5);
17865
+ const d = new Date(ms);
17866
+ if (isNaN(d.getTime()) || d.getUTCFullYear() > 9999) return null;
17867
+ const iso = d.toISOString();
17868
+ return kind === "datetime" ? iso.slice(0, 19) : iso.slice(0, 10);
17869
+ }
17870
+ function parseStyleDateXfs(xml) {
17871
+ const doc = parseXml(xml);
17872
+ const customFormats = /* @__PURE__ */ new Map();
17873
+ for (const el of getElements(doc.documentElement, "numFmt")) {
17874
+ const id = parseInt(el.getAttribute("numFmtId") ?? "", 10);
17875
+ if (isNaN(id)) continue;
17876
+ customFormats.set(id, el.getAttribute("formatCode") ?? "");
17877
+ }
17878
+ const dateXfs = /* @__PURE__ */ new Map();
17879
+ const cellXfsEls = getElements(doc.documentElement, "cellXfs");
17880
+ if (cellXfsEls.length === 0) return dateXfs;
17881
+ const xfs = getElements(cellXfsEls[0], "xf");
17882
+ for (let i = 0; i < xfs.length; i++) {
17883
+ const fmtId = parseInt(xfs[i].getAttribute("numFmtId") ?? "", 10);
17884
+ if (isNaN(fmtId)) continue;
17885
+ const kind = dateKindOfFmt(fmtId, customFormats);
17886
+ if (kind) dateXfs.set(i, kind);
17887
+ }
17888
+ return dateXfs;
17889
+ }
17897
17890
  function parseWorkbook(xml) {
17898
17891
  const doc = parseXml(xml);
17899
17892
  const sheets = [];
@@ -17905,7 +17898,9 @@ function parseWorkbook(xml) {
17905
17898
  rId: el.getAttribute("r:id") ?? ""
17906
17899
  });
17907
17900
  }
17908
- return sheets;
17901
+ const prEls = getElements(doc.documentElement, "workbookPr");
17902
+ const d1904 = prEls.length > 0 ? prEls[0].getAttribute("date1904") : null;
17903
+ return { sheets, date1904: d1904 === "1" || d1904 === "true" };
17909
17904
  }
17910
17905
  function parseRels(xml) {
17911
17906
  const doc = parseXml(xml);
@@ -17918,21 +17913,25 @@ function parseRels(xml) {
17918
17913
  }
17919
17914
  return map;
17920
17915
  }
17921
- function parseWorksheet(xml, sharedStrings) {
17916
+ function parseWorksheet(xml, sharedStrings, dateXfs, date1904) {
17922
17917
  const doc = parseXml(xml);
17923
17918
  const grid = [];
17924
17919
  let maxRow = 0;
17925
17920
  let maxCol = 0;
17926
17921
  const rows = getElements(doc.documentElement, "row");
17922
+ let prevRow = -1;
17927
17923
  for (const rowEl of rows) {
17928
- const rowNum = parseInt(rowEl.getAttribute("r") ?? "0", 10) - 1;
17924
+ const rAttr = rowEl.getAttribute("r");
17925
+ const rowNum = rAttr !== null ? parseInt(rAttr, 10) - 1 : prevRow + 1;
17929
17926
  if (rowNum < 0 || rowNum >= MAX_ROWS2) continue;
17927
+ if (Number.isFinite(rowNum)) prevRow = rowNum;
17930
17928
  const cells = getElements(rowEl, "c");
17929
+ let prevCol = -1;
17931
17930
  for (const cellEl of cells) {
17932
17931
  const ref = cellEl.getAttribute("r");
17933
- if (!ref) continue;
17934
- const pos = parseCellRef(ref);
17935
- if (!pos || pos.col >= MAX_COLS2) continue;
17932
+ const pos = ref !== null ? parseCellRef(ref) : { col: prevCol + 1, row: rowNum };
17933
+ if (!pos || !Number.isFinite(pos.row) || pos.row < 0 || pos.row >= MAX_ROWS2 || pos.col >= MAX_COLS2) continue;
17934
+ prevCol = pos.col;
17936
17935
  const type = cellEl.getAttribute("t");
17937
17936
  const vElements = getElements(cellEl, "v");
17938
17937
  const fElements = getElements(cellEl, "f");
@@ -17946,12 +17945,19 @@ function parseWorksheet(xml, sharedStrings) {
17946
17945
  value = raw === "1" ? "TRUE" : "FALSE";
17947
17946
  } else {
17948
17947
  value = cleanNumericValue(raw);
17948
+ if (type === null || type === "n") {
17949
+ const sAttr = cellEl.getAttribute("s");
17950
+ const kind = sAttr !== null ? dateXfs.get(parseInt(sAttr, 10)) : void 0;
17951
+ if (kind) {
17952
+ const iso = dateSerialToIso(parseFloat(raw), date1904, kind);
17953
+ if (iso) value = iso;
17954
+ }
17955
+ }
17949
17956
  }
17950
17957
  } else if (type === "inlineStr") {
17951
17958
  const isEl = getElements(cellEl, "is");
17952
17959
  if (isEl.length > 0) {
17953
- const tElements = getElements(isEl[0], "t");
17954
- value = tElements.map((t) => t.textContent ?? "").join("");
17960
+ value = collectRichText(isEl[0]);
17955
17961
  }
17956
17962
  }
17957
17963
  if (!value && fElements.length > 0) {
@@ -17970,7 +17976,14 @@ function parseWorksheet(xml, sharedStrings) {
17970
17976
  const ref = el.getAttribute("ref");
17971
17977
  if (!ref) continue;
17972
17978
  const m = parseMergeRef(ref);
17973
- if (m) merges.push(m);
17979
+ if (m) {
17980
+ merges.push({
17981
+ startCol: Math.min(m.startCol, MAX_COLS2 - 1),
17982
+ startRow: Math.min(m.startRow, MAX_ROWS2 - 1),
17983
+ endCol: Math.min(m.endCol, MAX_COLS2 - 1),
17984
+ endRow: Math.min(m.endRow, MAX_ROWS2 - 1)
17985
+ });
17986
+ }
17974
17987
  }
17975
17988
  return { grid, merges, maxRow, maxCol };
17976
17989
  }
@@ -18034,7 +18047,7 @@ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex) {
18034
18047
  return blocks;
18035
18048
  }
18036
18049
  async function parseXlsxDocument(buffer, options) {
18037
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE3);
18050
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
18038
18051
  const zip = await JSZip4.loadAsync(buffer);
18039
18052
  const warnings = [];
18040
18053
  const workbookFile = zip.file("xl/workbook.xml");
@@ -18046,10 +18059,18 @@ async function parseXlsxDocument(buffer, options) {
18046
18059
  if (ssFile) {
18047
18060
  sharedStrings = parseSharedStrings(await ssFile.async("text"));
18048
18061
  }
18049
- const sheets = parseWorkbook(await workbookFile.async("text"));
18062
+ const { sheets, date1904 } = parseWorkbook(await workbookFile.async("text"));
18050
18063
  if (sheets.length === 0) {
18051
18064
  throw new KordocError("XLSX \uD30C\uC77C\uC5D0 \uC2DC\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4");
18052
18065
  }
18066
+ let dateXfs = /* @__PURE__ */ new Map();
18067
+ const stylesFile = zip.file("xl/styles.xml");
18068
+ if (stylesFile) {
18069
+ try {
18070
+ dateXfs = parseStyleDateXfs(await stylesFile.async("text"));
18071
+ } catch {
18072
+ }
18073
+ }
18053
18074
  let relsMap = /* @__PURE__ */ new Map();
18054
18075
  const relsFile = zip.file("xl/_rels/workbook.xml.rels");
18055
18076
  if (relsFile) {
@@ -18057,7 +18078,7 @@ async function parseXlsxDocument(buffer, options) {
18057
18078
  }
18058
18079
  let pageFilter = null;
18059
18080
  if (options?.pages) {
18060
- const { parsePageRange: parsePageRange2 } = await import("./page-range-CIRRJPXJ.js");
18081
+ const { parsePageRange: parsePageRange2 } = await import("./page-range-LNMXJU6W.js");
18061
18082
  pageFilter = parsePageRange2(options.pages, sheets.length);
18062
18083
  }
18063
18084
  const blocks = [];
@@ -18087,7 +18108,7 @@ async function parseXlsxDocument(buffer, options) {
18087
18108
  }
18088
18109
  try {
18089
18110
  const sheetXml = await sheetFile.async("text");
18090
- const { grid, merges, maxRow, maxCol } = parseWorksheet(sheetXml, sharedStrings);
18111
+ const { grid, merges, maxRow, maxCol } = parseWorksheet(sheetXml, sharedStrings, dateXfs, date1904);
18091
18112
  const sheetBlocks = sheetToBlocks(sheet.name, grid, merges, maxRow, maxCol, i);
18092
18113
  blocks.push(...sheetBlocks);
18093
18114
  } catch (err) {
@@ -18131,7 +18152,10 @@ var OP_CONTINUE = 60;
18131
18152
  var OP_BOUNDSHEET8 = 133;
18132
18153
  var OP_SST = 252;
18133
18154
  var OP_CODEPAGE = 66;
18155
+ var OP_DATE1904 = 34;
18134
18156
  var OP_FILEPASS = 47;
18157
+ var OP_FORMAT = 1054;
18158
+ var OP_XF = 224;
18135
18159
  var OP_NUMBER = 515;
18136
18160
  var OP_RK = 638;
18137
18161
  var OP_MULRK = 189;
@@ -18143,6 +18167,8 @@ var OP_BOOLERR = 517;
18143
18167
  var OP_BLANK = 513;
18144
18168
  var OP_MULBLANK = 190;
18145
18169
  var OP_MERGECELLS = 229;
18170
+ var OP_SHRFMLA = 1212;
18171
+ var OP_ARRAY = 545;
18146
18172
  var DT_GLOBALS = 5;
18147
18173
  var DT_WORKSHEET = 16;
18148
18174
  var MAX_RECORDS2 = 1e6;
@@ -18238,7 +18264,7 @@ function decodeUtf16Le(buf) {
18238
18264
  }
18239
18265
 
18240
18266
  // src/xls/sst.ts
18241
- function parseString(buf, offset, segments) {
18267
+ function parseString(buf, offset, segments, segCursor) {
18242
18268
  if (offset + 3 > buf.length) return null;
18243
18269
  const cch = buf.readUInt16LE(offset);
18244
18270
  let flags = buf.readUInt8(offset + 2);
@@ -18261,7 +18287,15 @@ function parseString(buf, offset, segments) {
18261
18287
  const charBytes = [];
18262
18288
  let charsRead = 0;
18263
18289
  while (charsRead < cch) {
18264
- const nextBoundary = segments.find((s) => s > off) ?? buf.length;
18290
+ while (segCursor.idx < segments.length && segments[segCursor.idx] < off) segCursor.idx++;
18291
+ if (segCursor.idx < segments.length && segments[segCursor.idx] === off) {
18292
+ if (off >= buf.length) return null;
18293
+ flags = buf.readUInt8(off);
18294
+ highByte = (flags & 1) !== 0;
18295
+ off += 1;
18296
+ segCursor.idx++;
18297
+ }
18298
+ const nextBoundary = segCursor.idx < segments.length ? segments[segCursor.idx] : buf.length;
18265
18299
  const remainChars = cch - charsRead;
18266
18300
  const bytesPerChar = highByte ? 2 : 1;
18267
18301
  const bytesAvail = nextBoundary - off;
@@ -18272,12 +18306,10 @@ function parseString(buf, offset, segments) {
18272
18306
  charBytes.push(highByte ? slice : padToUtf16(slice));
18273
18307
  off += bytesToRead;
18274
18308
  charsRead += charsInThisRun;
18275
- }
18276
- if (charsRead < cch) {
18277
- if (off >= buf.length) return null;
18278
- flags = buf.readUInt8(off);
18279
- highByte = (flags & 1) !== 0;
18280
- off += 1;
18309
+ } else if (nextBoundary < buf.length) {
18310
+ off = nextBoundary;
18311
+ } else {
18312
+ return null;
18281
18313
  }
18282
18314
  }
18283
18315
  const text = decodeUtf16Le(Buffer.concat(charBytes));
@@ -18302,8 +18334,9 @@ function decodeSST(records) {
18302
18334
  const cstUnique = combined.readUInt32LE(4);
18303
18335
  const strings = [];
18304
18336
  let off = 8;
18337
+ const segCursor = { idx: 0 };
18305
18338
  for (let i = 0; i < cstUnique && off < combined.length; i++) {
18306
- const r = parseString(combined, off, segments);
18339
+ const r = parseString(combined, off, segments, segCursor);
18307
18340
  if (!r) break;
18308
18341
  strings.push(r.text);
18309
18342
  off += r.consumed;
@@ -18378,7 +18411,7 @@ function decodeFormulaStringRecord(data) {
18378
18411
  return decodeUtf16Le(padded);
18379
18412
  }
18380
18413
  }
18381
- function extractSheetCells(records, bofIndex, sst) {
18414
+ function extractSheetCells(records, bofIndex, sst, convertNum) {
18382
18415
  const cells = [];
18383
18416
  const merges = [];
18384
18417
  const bofOffset = records[bofIndex].offset;
@@ -18396,14 +18429,16 @@ function extractSheetCells(records, bofIndex, sst) {
18396
18429
  case OP_NUMBER: {
18397
18430
  const h = readCellHeader(rec.data);
18398
18431
  if (h && rec.data.length >= 14) {
18399
- cells.push({ row: h.row, col: h.col, value: rec.data.readDoubleLE(6) });
18432
+ const n = rec.data.readDoubleLE(6);
18433
+ cells.push({ row: h.row, col: h.col, value: convertNum ? convertNum(n, h.ixfe) : n });
18400
18434
  }
18401
18435
  break;
18402
18436
  }
18403
18437
  case OP_RK: {
18404
18438
  const h = readCellHeader(rec.data);
18405
18439
  if (h && rec.data.length >= 10) {
18406
- cells.push({ row: h.row, col: h.col, value: decodeRk(rec.data.readInt32LE(6)) });
18440
+ const n = decodeRk(rec.data.readInt32LE(6));
18441
+ cells.push({ row: h.row, col: h.col, value: convertNum ? convertNum(n, h.ixfe) : n });
18407
18442
  }
18408
18443
  break;
18409
18444
  }
@@ -18411,7 +18446,7 @@ function extractSheetCells(records, bofIndex, sst) {
18411
18446
  const m = decodeMulRk(rec.data);
18412
18447
  if (m) {
18413
18448
  for (const c of m.cells) {
18414
- cells.push({ row: m.row, col: c.col, value: c.value });
18449
+ cells.push({ row: m.row, col: c.col, value: convertNum ? convertNum(c.value, c.ixfe) : c.value });
18415
18450
  }
18416
18451
  }
18417
18452
  break;
@@ -18436,19 +18471,26 @@ function extractSheetCells(records, bofIndex, sst) {
18436
18471
  if (h && rec.data.length >= 14) {
18437
18472
  const result = decodeFormulaResult(rec.data.subarray(6, 14));
18438
18473
  if (result.kind === "stringRef") {
18439
- const next = records[i + 1];
18474
+ let j = i + 1;
18475
+ while (j < records.length && (records[j].opcode === OP_SHRFMLA || records[j].opcode === OP_ARRAY)) j++;
18476
+ const next = records[j];
18440
18477
  if (next && next.opcode === OP_STRING) {
18441
18478
  cells.push({
18442
18479
  row: h.row,
18443
18480
  col: h.col,
18444
18481
  value: decodeFormulaStringRecord(next.data)
18445
18482
  });
18446
- i++;
18483
+ i = j;
18447
18484
  } else {
18448
18485
  cells.push({ row: h.row, col: h.col, value: "" });
18449
18486
  }
18450
18487
  } else {
18451
- cells.push({ row: h.row, col: h.col, value: result.value });
18488
+ const v = result.value;
18489
+ cells.push({
18490
+ row: h.row,
18491
+ col: h.col,
18492
+ value: convertNum && typeof v === "number" ? convertNum(v, h.ixfe) : v
18493
+ });
18452
18494
  }
18453
18495
  }
18454
18496
  break;
@@ -18498,7 +18540,7 @@ function extractSheetCells(records, bofIndex, sst) {
18498
18540
 
18499
18541
  // src/xls/parser.ts
18500
18542
  var MAX_SHEETS2 = 100;
18501
- var MAX_ROWS3 = 1e5;
18543
+ var MAX_ROWS3 = 65536;
18502
18544
  var MAX_COLS3 = 1e3;
18503
18545
  function decodeBoundSheet(data) {
18504
18546
  if (data.length < 8) return null;
@@ -18521,10 +18563,33 @@ function decodeBoundSheet(data) {
18521
18563
  }
18522
18564
  return { name, lbPlyPos, dt };
18523
18565
  }
18566
+ function decodeFormatRecord(data) {
18567
+ if (data.length < 5) return null;
18568
+ const ifmt = data.readUInt16LE(0);
18569
+ const cch = data.readUInt16LE(2);
18570
+ const flags = data.readUInt8(4);
18571
+ const highByte = (flags & 1) !== 0;
18572
+ const start = 5;
18573
+ let code;
18574
+ if (highByte) {
18575
+ const end = Math.min(start + cch * 2, data.length);
18576
+ code = decodeUtf16Le(data.subarray(start, end));
18577
+ } else {
18578
+ const end = Math.min(start + cch, data.length);
18579
+ const slice = data.subarray(start, end);
18580
+ const padded = Buffer.alloc(slice.length * 2);
18581
+ for (let i = 0; i < slice.length; i++) padded[i * 2] = slice[i];
18582
+ code = decodeUtf16Le(padded);
18583
+ }
18584
+ return { ifmt, code };
18585
+ }
18524
18586
  function processGlobals(records) {
18525
18587
  const sheets = [];
18526
18588
  let codePage = 1200;
18527
18589
  let encrypted = false;
18590
+ let date1904 = false;
18591
+ const customFormats = /* @__PURE__ */ new Map();
18592
+ const xfFmtIds = [];
18528
18593
  const firstBof = records[0];
18529
18594
  if (!firstBof || firstBof.opcode !== OP_BOF) {
18530
18595
  throw new KordocError("XLS: \uCCAB \uB808\uCF54\uB4DC\uAC00 BOF\uAC00 \uC544\uB2D8");
@@ -18547,21 +18612,41 @@ function processGlobals(records) {
18547
18612
  codePage = r.data.readUInt16LE(0);
18548
18613
  } else if (r.opcode === OP_FILEPASS) {
18549
18614
  encrypted = true;
18615
+ } else if (r.opcode === OP_DATE1904 && r.data.length >= 2) {
18616
+ date1904 = r.data.readUInt16LE(0) === 1;
18617
+ } else if (r.opcode === OP_FORMAT) {
18618
+ const f = decodeFormatRecord(r.data);
18619
+ if (f) customFormats.set(f.ifmt, f.code);
18620
+ } else if (r.opcode === OP_XF && r.data.length >= 4) {
18621
+ xfFmtIds.push(r.data.readUInt16LE(2));
18550
18622
  }
18551
18623
  i++;
18552
18624
  }
18625
+ const dateXfs = /* @__PURE__ */ new Map();
18626
+ for (let k = 0; k < xfFmtIds.length; k++) {
18627
+ const kind = dateKindOfFmt(xfFmtIds[k], customFormats);
18628
+ if (kind) dateXfs.set(k, kind);
18629
+ }
18553
18630
  const globalsRecords = records.slice(0, i);
18554
18631
  const sst = decodeSST(globalsRecords);
18555
- return { sheets, sst, codePage, encrypted, endIndex: i };
18632
+ return { sheets, sst, codePage, encrypted, dateXfs, date1904, endIndex: i };
18556
18633
  }
18557
18634
  function findSheetBofIndex(records, lbPlyPos) {
18558
18635
  const exact = records.findIndex(
18559
18636
  (r) => r.opcode === OP_BOF && r.offset === lbPlyPos
18560
18637
  );
18561
18638
  if (exact >= 0) return exact;
18562
- const bofIndices = records.map((r, idx) => r.opcode === OP_BOF ? idx : -1).filter((idx) => idx >= 0);
18563
- if (bofIndices.length === 0) return -1;
18564
- return bofIndices.length > 1 ? bofIndices[1] : -1;
18639
+ let best = -1;
18640
+ let bestOffset = Infinity;
18641
+ for (let idx = 1; idx < records.length; idx++) {
18642
+ const r = records[idx];
18643
+ if (r.opcode !== OP_BOF) continue;
18644
+ if (r.offset >= lbPlyPos && r.offset < bestOffset) {
18645
+ best = idx;
18646
+ bestOffset = r.offset;
18647
+ }
18648
+ }
18649
+ return best;
18565
18650
  }
18566
18651
  function cellValueToText(v) {
18567
18652
  if (v === null || v === void 0) return "";
@@ -18692,10 +18777,18 @@ async function parseXlsDocument(buffer, options) {
18692
18777
  ]
18693
18778
  };
18694
18779
  }
18780
+ const convertNum = globals.dateXfs.size > 0 ? (n, ixfe) => {
18781
+ const kind = globals.dateXfs.get(ixfe);
18782
+ if (kind) {
18783
+ const iso = dateSerialToIso(n, globals.date1904, kind);
18784
+ if (iso) return iso;
18785
+ }
18786
+ return n;
18787
+ } : void 0;
18695
18788
  const totalSheets = Math.min(globals.sheets.length, MAX_SHEETS2);
18696
18789
  let pageFilter = null;
18697
18790
  if (options?.pages) {
18698
- const { parsePageRange: parsePageRange2 } = await import("./page-range-CIRRJPXJ.js");
18791
+ const { parsePageRange: parsePageRange2 } = await import("./page-range-LNMXJU6W.js");
18699
18792
  pageFilter = parsePageRange2(options.pages, totalSheets);
18700
18793
  }
18701
18794
  const allBlocks = [];
@@ -18718,7 +18811,7 @@ async function parseXlsDocument(buffer, options) {
18718
18811
  continue;
18719
18812
  }
18720
18813
  try {
18721
- const { sheet } = extractSheetCells(records, bofIdx, globals.sst);
18814
+ const { sheet } = extractSheetCells(records, bofIdx, globals.sst, convertNum);
18722
18815
  const blocks = sheetToBlocks2(meta.name, sheet, i);
18723
18816
  allBlocks.push(...blocks);
18724
18817
  } catch (e) {
@@ -18846,6 +18939,9 @@ var NARY_MAP = {
18846
18939
  "\u2A02": "\\bigotimes",
18847
18940
  "\u2A00": "\\bigodot"
18848
18941
  };
18942
+ function onOffVal(v) {
18943
+ return v !== "0" && v !== "false" && v !== "off";
18944
+ }
18849
18945
  function mapDelim(ch, isLeft) {
18850
18946
  const l = {
18851
18947
  "(": "(",
@@ -18872,10 +18968,23 @@ function mapDelim(ch, isLeft) {
18872
18968
  const map = isLeft ? l : r;
18873
18969
  return map[ch] ?? ch;
18874
18970
  }
18971
+ function isBalancedWrap(s) {
18972
+ let depth = 0;
18973
+ for (let i = 0; i < s.length; i++) {
18974
+ const ch = s[i];
18975
+ if (ch === "{" && s[i - 1] !== "\\") depth++;
18976
+ else if (ch === "}" && s[i - 1] !== "\\") {
18977
+ depth--;
18978
+ if (depth === 0) return i === s.length - 1;
18979
+ if (depth < 0) return false;
18980
+ }
18981
+ }
18982
+ return false;
18983
+ }
18875
18984
  function grp(body) {
18876
18985
  const s = body.trim();
18877
18986
  if (s.length === 0) return "{}";
18878
- if (s.startsWith("{") && s.endsWith("}")) return s;
18987
+ if (s.startsWith("{") && s.endsWith("}") && isBalancedWrap(s)) return s;
18879
18988
  return "{" + s + "}";
18880
18989
  }
18881
18990
  function childrenToLatex(parent) {
@@ -18969,8 +19078,8 @@ function nodeToLatex(el) {
18969
19078
  }
18970
19079
  const sh = firstKid(naryPr, "subHide");
18971
19080
  const ph = firstKid(naryPr, "supHide");
18972
- if (sh) subHide = (sh.getAttribute("m:val") ?? sh.getAttribute("val")) !== "0";
18973
- if (ph) supHide = (ph.getAttribute("m:val") ?? ph.getAttribute("val")) !== "0";
19081
+ if (sh) subHide = onOffVal(sh.getAttribute("m:val") ?? sh.getAttribute("val"));
19082
+ if (ph) supHide = onOffVal(ph.getAttribute("m:val") ?? ph.getAttribute("val"));
18974
19083
  const ll = firstKid(naryPr, "limLoc");
18975
19084
  if (ll) limLoc = ll.getAttribute("m:val") ?? ll.getAttribute("val") ?? "";
18976
19085
  }
@@ -19117,7 +19226,7 @@ function isDisplayMath(el) {
19117
19226
  }
19118
19227
 
19119
19228
  // src/docx/parser.ts
19120
- var MAX_DECOMPRESS_SIZE4 = 100 * 1024 * 1024;
19229
+ var MAX_DECOMPRESS_SIZE5 = 100 * 1024 * 1024;
19121
19230
  function matchesLocal(el, localName2) {
19122
19231
  return el.localName === localName2 || (el.tagName?.endsWith(`:${localName2}`) ?? false);
19123
19232
  }
@@ -19135,6 +19244,8 @@ function effectiveChildElements(parent) {
19135
19244
  result.push(...effectiveChildElements(c));
19136
19245
  }
19137
19246
  }
19247
+ } else if (matchesLocal(el, "ins") || matchesLocal(el, "smartTag")) {
19248
+ result.push(...effectiveChildElements(el));
19138
19249
  } else {
19139
19250
  result.push(el);
19140
19251
  }
@@ -19199,6 +19310,21 @@ function parseStyles(xml) {
19199
19310
  }
19200
19311
  styles.set(styleId, { name, basedOn, outlineLevel });
19201
19312
  }
19313
+ for (const [styleId, info] of styles) {
19314
+ if (info.outlineLevel !== void 0) continue;
19315
+ const seen = /* @__PURE__ */ new Set([styleId]);
19316
+ let cur = info.basedOn;
19317
+ while (cur && !seen.has(cur)) {
19318
+ seen.add(cur);
19319
+ const parent = styles.get(cur);
19320
+ if (!parent) break;
19321
+ if (parent.outlineLevel !== void 0) {
19322
+ info.outlineLevel = parent.outlineLevel;
19323
+ break;
19324
+ }
19325
+ cur = parent.basedOn;
19326
+ }
19327
+ }
19202
19328
  return styles;
19203
19329
  }
19204
19330
  function parseNumbering(xml) {
@@ -19286,8 +19412,12 @@ function collectOmmlRoots(p) {
19286
19412
  return out;
19287
19413
  }
19288
19414
  function extractRun(r) {
19289
- const tElements = getChildElements(r, "t");
19290
- const text = tElements.map((t) => t.textContent ?? "").join("");
19415
+ let text = "";
19416
+ for (const el of effectiveChildElements(r)) {
19417
+ if (matchesLocal(el, "t")) text += el.textContent ?? "";
19418
+ else if (matchesLocal(el, "br") || matchesLocal(el, "cr")) text += "\n";
19419
+ else if (matchesLocal(el, "tab")) text += " ";
19420
+ }
19291
19421
  let bold = false;
19292
19422
  let italic = false;
19293
19423
  const rPrEls = getChildElements(r, "rPr");
@@ -19442,7 +19572,7 @@ function collectTextboxParagraphs(node, inTxbx = false, out = [], depth = 0) {
19442
19572
  }
19443
19573
  return out;
19444
19574
  }
19445
- function parseTable(tbl2, styles, numbering, footnotes, rels) {
19575
+ function parseTable(tbl2, styles, numbering, footnotes, rels, keepEmptyCols) {
19446
19576
  const trElements = getChildElements(tbl2, "tr");
19447
19577
  if (trElements.length === 0) return null;
19448
19578
  const rawRows = [];
@@ -19504,7 +19634,7 @@ ${cell2.text}` : cell2.text;
19504
19634
  return { text: cell2.text, colSpan: cell2.colSpan, rowSpan, colAddr: cell2.col, rowAddr: r };
19505
19635
  })
19506
19636
  );
19507
- const table2 = buildTable(cellRows);
19637
+ const table2 = buildTable(cellRows, { keepAnchoredEmptyCols: keepEmptyCols });
19508
19638
  if (table2.rows === 0 || table2.cols === 0) return null;
19509
19639
  return { type: "table", table: table2 };
19510
19640
  }
@@ -19533,9 +19663,17 @@ async function buildImageMap(zip, rels, doc, warnings) {
19533
19663
  const imageMap = /* @__PURE__ */ new Map();
19534
19664
  const images = [];
19535
19665
  let imgIdx = 0;
19666
+ const imageRefs = [];
19536
19667
  for (const blip of findElements(doc.documentElement, "blip")) {
19537
19668
  const embedId = getAttr(blip, "embed");
19538
- if (!embedId || imageMap.has(embedId)) continue;
19669
+ if (embedId) imageRefs.push(embedId);
19670
+ }
19671
+ for (const imagedata of collectNonFallbackImagedata(doc.documentElement)) {
19672
+ const embedId = getAttr(imagedata, "id");
19673
+ if (embedId) imageRefs.push(embedId);
19674
+ }
19675
+ for (const embedId of imageRefs) {
19676
+ if (imageMap.has(embedId)) continue;
19539
19677
  const target = rels.get(embedId);
19540
19678
  if (!target) continue;
19541
19679
  const imgPath = target.startsWith("/") ? target.slice(1) : target.startsWith("word/") ? target : `word/${target}`;
@@ -19570,14 +19708,26 @@ function collectParagraphBlips(node, out = [], depth = 0) {
19570
19708
  if (depth > 40) return out;
19571
19709
  for (const el of effectiveChildElements(node)) {
19572
19710
  if (matchesLocal(el, "txbxContent") || matchesLocal(el, "Fallback")) continue;
19573
- if (matchesLocal(el, "blip")) out.push(el);
19711
+ if (matchesLocal(el, "blip") || matchesLocal(el, "imagedata")) out.push(el);
19574
19712
  else collectParagraphBlips(el, out, depth + 1);
19575
19713
  }
19576
19714
  return out;
19577
19715
  }
19716
+ function collectNonFallbackImagedata(node, out = [], depth = 0) {
19717
+ if (depth > 60) return out;
19718
+ const children = node.childNodes;
19719
+ for (let i = 0; i < children.length; i++) {
19720
+ if (children[i].nodeType !== 1) continue;
19721
+ const el = children[i];
19722
+ if (matchesLocal(el, "Fallback")) continue;
19723
+ if (matchesLocal(el, "imagedata")) out.push(el);
19724
+ else collectNonFallbackImagedata(el, out, depth + 1);
19725
+ }
19726
+ return out;
19727
+ }
19578
19728
  function emitParagraphImages(p, imageMap, linked, out) {
19579
19729
  for (const blip of collectParagraphBlips(p)) {
19580
- const embedId = getAttr(blip, "embed");
19730
+ const embedId = getAttr(blip, "embed") ?? getAttr(blip, "id");
19581
19731
  if (!embedId) continue;
19582
19732
  const filename = imageMap.get(embedId);
19583
19733
  if (!filename) continue;
@@ -19586,7 +19736,7 @@ function emitParagraphImages(p, imageMap, linked, out) {
19586
19736
  }
19587
19737
  }
19588
19738
  async function parseDocxDocument(buffer, options) {
19589
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
19739
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE5);
19590
19740
  const zip = await JSZip5.loadAsync(buffer);
19591
19741
  const warnings = [];
19592
19742
  const docFile = zip.file("word/document.xml");
@@ -19657,7 +19807,7 @@ async function parseDocxDocument(buffer, options) {
19657
19807
  if (imageMap.size > 0) emitParagraphImages(tp, imageMap, linkedImages, blocks);
19658
19808
  }
19659
19809
  } else if (localName2 === "tbl") {
19660
- const block = parseTable(el, styles, numbering, footnotes, rels);
19810
+ const block = parseTable(el, styles, numbering, footnotes, rels, options?.keepTrailingEmptyCols);
19661
19811
  if (block) blocks.push(block);
19662
19812
  }
19663
19813
  }
@@ -19749,7 +19899,7 @@ function parseHwpmlDocument(buffer, options) {
19749
19899
  if (localName(el) !== "SECTION") continue;
19750
19900
  sectionIdx++;
19751
19901
  if (pageFilter && !pageFilter.has(sectionIdx)) continue;
19752
- parseSection2(el, blocks, paraShapeMap, sectionIdx, warnings);
19902
+ parseSection2(el, blocks, paraShapeMap, sectionIdx, warnings, options?.keepTrailingEmptyCols ?? false);
19753
19903
  }
19754
19904
  const outline = blocks.filter((b) => b.type === "heading" && b.text).map((b) => ({ level: b.level ?? 1, text: b.text, pageNumber: b.pageNumber }));
19755
19905
  const markdown = blocksToMarkdown(blocks);
@@ -19785,10 +19935,10 @@ function buildParaShapeMap(root) {
19785
19935
  }
19786
19936
  return map;
19787
19937
  }
19788
- function parseSection2(section, blocks, paraShapeMap, sectionNum, warnings) {
19789
- walkContent(section, blocks, paraShapeMap, sectionNum, warnings, false);
19938
+ function parseSection2(section, blocks, paraShapeMap, sectionNum, warnings, keep) {
19939
+ walkContent(section, blocks, paraShapeMap, sectionNum, warnings, false, keep);
19790
19940
  }
19791
- function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, depth = 0) {
19941
+ function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, keep, depth = 0) {
19792
19942
  if (depth > MAX_XML_DEPTH2) return;
19793
19943
  const children = node.childNodes;
19794
19944
  for (let i = 0; i < children.length; i++) {
@@ -19801,24 +19951,24 @@ function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderF
19801
19951
  if (tag === "P") {
19802
19952
  if (!inHeaderFooter) {
19803
19953
  parseParagraph3(el, blocks, paraShapeMap, sectionNum);
19804
- walkTablesInP(el, blocks, paraShapeMap, sectionNum, warnings);
19954
+ walkTablesInP(el, blocks, paraShapeMap, sectionNum, warnings, keep);
19805
19955
  }
19806
19956
  continue;
19807
19957
  }
19808
19958
  if (tag === "TABLE") {
19809
19959
  if (!inHeaderFooter) {
19810
- parseTable2(el, blocks, paraShapeMap, sectionNum, warnings);
19960
+ parseTable2(el, blocks, paraShapeMap, sectionNum, warnings, keep);
19811
19961
  }
19812
19962
  continue;
19813
19963
  }
19814
19964
  if (tag === "PARALIST" || tag === "SECTION" || tag === "COLDEF") {
19815
- walkContent(el, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, depth + 1);
19965
+ walkContent(el, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, keep, depth + 1);
19816
19966
  continue;
19817
19967
  }
19818
- walkContent(el, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, depth + 1);
19968
+ walkContent(el, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, keep, depth + 1);
19819
19969
  }
19820
19970
  }
19821
- function walkTablesInP(node, blocks, paraShapeMap, sectionNum, warnings, depth = 0) {
19971
+ function walkTablesInP(node, blocks, paraShapeMap, sectionNum, warnings, keep, depth = 0) {
19822
19972
  if (depth > MAX_XML_DEPTH2) return;
19823
19973
  const children = node.childNodes;
19824
19974
  for (let i = 0; i < children.length; i++) {
@@ -19826,11 +19976,11 @@ function walkTablesInP(node, blocks, paraShapeMap, sectionNum, warnings, depth =
19826
19976
  if (el.nodeType !== 1) continue;
19827
19977
  const tag = localName(el);
19828
19978
  if (tag === "TABLE") {
19829
- parseTable2(el, blocks, paraShapeMap, sectionNum, warnings);
19979
+ parseTable2(el, blocks, paraShapeMap, sectionNum, warnings, keep);
19830
19980
  continue;
19831
19981
  }
19832
19982
  if (tag === "FOOTNOTE" || tag === "ENDNOTE" || tag === "HEADER" || tag === "FOOTER") continue;
19833
- walkTablesInP(el, blocks, paraShapeMap, sectionNum, warnings, depth + 1);
19983
+ walkTablesInP(el, blocks, paraShapeMap, sectionNum, warnings, keep, depth + 1);
19834
19984
  }
19835
19985
  }
19836
19986
  function parseParagraph3(el, blocks, paraShapeMap, sectionNum) {
@@ -19866,7 +20016,7 @@ function collectCharText(node, parts, depth = 0) {
19866
20016
  }
19867
20017
  }
19868
20018
  }
19869
- function parseTable2(el, blocks, paraShapeMap, sectionNum, warnings) {
20019
+ function parseTable2(el, blocks, paraShapeMap, sectionNum, warnings, keep) {
19870
20020
  const cells = [];
19871
20021
  const rowCount = parseInt(el.getAttribute("RowCount") ?? "0", 10);
19872
20022
  const colCount = parseInt(el.getAttribute("ColCount") ?? "0", 10);
@@ -19910,7 +20060,7 @@ function parseTable2(el, blocks, paraShapeMap, sectionNum, warnings) {
19910
20060
  const cellRows = grid.map(
19911
20061
  (row) => row.map((cell2) => cell2 ?? { text: "", colSpan: 1, rowSpan: 1 })
19912
20062
  );
19913
- const table2 = buildTable(cellRows);
20063
+ const table2 = buildTable(cellRows, { keepAnchoredEmptyCols: keep });
19914
20064
  const caption = extractShapeCaption(el);
19915
20065
  if (caption.text && caption.before) {
19916
20066
  blocks.push({ type: "paragraph", text: caption.text, pageNumber: sectionNum });
@@ -20146,7 +20296,7 @@ function findMatchingKey(cellLabel, values) {
20146
20296
  bestKey = key;
20147
20297
  }
20148
20298
  } else if (key.startsWith(cellLabel)) {
20149
- if (cellLabel.length >= key.length * 0.6 && cellLabel.length > bestLen) {
20299
+ if (cellLabel.length >= key.length * 0.75 && cellLabel.length > bestLen) {
20150
20300
  bestLen = cellLabel.length;
20151
20301
  bestKey = key;
20152
20302
  }
@@ -20186,7 +20336,7 @@ function fillInCellPatterns(cellText, values, matchedLabels, blockedLabels) {
20186
20336
  const matchKey = values.available(normalizedKw) ? normalizedKw : void 0;
20187
20337
  if (matchKey === void 0) return match;
20188
20338
  const val = values.peek(matchKey);
20189
- const isTruthy = ["\u2611", "\u2713", "\u2714", "v", "V", "true", "1", "yes", "o", "O"].includes(val.trim()) || val.trim() === "";
20339
+ const isTruthy = ["\u2611", "\u2713", "\u2714", "v", "V", "true", "1", "yes", "o", "O"].includes(val.trim());
20190
20340
  if (!isTruthy) return match;
20191
20341
  values.consume(matchKey);
20192
20342
  matchedLabels.add(matchKey);
@@ -20208,14 +20358,20 @@ function fillInCellPatterns(cellText, values, matchedLabels, blockedLabels) {
20208
20358
  );
20209
20359
  return matches.length > 0 ? { text, matches } : null;
20210
20360
  }
20211
- var INLINE_LABEL_RE = /([가-힣A-Za-z]{2,10})\s*[::]/g;
20361
+ var INLINE_LABEL_RE = /((?:[가-힣A-Za-z]{1,10} )?)([가-힣A-Za-z]{2,10})\s*[::]/g;
20212
20362
  function scanInlineSegments(text) {
20213
20363
  const labels = [];
20214
20364
  INLINE_LABEL_RE.lastIndex = 0;
20215
20365
  let m;
20216
20366
  while ((m = INLINE_LABEL_RE.exec(text)) !== null) {
20217
20367
  if (text[INLINE_LABEL_RE.lastIndex] === "/") continue;
20218
- labels.push({ label: m[1], start: m.index, end: INLINE_LABEL_RE.lastIndex });
20368
+ labels.push({
20369
+ label: m[2],
20370
+ ext: m[1] ? m[1] + m[2] : void 0,
20371
+ extStart: m[1] ? m.index : void 0,
20372
+ start: m.index + m[1].length,
20373
+ end: INLINE_LABEL_RE.lastIndex
20374
+ });
20219
20375
  }
20220
20376
  const segments = [];
20221
20377
  for (let i = 0; i < labels.length; i++) {
@@ -20233,21 +20389,44 @@ function scanInlineSegments(text) {
20233
20389
  labelStart: cur.start,
20234
20390
  valueStart: vs,
20235
20391
  valueEnd: ve,
20236
- value: text.slice(vs, ve)
20392
+ value: text.slice(vs, ve),
20393
+ ...cur.ext !== void 0 ? { extLabel: cur.ext, extStart: cur.extStart } : {}
20237
20394
  });
20238
20395
  }
20239
20396
  return segments;
20240
20397
  }
20398
+ function matchInlineSegment(seg, values, blockedLabels) {
20399
+ const nlabel = normalizeLabel(seg.label);
20400
+ if (seg.extLabel !== void 0) {
20401
+ const nExt = normalizeLabel(seg.extLabel);
20402
+ if (nExt !== nlabel && !blockedLabels?.has(nExt) && values.has(nExt)) {
20403
+ return { key: nExt, label: seg.extLabel, viaExt: true };
20404
+ }
20405
+ }
20406
+ if (blockedLabels?.has(nlabel)) return void 0;
20407
+ const key = findMatchingKey(nlabel, values);
20408
+ return key !== void 0 ? { key, label: seg.label, viaExt: false } : void 0;
20409
+ }
20410
+ function clampSegmentEnd(text, seg, next, nextViaExt) {
20411
+ let ve = seg.valueEnd;
20412
+ if (nextViaExt && next?.extStart !== void 0 && next.extStart < ve) ve = next.extStart;
20413
+ while (ve > seg.valueStart && /\s/.test(text[ve - 1])) ve--;
20414
+ return ve;
20415
+ }
20241
20416
  function padInsertion(text, pos, value) {
20242
20417
  const lead = pos > 0 && !/\s/.test(text[pos - 1]) ? " " : "";
20243
20418
  const trail = pos < text.length && !/\s/.test(text[pos]) ? " " : "";
20244
20419
  return lead + value + trail;
20245
20420
  }
20246
- function normalizeValues(values) {
20421
+ function normalizeValues(values, warnings) {
20247
20422
  const map = /* @__PURE__ */ new Map();
20248
20423
  for (const [label, raw] of Object.entries(values)) {
20249
20424
  const { value, format } = typeof raw === "object" && !Array.isArray(raw) ? raw : { value: raw, format: void 0 };
20250
- map.set(normalizeLabel(label), Array.isArray(value) ? value.map((v) => formatFillValue(v, format)) : formatFillValue(value, format));
20425
+ const key = normalizeLabel(label);
20426
+ if (map.has(key)) {
20427
+ warnings?.push(`\uC785\uB825 \uB77C\uBCA8 "${label}"\uC774 \uC815\uADDC\uD654 \uD0A4 "${key}"\uC5D0\uC11C \uB2E4\uB978 \uB77C\uBCA8\uACFC \uCDA9\uB3CC \u2014 \uB4A4 \uAC12\uC73C\uB85C \uB36E\uC5B4\uC500`);
20428
+ }
20429
+ map.set(key, Array.isArray(value) ? value.map((v) => formatFillValue(v, format)) : formatFillValue(value, format));
20251
20430
  }
20252
20431
  return map;
20253
20432
  }
@@ -20436,10 +20615,10 @@ function extractFromTable(table2) {
20436
20615
  }
20437
20616
  if (fields.length === 0 && table2.rows >= 2 && table2.cols >= 2) {
20438
20617
  const headerRow = table2.cells[0];
20439
- const allLabels = headerRow.every((cell2) => {
20440
- const t = cell2.text.trim();
20618
+ const allLabels = headerRow?.every((cell2) => {
20619
+ const t = cell2?.text.trim() ?? "";
20441
20620
  return t.length > 0 && t.length <= 20;
20442
- });
20621
+ }) ?? false;
20443
20622
  if (allLabels) {
20444
20623
  for (let r = 1; r < table2.rows; r++) {
20445
20624
  for (let c = 0; c < table2.cols; c++) {
@@ -20530,7 +20709,8 @@ function fillFormFields(blocks, values, blockedLabels) {
20530
20709
  const cloned = structuredClone(blocks);
20531
20710
  const filled = [];
20532
20711
  const matchedLabels = /* @__PURE__ */ new Set();
20533
- const normalizedValues = normalizeValues(values);
20712
+ const warnings = [];
20713
+ const normalizedValues = normalizeValues(values, warnings);
20534
20714
  const cursor = new ValueCursor(normalizedValues);
20535
20715
  const allTables = collectIRTables(cloned, 0);
20536
20716
  const patternFilledCells = /* @__PURE__ */ new Set();
@@ -20559,7 +20739,7 @@ function fillFormFields(blocks, values, blockedLabels) {
20559
20739
  if (newText !== block.text) block.text = newText;
20560
20740
  }
20561
20741
  const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
20562
- return { blocks: cloned, filled, unmatched };
20742
+ return { blocks: cloned, filled, unmatched, ...warnings.length > 0 ? { warnings } : {} };
20563
20743
  }
20564
20744
  function collectIRTables(blocks, depth) {
20565
20745
  if (depth > 16) return [];
@@ -20593,13 +20773,31 @@ function coveredPositions(table2) {
20593
20773
  }
20594
20774
  return covered;
20595
20775
  }
20776
+ function isHeaderDataTable(table2, covered) {
20777
+ if (table2.rows < 2) return false;
20778
+ const headerRow = table2.cells[0];
20779
+ if (!headerRow?.length) return false;
20780
+ const allLabels = headerRow.every((cell2) => {
20781
+ const t = cell2?.text.trim() ?? "";
20782
+ return t.length > 0 && t.length <= 20 && isLabelCell(t);
20783
+ });
20784
+ if (!allLabels) return false;
20785
+ for (let c = 0; c < table2.cols; c++) {
20786
+ if (covered.has(`1,${c}`)) continue;
20787
+ const cell2 = table2.cells[1]?.[c];
20788
+ if (!cell2) break;
20789
+ return !isLabelCell(cell2.text);
20790
+ }
20791
+ return true;
20792
+ }
20596
20793
  function fillTable(table2, values, filled, matchedLabels, patternFilledCells, blockedLabels) {
20597
20794
  if (table2.cols < 2) return;
20598
20795
  const covered = coveredPositions(table2);
20599
- for (let r = 0; r < table2.rows; r++) {
20796
+ const skipHeaderRow = isHeaderDataTable(table2, covered);
20797
+ for (let r = skipHeaderRow ? 1 : 0; r < table2.rows; r++) {
20600
20798
  for (let c = 0; c < table2.cols; c++) {
20601
20799
  if (covered.has(`${r},${c}`)) continue;
20602
- const labelCell = table2.cells[r][c];
20800
+ const labelCell = table2.cells[r]?.[c];
20603
20801
  if (!labelCell) continue;
20604
20802
  if (!isLabelCell(labelCell.text)) continue;
20605
20803
  let vc = c + labelCell.colSpan;
@@ -20632,10 +20830,10 @@ function fillTable(table2, values, filled, matchedLabels, patternFilledCells, bl
20632
20830
  }
20633
20831
  if (table2.rows >= 2 && table2.cols >= 2) {
20634
20832
  const headerRow = table2.cells[0];
20635
- const allLabels = headerRow.every((cell2) => {
20636
- const t = cell2.text.trim();
20833
+ const allLabels = headerRow?.every((cell2) => {
20834
+ const t = cell2?.text.trim() ?? "";
20637
20835
  return t.length > 0 && t.length <= 20 && isLabelCell(t);
20638
- });
20836
+ }) ?? false;
20639
20837
  if (!allLabels) return;
20640
20838
  for (let r = 1; r < table2.rows; r++) {
20641
20839
  for (let c = 0; c < table2.cols; c++) {
@@ -20650,7 +20848,11 @@ function fillTable(table2, values, filled, matchedLabels, patternFilledCells, bl
20650
20848
  if (!values.isArray(matchKey) && matchedLabels.has(matchKey)) continue;
20651
20849
  const newValue = values.consume(matchKey);
20652
20850
  if (newValue === void 0) continue;
20653
- valueCell.text = newValue;
20851
+ if (patternFilledCells?.has(valueCell)) {
20852
+ valueCell.text = newValue + " " + valueCell.text;
20853
+ } else {
20854
+ valueCell.text = newValue;
20855
+ }
20654
20856
  matchedLabels.add(matchKey);
20655
20857
  filled.push({
20656
20858
  label: headerCell.text.trim(),
@@ -20666,20 +20868,22 @@ function fillTable(table2, values, filled, matchedLabels, patternFilledCells, bl
20666
20868
  function fillInlineFields(text, values, filled, matchedLabels, blockedLabels) {
20667
20869
  const segments = scanInlineSegments(text);
20668
20870
  if (segments.length === 0) return text;
20871
+ const matches = segments.map((seg) => matchInlineSegment(seg, values, blockedLabels));
20669
20872
  let out = "";
20670
20873
  let pos = 0;
20671
- for (const seg of segments) {
20672
- const nlabel = normalizeLabel(seg.label);
20673
- if (blockedLabels?.has(nlabel)) continue;
20674
- const matchKey = findMatchingKey(nlabel, values);
20675
- if (matchKey === void 0) continue;
20874
+ for (let i = 0; i < segments.length; i++) {
20875
+ const seg = segments[i];
20876
+ const matched = matches[i];
20877
+ if (matched === void 0) continue;
20878
+ const matchKey = matched.key;
20676
20879
  const newValue = values.consume(matchKey);
20677
20880
  if (newValue === void 0) continue;
20678
20881
  matchedLabels.add(matchKey);
20679
- filled.push({ label: seg.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20882
+ filled.push({ label: matched.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20883
+ const ve = clampSegmentEnd(text, seg, segments[i + 1], matches[i + 1]?.viaExt ?? false);
20680
20884
  out += text.slice(pos, seg.valueStart);
20681
- out += seg.valueStart === seg.valueEnd ? padInsertion(text, seg.valueStart, newValue) : newValue;
20682
- pos = seg.valueEnd;
20885
+ out += seg.valueStart === ve ? padInsertion(text, seg.valueStart, newValue) : newValue;
20886
+ pos = ve;
20683
20887
  }
20684
20888
  out += text.slice(pos);
20685
20889
  return out;
@@ -20690,7 +20894,7 @@ import JSZip6 from "jszip";
20690
20894
 
20691
20895
  // src/roundtrip/source-map.ts
20692
20896
  function escapeXmlText(text) {
20693
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
20897
+ return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
20694
20898
  }
20695
20899
  function decodeXmlEntities(text) {
20696
20900
  return text.replace(/&(lt|gt|amp|quot|apos|#x?[0-9a-fA-F]+);/g, (m, ent) => {
@@ -21270,7 +21474,7 @@ function parseCentralDirectory(buf) {
21270
21474
  }
21271
21475
  return { entries, cdOffset, cdSize, eocdOffset };
21272
21476
  }
21273
- var CRC_TABLE2 = (() => {
21477
+ var CRC_TABLE = (() => {
21274
21478
  const table2 = new Uint32Array(256);
21275
21479
  for (let n = 0; n < 256; n++) {
21276
21480
  let c = n;
@@ -21279,10 +21483,10 @@ var CRC_TABLE2 = (() => {
21279
21483
  }
21280
21484
  return table2;
21281
21485
  })();
21282
- function crc322(data) {
21486
+ function crc32(data) {
21283
21487
  let crc = 4294967295;
21284
21488
  for (let i = 0; i < data.length; i++) {
21285
- crc = CRC_TABLE2[(crc ^ data[i]) & 255] ^ crc >>> 8;
21489
+ crc = CRC_TABLE[(crc ^ data[i]) & 255] ^ crc >>> 8;
21286
21490
  }
21287
21491
  return (crc ^ 4294967295) >>> 0;
21288
21492
  }
@@ -21320,8 +21524,11 @@ function patchZipEntries(original, replacements, additions) {
21320
21524
  const header = copyBytes(original, e.localOffset, e.localOffset + headerLen);
21321
21525
  const hview = new DataView(header.buffer, header.byteOffset, header.byteLength);
21322
21526
  const method = e.method;
21527
+ if (method !== 0 && method !== 8) {
21528
+ throw new KordocError(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 ZIP \uC555\uCD95 \uBC29\uC2DD(method=${method}): ${e.name} \u2014 STORE(0)/DEFLATE(8)\uB9CC \uAD50\uCCB4 \uAC00\uB2A5`);
21529
+ }
21323
21530
  const compData = method === 0 ? newData : new Uint8Array(deflateRawSync(newData));
21324
- const crc = crc322(newData);
21531
+ const crc = crc32(newData);
21325
21532
  const flags = e.flags & ~8;
21326
21533
  hview.setUint16(6, flags, true);
21327
21534
  hview.setUint32(14, crc, true);
@@ -21339,7 +21546,7 @@ function patchZipEntries(original, replacements, additions) {
21339
21546
  const deflated = new Uint8Array(deflateRawSync(data));
21340
21547
  const method = deflated.length < data.length ? 8 : 0;
21341
21548
  const compData = method === 8 ? deflated : data;
21342
- const crc = crc322(data);
21549
+ const crc = crc32(data);
21343
21550
  const header = new Uint8Array(30 + nameBytes.length);
21344
21551
  const hv = new DataView(header.buffer);
21345
21552
  hv.setUint32(0, LOCAL_SIG, true);
@@ -21421,7 +21628,8 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
21421
21628
  if (sectionPaths.length === 0) {
21422
21629
  throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
21423
21630
  }
21424
- const normalizedValues = normalizeValues(values);
21631
+ const warnings = [];
21632
+ const normalizedValues = normalizeValues(values, warnings);
21425
21633
  const cursor = new ValueCursor(normalizedValues);
21426
21634
  const matchedLabels = /* @__PURE__ */ new Set();
21427
21635
  const filled = [];
@@ -21483,7 +21691,17 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
21483
21691
  }
21484
21692
  }
21485
21693
  for (const table2 of allTables) {
21486
- for (let rowIdx = 0; rowIdx < table2.rows.length; rowIdx++) {
21694
+ const skipHeaderRow = table2.rows.length >= 2 && (() => {
21695
+ const first = table2.rows[0];
21696
+ const allLabels = first.length > 0 && first.every((cell2) => {
21697
+ const t = cellLabelText(cell2).trim();
21698
+ return t.length > 0 && t.length <= 20 && isLabelCell(t);
21699
+ });
21700
+ if (!allLabels) return false;
21701
+ const d0 = table2.rows[1][0];
21702
+ return d0 === void 0 || !isLabelCell(cellLabelText(d0));
21703
+ })();
21704
+ for (let rowIdx = skipHeaderRow ? 1 : 0; rowIdx < table2.rows.length; rowIdx++) {
21487
21705
  const cells = table2.rows[rowIdx];
21488
21706
  for (let colIdx = 0; colIdx < cells.length - 1; colIdx++) {
21489
21707
  const labelText = cellLabelText(cells[colIdx]);
@@ -21554,9 +21772,30 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
21554
21772
  const matchKey = findMatchingKey(headerLabel, cursor);
21555
21773
  if (matchKey === void 0) continue;
21556
21774
  if (!cursor.isArray(matchKey) && matchedLabels.has(matchKey)) continue;
21775
+ const dataCell = dataCells[colIdx];
21776
+ if (patternApplied.has(dataCell)) {
21777
+ const target = dataCell.paragraphs.find((p) => p.tRanges.length > 0) ?? dataCell.paragraphs[0];
21778
+ if (!target) continue;
21779
+ const l = led(target);
21780
+ if (l.fullText !== void 0) continue;
21781
+ const newValue2 = cursor.consume(matchKey);
21782
+ if (newValue2 === void 0) continue;
21783
+ l.ranges.push({ start: 0, end: 0, replacement: newValue2 + " " });
21784
+ l.filledIdx.push(filled.length);
21785
+ l.matchKeys.push(matchKey);
21786
+ matchedLabels.add(matchKey);
21787
+ filled.push({
21788
+ label: cellLabelText(headerCells[colIdx]).trim(),
21789
+ value: newValue2,
21790
+ row: rowIdx,
21791
+ col: colIdx,
21792
+ key: matchKey
21793
+ });
21794
+ continue;
21795
+ }
21557
21796
  const newValue = cursor.consume(matchKey);
21558
21797
  if (newValue === void 0) continue;
21559
- const paras = dataCells[colIdx].paragraphs;
21798
+ const paras = dataCell.paragraphs;
21560
21799
  if (paras.length === 0) continue;
21561
21800
  const l0 = led(paras[0]);
21562
21801
  l0.fullText = newValue;
@@ -21585,20 +21824,23 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
21585
21824
  const existing = ledger.get(para2);
21586
21825
  if (existing?.fullText !== void 0) continue;
21587
21826
  const text = matchText(para2);
21588
- for (const seg of scanInlineSegments(text)) {
21589
- const nlabel = normalizeLabel(seg.label);
21590
- if (blockedLabels?.has(nlabel)) continue;
21591
- const matchKey = findMatchingKey(nlabel, cursor);
21592
- if (matchKey === void 0) continue;
21827
+ const segments = scanInlineSegments(text);
21828
+ const matches = segments.map((seg) => matchInlineSegment(seg, cursor, blockedLabels));
21829
+ for (let i = 0; i < segments.length; i++) {
21830
+ const seg = segments[i];
21831
+ const matched = matches[i];
21832
+ if (matched === void 0) continue;
21833
+ const matchKey = matched.key;
21593
21834
  const newValue = cursor.consume(matchKey);
21594
21835
  if (newValue === void 0) continue;
21595
- const replacement = seg.valueStart === seg.valueEnd ? padInsertion(text, seg.valueStart, newValue) : newValue;
21836
+ const ve = clampSegmentEnd(text, seg, segments[i + 1], matches[i + 1]?.viaExt ?? false);
21837
+ const replacement = seg.valueStart === ve ? padInsertion(text, seg.valueStart, newValue) : newValue;
21596
21838
  const l = led(para2);
21597
- l.ranges.push({ start: seg.valueStart, end: seg.valueEnd, replacement });
21839
+ l.ranges.push({ start: seg.valueStart, end: ve, replacement });
21598
21840
  matchedLabels.add(matchKey);
21599
21841
  l.filledIdx.push(filled.length);
21600
21842
  l.matchKeys.push(matchKey);
21601
- filled.push({ label: seg.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
21843
+ filled.push({ label: matched.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
21602
21844
  }
21603
21845
  }
21604
21846
  const splices = [];
@@ -21651,7 +21893,7 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
21651
21893
  }
21652
21894
  }
21653
21895
  for (const k of failedKeys) {
21654
- if (!succeededKeys.has(k)) matchedLabels.delete(k);
21896
+ if (!succeededKeys.has(k) || cursor.isArray(k)) matchedLabels.delete(k);
21655
21897
  }
21656
21898
  const cleanFilled = filled.filter((f) => f !== null);
21657
21899
  const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
@@ -21659,7 +21901,8 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
21659
21901
  return {
21660
21902
  buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
21661
21903
  filled: cleanFilled,
21662
- unmatched
21904
+ unmatched,
21905
+ ...warnings.length > 0 ? { warnings } : {}
21663
21906
  };
21664
21907
  }
21665
21908
 
@@ -21936,27 +22179,39 @@ function parseMarkdownToBlocks(md2) {
21936
22179
  if (/^<table[\s>]/i.test(line.trimStart())) {
21937
22180
  const htmlLines = [];
21938
22181
  let depth = 0;
21939
- while (i < lines.length) {
21940
- const l = lines[i];
22182
+ let closed = false;
22183
+ let j = i;
22184
+ while (j < lines.length) {
22185
+ const l = lines[j];
21941
22186
  htmlLines.push(l);
21942
22187
  depth += (l.match(/<table[\s>]/gi) ?? []).length;
21943
22188
  depth -= (l.match(/<\/table>/gi) ?? []).length;
21944
- i++;
21945
- if (depth <= 0) break;
22189
+ j++;
22190
+ if (depth <= 0) {
22191
+ closed = true;
22192
+ break;
22193
+ }
22194
+ }
22195
+ if (closed) {
22196
+ blocks.push({ type: "html_table", text: htmlLines.join("\n") });
22197
+ i = j;
22198
+ continue;
21946
22199
  }
21947
- blocks.push({ type: "html_table", text: htmlLines.join("\n") });
21948
- continue;
21949
22200
  }
21950
22201
  if (line.trimStart().startsWith("|")) {
21951
22202
  const tableRows = [];
22203
+ let sepSeen = false;
21952
22204
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
21953
22205
  const row = lines[i];
21954
- const sepCells = row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
21955
- if (sepCells.every((c) => /^\s*:?-+:?\s*$/.test(c))) {
21956
- i++;
21957
- continue;
22206
+ if (tableRows.length === 1 && !sepSeen) {
22207
+ const sepCells = row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
22208
+ if (sepCells.every((c) => /^\s*:?-+:?\s*$/.test(c))) {
22209
+ sepSeen = true;
22210
+ i++;
22211
+ continue;
22212
+ }
21958
22213
  }
21959
- const cells = row.split("|").slice(1, -1).map((c) => c.trim());
22214
+ const cells = row.split(/(?<!\\)\|/).slice(1, -1).map((c) => c.trim().replace(/\\\|/g, "|"));
21960
22215
  if (cells.length > 0) tableRows.push(cells);
21961
22216
  i++;
21962
22217
  }
@@ -22905,13 +23160,26 @@ function normalize(s) {
22905
23160
  return s.replace(/\s+/g, " ").trim();
22906
23161
  }
22907
23162
  var MAX_LEVENSHTEIN_LEN = 1e4;
23163
+ function approxDistance(a, b) {
23164
+ const bigramCounts = (s) => {
23165
+ const m = /* @__PURE__ */ new Map();
23166
+ for (let i = 0; i < s.length - 1; i++) {
23167
+ const g = s.slice(i, i + 2);
23168
+ m.set(g, (m.get(g) ?? 0) + 1);
23169
+ }
23170
+ return m;
23171
+ };
23172
+ const ca = bigramCounts(a);
23173
+ const cb = bigramCounts(b);
23174
+ let inter = 0;
23175
+ for (const [g, n] of ca) inter += Math.min(n, cb.get(g) ?? 0);
23176
+ const total = Math.max(a.length - 1, 0) + Math.max(b.length - 1, 0);
23177
+ const dice = total > 0 ? 2 * inter / total : 1;
23178
+ return Math.round(Math.max(a.length, b.length) * (1 - dice));
23179
+ }
22908
23180
  function levenshtein(a, b) {
22909
23181
  if (a.length + b.length > MAX_LEVENSHTEIN_LEN) {
22910
- const sampleLen = Math.min(500, a.length, b.length);
22911
- let diffs = 0;
22912
- for (let i = 0; i < sampleLen; i++) if (a[i] !== b[i]) diffs++;
22913
- const sampleRate = sampleLen > 0 ? diffs / sampleLen : 1;
22914
- return Math.abs(a.length - b.length) + Math.round(Math.min(a.length, b.length) * sampleRate);
23182
+ return approxDistance(a, b);
22915
23183
  }
22916
23184
  if (a.length > b.length) [a, b] = [b, a];
22917
23185
  const m = a.length;
@@ -23063,9 +23331,16 @@ function bestSimInRange(arr, from, to, target) {
23063
23331
  return best;
23064
23332
  }
23065
23333
  function escapeGfm(text) {
23066
- return text.replace(/([~*])/g, "\\$1");
23334
+ const NUL = String.fromCharCode(0);
23335
+ const spans = [];
23336
+ const masked = text.replace(/!\[[^\]]*\]\([^)\n]*\)|\]\((?:https?:|mailto:|tel:|#)[^)\n]*\)/gi, (m) => {
23337
+ spans.push(m);
23338
+ return NUL + (spans.length - 1) + NUL;
23339
+ });
23340
+ const escaped = masked.replace(/([~*_`])/g, "\\$1");
23341
+ return escaped.replace(new RegExp(NUL + "(\\d+)" + NUL, "g"), (_, n) => spans[Number(n)]);
23067
23342
  }
23068
- var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
23343
+ var HWP_SHAPE_ALT_TEXT_RE = /^(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?$/gm;
23069
23344
  function sanitizeText(text) {
23070
23345
  let result = mapPuaText(text).replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
23071
23346
  if (result.length <= 30 && result.includes(" ")) {
@@ -23081,7 +23356,7 @@ function normForMatch(text) {
23081
23356
  return sanitizeText(text).replace(/\s+/g, " ").trim();
23082
23357
  }
23083
23358
  function unescapeGfm(text) {
23084
- return text.replace(/\\([~*])/g, "$1");
23359
+ return text.replace(/\\([~*_`])/g, "$1");
23085
23360
  }
23086
23361
  function summarize(text) {
23087
23362
  const t = text.replace(/\s+/g, " ").trim();
@@ -23148,7 +23423,7 @@ function parseGfmTable(lines) {
23148
23423
  return rows;
23149
23424
  }
23150
23425
  function unescapeGfmCell(text) {
23151
- return text.replace(/<br\s*\/?>/gi, "\n").replace(/\\\|/g, "|").replace(/\\([~*])/g, "$1");
23426
+ return text.replace(/<br\s*\/?>/gi, "\n").replace(/\\\|/g, "|").replace(/\\([~*_`])/g, "$1");
23152
23427
  }
23153
23428
  function replicateCellInnerHtml(cell2) {
23154
23429
  if (cell2.blocks?.length) {
@@ -23236,10 +23511,10 @@ function parseHtmlTable(raw) {
23236
23511
  }
23237
23512
  } else {
23238
23513
  if (!isClose) {
23239
- const cs = parseInt(attrs.match(/colspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
23240
- const rs = parseInt(attrs.match(/rowspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
23514
+ const cs = parseInt(attrs.match(/colspan\s*=\s*["']?(\d+)/i)?.[1] || "1", 10);
23515
+ const rs = parseInt(attrs.match(/rowspan\s*=\s*["']?(\d+)/i)?.[1] || "1", 10);
23241
23516
  cellStart = m.index + m[0].length;
23242
- cellInfo = { colSpan: isNaN(cs) ? 1 : cs, rowSpan: isNaN(rs) ? 1 : rs };
23517
+ cellInfo = { colSpan: clampSpan(isNaN(cs) ? 1 : cs, MAX_COLS), rowSpan: clampSpan(isNaN(rs) ? 1 : rs, MAX_ROWS) };
23243
23518
  } else if (cellStart >= 0 && cellInfo && currentRow) {
23244
23519
  currentRow.push({ inner: raw.slice(cellStart, m.index), colSpan: cellInfo.colSpan, rowSpan: cellInfo.rowSpan });
23245
23520
  cellStart = -1;
@@ -23883,8 +24158,8 @@ function layoutHtmlRows(rows) {
23883
24158
  let c = 0;
23884
24159
  for (const cell2 of rows[r].cells) {
23885
24160
  while (occupied.has(`${r},${c}`)) c++;
23886
- const colSpan = Math.max(1, cell2.colSpan);
23887
- const rowSpan = Math.max(1, cell2.rowSpan);
24161
+ const colSpan = clampSpan(cell2.colSpan, MAX_COLS);
24162
+ const rowSpan = clampSpan(cell2.rowSpan, MAX_ROWS);
23888
24163
  placed.push({ r, c, colSpan, rowSpan, inner: cell2.inner, isHeader: rows[r].tag === "th" });
23889
24164
  for (let dr = 0; dr < rowSpan; dr++) {
23890
24165
  for (let dc = 0; dc < colSpan; dc++) occupied.add(`${r + dr},${c + dc}`);
@@ -25043,12 +25318,20 @@ function diffBlocks(blocksA, blocksB) {
25043
25318
  function alignBlocks(a, b) {
25044
25319
  const m = a.length, n = b.length;
25045
25320
  if (m * n > 1e7) return fallbackAlign(a, b);
25321
+ const lenOf = (blk) => {
25322
+ const t = blk.text !== void 0 ? blk.text : blk.type === "table" && blk.table ? blk.table.cells.flat().map((c) => c?.text ?? "").join(" ") : "";
25323
+ return t.replace(/\s+/g, " ").trim().length;
25324
+ };
25325
+ const aLen = a.map(lenOf);
25326
+ const bLen = b.map(lenOf);
25046
25327
  const simCache = /* @__PURE__ */ new Map();
25047
25328
  const getSim = (i2, j2) => {
25048
25329
  const key = `${i2},${j2}`;
25049
25330
  let v = simCache.get(key);
25050
25331
  if (v === void 0) {
25051
- v = blockSimilarity(a[i2], b[j2]);
25332
+ const mx = Math.max(aLen[i2], bLen[j2]);
25333
+ const cut = a[i2].type === "table" || b[j2].type === "table" ? 6 / 7 : 1 - SIMILARITY_THRESHOLD;
25334
+ v = mx > 0 && (mx - Math.min(aLen[i2], bLen[j2])) / mx > cut ? 0 : blockSimilarity(a[i2], b[j2]);
25052
25335
  simCache.set(key, v);
25053
25336
  }
25054
25337
  return v;
@@ -25109,8 +25392,8 @@ function blockSimilarity(a, b) {
25109
25392
  }
25110
25393
  function tableSimilarity(a, b) {
25111
25394
  const dimSim = 1 - Math.abs(a.rows * a.cols - b.rows * b.cols) / Math.max(a.rows * a.cols, b.rows * b.cols, 1);
25112
- const textsA = a.cells.flat().map((c) => c.text).join(" ");
25113
- const textsB = b.cells.flat().map((c) => c.text).join(" ");
25395
+ const textsA = a.cells.flat().map((c) => c?.text ?? "").join(" ");
25396
+ const textsB = b.cells.flat().map((c) => c?.text ?? "").join(" ");
25114
25397
  const contentSim = normalizedSimilarity(textsA, textsB);
25115
25398
  return dimSim * 0.3 + contentSim * 0.7;
25116
25399
  }
@@ -25121,8 +25404,8 @@ function diffTableCells(a, b) {
25121
25404
  for (let r = 0; r < maxRows; r++) {
25122
25405
  const row = [];
25123
25406
  for (let c = 0; c < maxCols; c++) {
25124
- const cellA = r < a.rows && c < a.cols ? a.cells[r][c].text : void 0;
25125
- const cellB = r < b.rows && c < b.cols ? b.cells[r][c].text : void 0;
25407
+ const cellA = r < a.rows && c < a.cols ? a.cells[r]?.[c]?.text : void 0;
25408
+ const cellB = r < b.rows && c < b.cols ? b.cells[r]?.[c]?.text : void 0;
25126
25409
  let type;
25127
25410
  if (cellA === void 0) type = "added";
25128
25411
  else if (cellB === void 0) type = "removed";
@@ -26881,16 +27164,16 @@ function buildOrigUnits(blocks) {
26881
27164
  for (let i = 0; i < blocks.length; i++) {
26882
27165
  const block = blocks[i];
26883
27166
  let consume = 1;
26884
- let chunk2;
27167
+ let chunk;
26885
27168
  if (block.type === "paragraph" && block.text && /^\[별표\s*\d+/.test(sanitizeText(block.text))) {
26886
27169
  const next = blocks[i + 1];
26887
27170
  if (next?.type === "paragraph" && next.text && /관련\)?$/.test(next.text)) consume = 2;
26888
- chunk2 = blocksToMarkdown(blocks.slice(i, i + consume));
27171
+ chunk = blocksToMarkdown(blocks.slice(i, i + consume));
26889
27172
  } else {
26890
- chunk2 = blocksToMarkdown([block]);
27173
+ chunk = blocksToMarkdown([block]);
26891
27174
  }
26892
- if (chunk2) {
26893
- const subUnits = splitMarkdownUnits(chunk2);
27175
+ if (chunk) {
27176
+ const subUnits = splitMarkdownUnits(chunk);
26894
27177
  const isFragment = consume === 2 || (block.type === "paragraph" || block.type === "heading") && subUnits.length > 1;
26895
27178
  for (let s = 0; s < subUnits.length; s++) {
26896
27179
  const u = { ...subUnits[s], blockIdx: i, fragment: isFragment || void 0 };
@@ -27157,6 +27440,9 @@ var Surgeon = class {
27157
27440
  miniFatSectors = [];
27158
27441
  dirSectors = [];
27159
27442
  entries = [];
27443
+ /** replace()가 FREESECT로 해제한 섹터 — finish()에서 재할당 안 된 것만 0으로 지움 (데이터 잔존 방지) */
27444
+ freedSectors = [];
27445
+ freedMiniSectors = [];
27160
27446
  constructor(file) {
27161
27447
  if (file.length < SECTOR || file.readUInt32LE(0) !== 3759263696) {
27162
27448
  throw new OleSurgeonError("OLE \uC2DC\uADF8\uB2C8\uCC98\uAC00 \uC544\uB2D9\uB2C8\uB2E4");
@@ -27280,6 +27566,8 @@ var Surgeon = class {
27280
27566
  if (this.fat[i] !== FREESECT) continue;
27281
27567
  if (SECTOR + (i + 1) * SECTOR > this.buf.length) continue;
27282
27568
  this.fat[i] = ENDOFCHAIN;
27569
+ const off = this.sectorOffset(i);
27570
+ this.buf.fill(0, off, off + SECTOR);
27283
27571
  out.push(i);
27284
27572
  }
27285
27573
  while (out.length < n) {
@@ -27379,9 +27667,15 @@ var Surgeon = class {
27379
27667
  const entry = this.findEntry(path);
27380
27668
  if (entry.size > 0 && entry.start !== ENDOFCHAIN) {
27381
27669
  if (entry.size < MINI_CUTOFF) {
27382
- for (const s of this.miniChain(entry.start)) this.miniFat[s] = FREESECT;
27670
+ for (const s of this.miniChain(entry.start)) {
27671
+ this.miniFat[s] = FREESECT;
27672
+ this.freedMiniSectors.push(s);
27673
+ }
27383
27674
  } else {
27384
- for (const s of this.chain(entry.start)) this.fat[s] = FREESECT;
27675
+ for (const s of this.chain(entry.start)) {
27676
+ this.fat[s] = FREESECT;
27677
+ this.freedSectors.push(s);
27678
+ }
27385
27679
  }
27386
27680
  }
27387
27681
  if (newData.length < MINI_CUTOFF) {
@@ -27410,9 +27704,27 @@ var Surgeon = class {
27410
27704
  this.writeDirEntry(entry);
27411
27705
  }
27412
27706
  finish() {
27707
+ this.wipeFreedSectors();
27413
27708
  this.flushFat();
27414
27709
  return this.buf;
27415
27710
  }
27711
+ /** 해제 후 재할당되지 않고 남은 FREESECT 섹터의 바이트를 0으로 채움 (데이터 remanence 제거) */
27712
+ wipeFreedSectors() {
27713
+ for (const s of this.freedSectors) {
27714
+ if (this.fat[s] !== FREESECT) continue;
27715
+ const off = this.sectorOffset(s);
27716
+ this.buf.fill(0, off, off + SECTOR);
27717
+ }
27718
+ if (this.freedMiniSectors.length > 0) {
27719
+ const root = this.rootEntry();
27720
+ const rootChain = root.start === ENDOFCHAIN || root.size === 0 ? [] : this.chain(root.start);
27721
+ for (const s of this.freedMiniSectors) {
27722
+ if (this.miniFat[s] !== FREESECT) continue;
27723
+ const off = this.miniOffset(s, rootChain);
27724
+ this.buf.fill(0, off, off + MINI_SECTOR);
27725
+ }
27726
+ }
27727
+ }
27416
27728
  };
27417
27729
 
27418
27730
  // src/roundtrip/hwp5-patch.ts
@@ -28297,6 +28609,216 @@ async function validateHwpx(buffer) {
28297
28609
  return { ok: issues.length === 0, issues, entryCount: names.length };
28298
28610
  }
28299
28611
 
28612
+ // src/redact.ts
28613
+ var DEFAULT_REDACT_RULES = [
28614
+ "rrn",
28615
+ "phone",
28616
+ "email",
28617
+ "card",
28618
+ "account"
28619
+ ];
28620
+ var RULE_PRIORITY = [
28621
+ "rrn",
28622
+ "email",
28623
+ "card",
28624
+ "phone",
28625
+ "driver",
28626
+ "account",
28627
+ "passport"
28628
+ ];
28629
+ function luhnValid(digits) {
28630
+ let sum = 0;
28631
+ for (let i = 0; i < digits.length; i++) {
28632
+ let d = digits.charCodeAt(digits.length - 1 - i) - 48;
28633
+ if (i % 2 === 1) {
28634
+ d *= 2;
28635
+ if (d > 9) d -= 9;
28636
+ }
28637
+ sum += d;
28638
+ }
28639
+ return sum % 10 === 0;
28640
+ }
28641
+ function birthdateValid(front6) {
28642
+ const mm = Number(front6.slice(2, 4));
28643
+ const dd = Number(front6.slice(4, 6));
28644
+ return mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31;
28645
+ }
28646
+ var RULES2 = {
28647
+ // 주민/외국인등록번호 — 뒷자리 첫 숫자 1-8 + 생년월일 유효성으로 오탐 축소.
28648
+ // 유니코드 대시 변형(‐ ‑ – —)은 rrn만 허용. 앞 6자리 유지, 뒤 7자리 전부 마스크.
28649
+ rrn: {
28650
+ pattern: /(?<!\d)(\d{6})([-‐‑–—])([1-8]\d{6})(?!\d)/g,
28651
+ validate: (m) => birthdateValid(m[1]),
28652
+ mask: (m, mc) => m[1] + m[2] + mc.repeat(7)
28653
+ },
28654
+ // 이메일 — 로컬파트 첫 글자만 남기고 마스크, 도메인 유지
28655
+ email: {
28656
+ pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
28657
+ mask: (m, mc) => {
28658
+ const at = m[0].indexOf("@");
28659
+ return m[0][0] + mc.repeat(at - 1) + m[0].slice(at);
28660
+ }
28661
+ },
28662
+ // 카드번호 — 구분자 필수(무구분 16자리는 오탐 높아 제외), 동일 구분자 강제(\2),
28663
+ // Luhn 체크. 가운데 8자리 마스크.
28664
+ card: {
28665
+ pattern: /(?<!\d)(\d{4})([- ])(\d{4})\2(\d{4})\2(\d{4})(?!\d)/g,
28666
+ validate: (m) => luhnValid(m[1] + m[3] + m[4] + m[5]),
28667
+ mask: (m, mc) => m[1] + m[2] + mc.repeat(4) + m[2] + mc.repeat(4) + m[2] + m[5]
28668
+ },
28669
+ // 전화번호 — 휴대폰(01[016789])·서울(02)·지역(0[3-6]\d)·인터넷(070)은 구분자
28670
+ // -·.·공백 또는 무구분(동일 구분자 강제), 대표번호(15xx/16xx/18xx)는 구분자 필수.
28671
+ // 가운데 자리만 마스크 (대표번호는 뒤 4자리). 선행 [\d-] 금지 — 계좌 부분매치 방지.
28672
+ phone: {
28673
+ pattern: /(?<![\d-])(?:(01[016789]|070|02|0[3-6]\d)([-. ]?)(\d{3,4})\2(\d{4})|(1[568]\d{2})([-. ])(\d{4}))(?!\d)/g,
28674
+ mask: (m, mc) => m[1] !== void 0 ? m[1] + m[2] + mc.repeat(m[3].length) + m[2] + m[4] : m[5] + m[6] + mc.repeat(4)
28675
+ },
28676
+ // 운전면허 (기본 OFF) — 신형 12자리만 (지역명 2글자 선행 구버전은 스킵). 뒷 8자리 마스크.
28677
+ driver: {
28678
+ pattern: /(?<![\d-])(\d{2})-(\d{2})-\d{6}-\d{2}(?!-?\d)/g,
28679
+ mask: (m, mc) => m[1] + "-" + m[2] + "-" + mc.repeat(6) + "-" + mc.repeat(2)
28680
+ },
28681
+ // 계좌번호 — 3~4그룹 + 총 자릿수 10~16. rrn·card·phone과 겹치면 그쪽 우선.
28682
+ // 마지막 그룹 빼고 전부 마스크. 사업자등록번호(3-2-5, 10자리)도 걸린다 — 계약상 의도
28683
+ // (테스트로 명시). 날짜(2026-07-16)는 8자리라 총자릿수 검증에서 탈락.
28684
+ account: {
28685
+ pattern: /(?<!\d)(?<!\d-)\d{2,6}(?:-\d{2,6}){1,2}-\d{2,8}(?!-?\d)/g,
28686
+ validate: (m) => {
28687
+ const digits = m[0].replace(/-/g, "").length;
28688
+ return digits >= 10 && digits <= 16;
28689
+ },
28690
+ mask: (m, mc) => {
28691
+ const parts = m[0].split("-");
28692
+ return parts.map((p, i) => i === parts.length - 1 ? p : mc.repeat(p.length)).join("-");
28693
+ }
28694
+ },
28695
+ // 여권번호 (기본 OFF) — 단어 경계, 첫 글자만 남기고 전부 마스크
28696
+ passport: {
28697
+ pattern: /\b([MSRODG])\d{8}(?![0-9A-Za-z])/g,
28698
+ mask: (m, mc) => m[1] + mc.repeat(8)
28699
+ }
28700
+ };
28701
+ function redactText(text, options) {
28702
+ const maskChar = options?.maskChar ?? "\u25CF";
28703
+ if (maskChar.length !== 1 || /[0-9A-Za-z]/.test(maskChar)) {
28704
+ throw new Error(`maskChar\uB294 \uC601\uC22B\uC790\uAC00 \uC544\uB2CC 1\uAE00\uC790\uC5EC\uC57C \uD568: ${JSON.stringify(maskChar)}`);
28705
+ }
28706
+ const enabled = options?.rules ?? DEFAULT_REDACT_RULES;
28707
+ const hits = [];
28708
+ if (text === "" || enabled.length === 0) return { text, hits };
28709
+ const occupied = [];
28710
+ for (const rule of RULE_PRIORITY) {
28711
+ if (!enabled.includes(rule)) continue;
28712
+ const def = RULES2[rule];
28713
+ for (const m of text.matchAll(def.pattern)) {
28714
+ if (def.validate && !def.validate(m)) continue;
28715
+ const start = m.index;
28716
+ const end = start + m[0].length;
28717
+ if (occupied.some((o) => start < o.end && end > o.start)) continue;
28718
+ occupied.push({ start, end });
28719
+ hits.push({ rule, masked: def.mask(m, maskChar), index: start, length: m[0].length });
28720
+ }
28721
+ }
28722
+ hits.sort((a, b) => a.index - b.index);
28723
+ let out = "";
28724
+ let cursor = 0;
28725
+ for (const h of hits) {
28726
+ out += text.slice(cursor, h.index) + h.masked;
28727
+ cursor = h.index + h.length;
28728
+ }
28729
+ out += text.slice(cursor);
28730
+ return { text: out, hits };
28731
+ }
28732
+ function redactMarkdown(markdown, options) {
28733
+ const lines = markdown.split("\n");
28734
+ const hits = [];
28735
+ let offset = 0;
28736
+ const outLines = lines.map((line) => {
28737
+ if (line.includes("data:image/")) {
28738
+ offset += line.length + 1;
28739
+ return line;
28740
+ }
28741
+ const r = redactText(line, options);
28742
+ for (const h of r.hits) hits.push({ ...h, index: h.index + offset });
28743
+ offset += line.length + 1;
28744
+ return r.text;
28745
+ });
28746
+ return { text: outLines.join("\n"), hits };
28747
+ }
28748
+
28749
+ // src/chunks.ts
28750
+ var LIST_MARKER_RE = /^(?:[□■◇◆○◎●◦ㅇ•▪▸▶※-]|\d{1,3}[.)]|[가나다라마바사아자차카타파하][.)]|\([가나다라마바사아자차카타파하0-9]{1,3}\)|[①-⑳㉮-㉻㈎-㈛])\s/;
28751
+ function listDepthOf(block) {
28752
+ if (block.listDepth !== void 0) return block.listDepth;
28753
+ if (block.type === "list") return 0;
28754
+ if (block.type === "paragraph" && block.text && LIST_MARKER_RE.test(block.text.trim())) return 0;
28755
+ return void 0;
28756
+ }
28757
+ function sameBreadcrumb(a, b) {
28758
+ return a.length === b.length && a.every((v, i) => v === b[i]);
28759
+ }
28760
+ function blocksToChunks(blocks, options) {
28761
+ const granularity = options?.granularity ?? "section";
28762
+ const includeCells = options?.includeTableCells ?? false;
28763
+ const chunks = [];
28764
+ const headingStack = [];
28765
+ const listStack = [];
28766
+ const crumb = () => [...headingStack.map((h) => h.text), ...listStack.map((l) => l.text)];
28767
+ const push = (type, breadcrumb, text, blockRange, page, table2) => {
28768
+ const chunk = { id: "c" + String(chunks.length + 1).padStart(4, "0"), type, breadcrumb, text, blockRange };
28769
+ if (page !== void 0) chunk.page = page;
28770
+ if (table2) chunk.table = table2;
28771
+ chunks.push(chunk);
28772
+ };
28773
+ let run = null;
28774
+ const flushRun = () => {
28775
+ if (!run) return;
28776
+ push("text", run.breadcrumb, blocksToMarkdown(run.blocks), [run.start, run.end], run.page);
28777
+ run = null;
28778
+ };
28779
+ for (let i = 0; i < blocks.length; i++) {
28780
+ const block = blocks[i];
28781
+ const md2 = blocksToMarkdown([block]);
28782
+ if (!md2) continue;
28783
+ if (block.type === "heading") {
28784
+ flushRun();
28785
+ const level = Math.min(block.level || 2, 6);
28786
+ while (headingStack.length && headingStack[headingStack.length - 1].level >= level) headingStack.pop();
28787
+ listStack.length = 0;
28788
+ push("heading", crumb(), md2, [i, i], block.pageNumber);
28789
+ headingStack.push({ level, text: (block.text ?? "").trim() });
28790
+ continue;
28791
+ }
28792
+ if (block.type === "table" && block.table) {
28793
+ flushRun();
28794
+ const summary = { rows: block.table.rows, cols: block.table.cols };
28795
+ if (includeCells) summary.cells = block.table.cells.map((row) => row.map((c) => c.text));
28796
+ push("table", crumb(), md2, [i, i], block.pageNumber, summary);
28797
+ continue;
28798
+ }
28799
+ const depth = listDepthOf(block);
28800
+ if (depth !== void 0) {
28801
+ while (listStack.length && listStack[listStack.length - 1].depth >= depth) listStack.pop();
28802
+ }
28803
+ const breadcrumb = crumb();
28804
+ if (depth !== void 0) listStack.push({ depth, text: (block.text ?? "").trim() });
28805
+ if (granularity === "block") {
28806
+ push("text", breadcrumb, md2, [i, i], block.pageNumber);
28807
+ continue;
28808
+ }
28809
+ if (run && sameBreadcrumb(run.breadcrumb, breadcrumb)) {
28810
+ run.blocks.push(block);
28811
+ run.end = i;
28812
+ if (run.page === void 0) run.page = block.pageNumber;
28813
+ } else {
28814
+ flushRun();
28815
+ run = { breadcrumb, blocks: [block], start: i, end: i, page: block.pageNumber };
28816
+ }
28817
+ }
28818
+ flushRun();
28819
+ return chunks;
28820
+ }
28821
+
28300
28822
  // src/roundtrip/session.ts
28301
28823
  import JSZip12 from "jszip";
28302
28824
  async function buildState(bytes) {
@@ -29272,7 +29794,7 @@ function drawPara(p, ox, oy, areaW, ctx, depth, segPages) {
29272
29794
  }
29273
29795
  if (text.trim().length > 0) {
29274
29796
  const attrs = [`x="${pt(cx)}"`, `y="${pt(y)}"`, `font-size="${pt(st.height)}"`];
29275
- if (st.fontFamily) attrs.push(`font-family="${st.fontFamily}"`);
29797
+ if (st.fontFamily) attrs.push(`font-family="${escapeXml3(st.fontFamily)}"`);
29276
29798
  if ([...text].length > 1 && sw > 50) {
29277
29799
  attrs.push(`textLength="${pt(sw)}"`, `lengthAdjust="${plan.scale < 1 ? "spacingAndGlyphs" : "spacing"}"`);
29278
29800
  }
@@ -29439,8 +29961,10 @@ function drawShape(o, x, y, ctx, depth) {
29439
29961
  for (const p of elements2(sub)) if (ln2(p) === "p") drawPara(p, x, y, w, ctx, depth + 1);
29440
29962
  }
29441
29963
  }
29442
- function cellContentExtent(cell2) {
29964
+ function cellContentExtent(cell2, memo) {
29443
29965
  if (!cell2.sub) return 0;
29966
+ const hit = memo?.cell.get(cell2.el);
29967
+ if (hit !== void 0) return hit;
29444
29968
  let ext = 0;
29445
29969
  for (const p of elements2(cell2.sub)) {
29446
29970
  if (ln2(p) !== "p") continue;
@@ -29449,7 +29973,7 @@ function cellContentExtent(cell2) {
29449
29973
  const baseV = m.segs[0]?.vertpos ?? 0;
29450
29974
  for (const o of m.objs) {
29451
29975
  if (o.inline) {
29452
- const h = o.tag === "tbl" ? Math.max(o.height, measureTableHeight(o.el)) : o.height;
29976
+ const h = o.tag === "tbl" ? Math.max(o.height, measureTableHeight(o.el, memo)) : o.height;
29453
29977
  ext = Math.max(ext, baseV + h);
29454
29978
  continue;
29455
29979
  }
@@ -29461,6 +29985,7 @@ function cellContentExtent(cell2) {
29461
29985
  ext = Math.max(ext, anchor + num3(om, "top") + num3(pos, "vertOffset") + o.height);
29462
29986
  }
29463
29987
  }
29988
+ memo?.cell.set(cell2.el, ext);
29464
29989
  return ext;
29465
29990
  }
29466
29991
  function edgeLine(x1, y1, x2, y2, e) {
@@ -29483,8 +30008,9 @@ function collectCells(tbl2) {
29483
30008
  if (!addr || !csz) continue;
29484
30009
  cells.push({
29485
30010
  el: tc2,
29486
- ca: num3(addr, "colAddr"),
29487
- ra: num3(addr, "rowAddr"),
30011
+ // 음수 주소(uint32 역변환·손상 입력) 방어 — colX/rowY 인덱스 이탈로 NaN 좌표 방지
30012
+ ca: Math.max(0, num3(addr, "colAddr")),
30013
+ ra: Math.max(0, num3(addr, "rowAddr")),
29488
30014
  cs: Math.max(1, num3(span, "colSpan", 1)),
29489
30015
  rs: Math.max(1, num3(span, "rowSpan", 1)),
29490
30016
  w: num3(csz, "width"),
@@ -29500,16 +30026,19 @@ function collectCells(tbl2) {
29500
30026
  }
29501
30027
  return cells;
29502
30028
  }
29503
- function measureTableHeight(tbl2) {
30029
+ function measureTableHeight(tbl2, memo) {
30030
+ const hit = memo?.table.get(tbl2);
30031
+ if (hit !== void 0) return hit;
29504
30032
  const cells = collectCells(tbl2);
29505
30033
  if (cells.length === 0 || cells.length > 4096) return 0;
29506
30034
  const nRows = Math.max(...cells.map((c) => c.ra + c.rs));
29507
30035
  const rowH = solveRowHeights(
29508
- cells.map((c) => ({ rowAddr: c.ra, rowSpan: c.rs, height: c.h, contentH: c.rs === 1 ? cellContentExtent(c) : void 0 })),
30036
+ cells.map((c) => ({ rowAddr: c.ra, rowSpan: c.rs, height: c.h, contentH: c.rs === 1 ? cellContentExtent(c, memo) : void 0 })),
29509
30037
  nRows
29510
30038
  );
29511
30039
  let sum = 0;
29512
30040
  for (const h of rowH) sum += h;
30041
+ memo?.table.set(tbl2, sum);
29513
30042
  return sum;
29514
30043
  }
29515
30044
  function drawTable(tbl2, tx, ty, ctx, depth) {
@@ -29526,7 +30055,7 @@ function drawTable(tbl2, tx, ty, ctx, depth) {
29526
30055
  const colCons = cells.map((c) => ({ a: c.ca, b: c.ca + c.cs, size: c.w }));
29527
30056
  const colX = solveBoundaries(colCons, nCols, num3(tblSz, "width") || void 0);
29528
30057
  const rowH = solveRowHeights(
29529
- cells.map((c) => ({ rowAddr: c.ra, rowSpan: c.rs, height: c.h, contentH: c.rs === 1 ? cellContentExtent(c) : void 0 })),
30058
+ cells.map((c) => ({ rowAddr: c.ra, rowSpan: c.rs, height: c.h, contentH: c.rs === 1 ? cellContentExtent(c, ctx.extentMemo) : void 0 })),
29530
30059
  nRows
29531
30060
  );
29532
30061
  const rowY = [0];
@@ -29546,7 +30075,7 @@ function drawTable(tbl2, tx, ty, ctx, depth) {
29546
30075
  const { c } = g;
29547
30076
  if (!c.sub) continue;
29548
30077
  const innerH = g.h - c.marginT - c.marginB;
29549
- const extent = cellContentExtent(c);
30078
+ const extent = cellContentExtent(c, ctx.extentMemo);
29550
30079
  const va = c.sub.getAttribute("vertAlign") ?? "TOP";
29551
30080
  let yoff = 0;
29552
30081
  if (va === "CENTER") yoff = Math.max(0, (innerH - extent) / 2);
@@ -29609,8 +30138,9 @@ function sniffMime(name, bytes) {
29609
30138
  const lower = name.toLowerCase();
29610
30139
  if (lower.endsWith(".png") || bytes.length > 4 && bytes[0] === 137 && bytes[1] === 80) return "image/png";
29611
30140
  if (lower.endsWith(".bmp") || bytes.length > 2 && bytes[0] === 66 && bytes[1] === 77) return "image/bmp";
29612
- if (lower.endsWith(".gif")) return "image/gif";
30141
+ if (lower.endsWith(".gif") || bytes.length > 3 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return "image/gif";
29613
30142
  if (lower.endsWith(".svg")) return "image/svg+xml";
30143
+ if (bytes.length > 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
29614
30144
  return "image/jpeg";
29615
30145
  }
29616
30146
  function readSectionGeom(root) {
@@ -29741,7 +30271,8 @@ async function renderHwpxToSvg(input, options) {
29741
30271
  highlights: (options?.highlights ?? []).map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0),
29742
30272
  warnings,
29743
30273
  warned: /* @__PURE__ */ new Set(),
29744
- stats: { texts: 0, images: 0, tables: 0 }
30274
+ stats: { texts: 0, images: 0, tables: 0 },
30275
+ extentMemo: { cell: /* @__PURE__ */ new WeakMap(), table: /* @__PURE__ */ new WeakMap() }
29745
30276
  };
29746
30277
  const rendered = [];
29747
30278
  let noCacheSkipped = false;
@@ -29844,7 +30375,7 @@ async function parseHwp3(buffer, options) {
29844
30375
  const { markdown, blocks, metadata, outline, warnings } = parseHwp3Document(buffer, options);
29845
30376
  return { success: true, fileType: "hwp3", markdown, blocks, metadata, outline, warnings };
29846
30377
  } catch (err) {
29847
- return { success: false, fileType: "hwp3", error: err instanceof Error ? err.message : "HWP3 \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
30378
+ return { success: false, fileType: "hwp3", error: sanitizeError(err), code: classifyError(err) };
29848
30379
  }
29849
30380
  }
29850
30381
  async function parseHwpx(buffer, options) {
@@ -29852,7 +30383,7 @@ async function parseHwpx(buffer, options) {
29852
30383
  const { markdown, blocks, metadata, outline, warnings, images } = await parseHwpxDocument(buffer, options);
29853
30384
  return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
29854
30385
  } catch (err) {
29855
- return { success: false, fileType: "hwpx", error: err instanceof Error ? err.message : "HWPX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
30386
+ return { success: false, fileType: "hwpx", error: sanitizeError(err), code: classifyError(err) };
29856
30387
  }
29857
30388
  }
29858
30389
  async function parseHwp(buffer, options) {
@@ -29877,13 +30408,13 @@ async function parseHwp(buffer, options) {
29877
30408
  }
29878
30409
  return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
29879
30410
  } catch (err) {
29880
- return { success: false, fileType: "hwp", error: err instanceof Error ? err.message : "HWP \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
30411
+ return { success: false, fileType: "hwp", error: sanitizeError(err), code: classifyError(err) };
29881
30412
  }
29882
30413
  }
29883
30414
  async function parsePdf(buffer, options) {
29884
30415
  let parsePdfDocument;
29885
30416
  try {
29886
- const mod = await import("./parser-UGB3NIVH.js");
30417
+ const mod = await import("./parser-XX4QTDFQ.js");
29887
30418
  parsePdfDocument = mod.parsePdfDocument;
29888
30419
  } catch {
29889
30420
  return {
@@ -29894,11 +30425,11 @@ async function parsePdf(buffer, options) {
29894
30425
  };
29895
30426
  }
29896
30427
  try {
29897
- const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary } = await parsePdfDocument(buffer, options);
29898
- return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary };
30428
+ const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images } = await parsePdfDocument(buffer, options);
30429
+ return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images };
29899
30430
  } catch (err) {
29900
30431
  const isImageBased = err instanceof Error && "isImageBased" in err ? true : void 0;
29901
- return { success: false, fileType: "pdf", error: err instanceof Error ? err.message : "PDF \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err), isImageBased };
30432
+ return { success: false, fileType: "pdf", error: sanitizeError(err), code: classifyError(err), isImageBased };
29902
30433
  }
29903
30434
  }
29904
30435
  async function parseXlsx(buffer, options) {
@@ -29906,7 +30437,7 @@ async function parseXlsx(buffer, options) {
29906
30437
  const { markdown, blocks, metadata, warnings } = await parseXlsxDocument(buffer, options);
29907
30438
  return { success: true, fileType: "xlsx", markdown, blocks, metadata, warnings };
29908
30439
  } catch (err) {
29909
- return { success: false, fileType: "xlsx", error: err instanceof Error ? err.message : "XLSX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
30440
+ return { success: false, fileType: "xlsx", error: sanitizeError(err), code: classifyError(err) };
29910
30441
  }
29911
30442
  }
29912
30443
  async function parseXls(buffer, options) {
@@ -29914,7 +30445,7 @@ async function parseXls(buffer, options) {
29914
30445
  const { markdown, blocks, metadata, warnings } = await parseXlsDocument(buffer, options);
29915
30446
  return { success: true, fileType: "xls", markdown, blocks, metadata, warnings };
29916
30447
  } catch (err) {
29917
- return { success: false, fileType: "xls", error: err instanceof Error ? err.message : "XLS \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
30448
+ return { success: false, fileType: "xls", error: sanitizeError(err), code: classifyError(err) };
29918
30449
  }
29919
30450
  }
29920
30451
  async function parseDocx(buffer, options) {
@@ -29922,7 +30453,7 @@ async function parseDocx(buffer, options) {
29922
30453
  const { markdown, blocks, metadata, outline, warnings, images } = await parseDocxDocument(buffer, options);
29923
30454
  return { success: true, fileType: "docx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
29924
30455
  } catch (err) {
29925
- return { success: false, fileType: "docx", error: err instanceof Error ? err.message : "DOCX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
30456
+ return { success: false, fileType: "docx", error: sanitizeError(err), code: classifyError(err) };
29926
30457
  }
29927
30458
  }
29928
30459
  async function parseHwpml(buffer, options) {
@@ -29930,7 +30461,7 @@ async function parseHwpml(buffer, options) {
29930
30461
  const { markdown, blocks, metadata, outline, warnings } = parseHwpmlDocument(buffer, options);
29931
30462
  return { success: true, fileType: "hwpml", markdown, blocks, metadata, outline, warnings };
29932
30463
  } catch (err) {
29933
- return { success: false, fileType: "hwpml", error: err instanceof Error ? err.message : "HWPML \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
30464
+ return { success: false, fileType: "hwpml", error: sanitizeError(err), code: classifyError(err) };
29934
30465
  }
29935
30466
  }
29936
30467
  async function fillForm(input, values, outputFormat = "markdown") {
@@ -29973,6 +30504,7 @@ async function fillForm(input, values, outputFormat = "markdown") {
29973
30504
  return { output: markdown, format: "markdown", fill };
29974
30505
  }
29975
30506
  export {
30507
+ DEFAULT_REDACT_RULES,
29976
30508
  HwpxSession,
29977
30509
  PRESET_ALIAS,
29978
30510
  SPACE_EM_FIXED,
@@ -29980,6 +30512,7 @@ export {
29980
30512
  VERSION,
29981
30513
  ValueCursor,
29982
30514
  applySplices,
30515
+ blocksToChunks,
29983
30516
  blocksToMarkdown,
29984
30517
  blocksToPdf,
29985
30518
  buildParagraphSplices,
@@ -30027,6 +30560,8 @@ export {
30027
30560
  patchHwpx,
30028
30561
  patchHwpxBlocks,
30029
30562
  placeSealHwpx,
30563
+ redactMarkdown,
30564
+ redactText,
30030
30565
  renderHtml,
30031
30566
  renderHwpxToSvg,
30032
30567
  scanSectionXml,