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
@@ -3,24 +3,17 @@ import {
3
3
  HEADING_RATIO_H1,
4
4
  HEADING_RATIO_H2,
5
5
  HEADING_RATIO_H3,
6
- MAX_COLS,
7
- MAX_ROWS,
8
- blocksToMarkdown,
9
- buildTable,
10
- convertTableToText,
11
- dedupeRunningHeaders,
12
- flattenLayoutTables,
13
- mapPuaText
14
- } from "./chunk-2SN3CKME.js";
6
+ inlineImagesIntoMarkdown
7
+ } from "./chunk-UOO7LSDG.js";
15
8
  import {
16
9
  detectFormat,
17
10
  detectOle2Format,
18
11
  detectZipFormat,
19
12
  parseLenientCfb
20
- } from "./chunk-CFQLK3LM.js";
13
+ } from "./chunk-YOO6ET6M.js";
21
14
  import {
22
15
  parsePageRange
23
- } from "./chunk-3TBUDJDE.js";
16
+ } from "./chunk-MOL7MDBG.js";
24
17
  import {
25
18
  allLinesegRemovalSplices,
26
19
  applySplices,
@@ -32,25 +25,36 @@ import {
32
25
  paraTextPureT,
33
26
  patchZipEntries,
34
27
  scanSectionXml
35
- } from "./chunk-NGSHDBQR.js";
28
+ } from "./chunk-AX2R5Q2N.js";
29
+ import {
30
+ MAX_COLS,
31
+ MAX_ROWS,
32
+ blocksToMarkdown,
33
+ buildTable,
34
+ convertTableToText,
35
+ dedupeRunningHeaders,
36
+ flattenLayoutTables,
37
+ mapPuaText
38
+ } from "./chunk-SSTK6IKK.js";
36
39
  import {
37
40
  SPACE_EM_FIXED,
38
41
  charWidthEm1000,
39
42
  fitRatioForFewerLines,
40
43
  measureTextWidth,
41
44
  simulateWrap
42
- } from "./chunk-BKWHX3RC.js";
45
+ } from "./chunk-37HJHCPA.js";
43
46
  import {
44
47
  MAX_DECOMPRESS_SIZE,
45
48
  MAX_XML_DEPTH,
46
49
  MAX_ZIP_ENTRIES,
50
+ ZipBombError,
47
51
  applyPageText,
48
52
  clampSpan,
49
53
  createSectionShared,
50
54
  createXmlParser,
51
55
  extractTextFromNode,
52
56
  findChildByLocalName
53
- } from "./chunk-Q7EN4EUJ.js";
57
+ } from "./chunk-GQVSHGG4.js";
54
58
  import {
55
59
  KordocError,
56
60
  classifyError,
@@ -58,10 +62,11 @@ import {
58
62
  isPathTraversal,
59
63
  normalizeSectionHref,
60
64
  precheckZipSize,
65
+ sanitizeError,
61
66
  sanitizeHref,
62
67
  stripDtd,
63
68
  toArrayBuffer
64
- } from "./chunk-CNM75ZSX.js";
69
+ } from "./chunk-O5VS7RIR.js";
65
70
 
66
71
  // src/index.ts
67
72
  import { readFile } from "fs/promises";
@@ -1276,6 +1281,7 @@ function toRoman(n) {
1276
1281
  return out;
1277
1282
  }
1278
1283
  function formatHeadNumber(n, numFormat) {
1284
+ if (n === 0 && numFormat === "DIGIT") return "0";
1279
1285
  if (n <= 0) n = 1;
1280
1286
  switch (numFormat) {
1281
1287
  case "DIGIT":
@@ -1333,25 +1339,25 @@ function resolveParaHeading(paraEl, ctx) {
1333
1339
  if (!numDef) return headingLevel ? { headingLevel } : null;
1334
1340
  let counters = ctx.shared.numState.get(numId);
1335
1341
  if (!counters) {
1336
- counters = new Array(11).fill(0);
1342
+ counters = new Array(11).fill(-1);
1337
1343
  ctx.shared.numState.set(numId, counters);
1338
1344
  }
1339
1345
  const head = numDef.heads.get(level);
1340
- counters[level] = counters[level] === 0 ? head?.start ?? 1 : counters[level] + 1;
1341
- for (let l = level + 1; l <= 10; l++) counters[l] = 0;
1346
+ counters[level] = counters[level] < 0 ? head?.start ?? 1 : counters[level] + 1;
1347
+ for (let l = level + 1; l <= 10; l++) counters[l] = -1;
1342
1348
  const fmtText = head ? head.text.trim() : `^${level}.`;
1343
1349
  const prefix = fmtText.replace(/\^(10|[1-9])/g, (_, d) => {
1344
1350
  const lv = parseInt(d, 10);
1345
1351
  const refHead = numDef.heads.get(lv);
1346
- const n = counters[lv] || refHead?.start || 1;
1352
+ const n = counters[lv] >= 0 ? counters[lv] : refHead?.start ?? 1;
1347
1353
  return formatHeadNumber(n, refHead?.numFormat || "DIGIT");
1348
1354
  });
1349
1355
  return { prefix: prefix || void 0, headingLevel };
1350
1356
  }
1351
1357
 
1352
1358
  // src/hwpx/table-build.ts
1353
- function buildTableWithCellMeta(state) {
1354
- const table2 = buildTable(state.rows);
1359
+ function buildTableWithCellMeta(state, keepAnchoredEmptyCols) {
1360
+ const table2 = buildTable(state.rows, { keepAnchoredEmptyCols });
1355
1361
  if (state.caption) table2.caption = state.caption;
1356
1362
  const anchors = [];
1357
1363
  {
@@ -1415,7 +1421,7 @@ function completeTable(newTable, tableStack, blocks, ctx) {
1415
1421
  if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
1416
1422
  return parentTable;
1417
1423
  }
1418
- const ir = buildTableWithCellMeta(newTable);
1424
+ const ir = buildTableWithCellMeta(newTable, ctx.shared.keepTrailingEmptyCols);
1419
1425
  const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
1420
1426
  if (parentTable?.cell) {
1421
1427
  const cell2 = parentTable.cell;
@@ -1449,7 +1455,8 @@ function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
1449
1455
  walkSection(doc.documentElement, blocks, null, [], ctx);
1450
1456
  return blocks;
1451
1457
  }
1452
- function extractImageRef(el) {
1458
+ function extractImageRef(el, depth = 0) {
1459
+ if (depth > MAX_XML_DEPTH) return null;
1453
1460
  const children = el.childNodes;
1454
1461
  if (!children) return null;
1455
1462
  for (let i = 0; i < children.length; i++) {
@@ -1460,7 +1467,7 @@ function extractImageRef(el) {
1460
1467
  const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
1461
1468
  if (ref) return ref;
1462
1469
  }
1463
- const nested = extractImageRef(child);
1470
+ const nested = extractImageRef(child, depth + 1);
1464
1471
  if (nested) return nested;
1465
1472
  }
1466
1473
  const directRef = el.getAttribute("binaryItemIDRef") || "";
@@ -1567,7 +1574,7 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1567
1574
  ;
1568
1575
  (cell2.blocks ??= []).push(cellBlock);
1569
1576
  } else if (!tableCtx) {
1570
- if (/^─{10,}$/.test(text)) {
1577
+ if (ctx.shared.kordocLayout && /^─{10,}$/.test(text)) {
1571
1578
  blocks.push({ type: "separator", pageNumber: ctx.sectionNum });
1572
1579
  tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1573
1580
  break;
@@ -1843,8 +1850,8 @@ function extractDrawTextBlocks(drawTextNode, blocks, ctx) {
1843
1850
  } else {
1844
1851
  const info = extractParagraphInfo(child, ctx.styleMap, ctx);
1845
1852
  let text = info.text.trim();
1853
+ const ph = resolveParaHeading(child, ctx);
1846
1854
  if (text) {
1847
- const ph = resolveParaHeading(child, ctx);
1848
1855
  if (ph?.prefix) text = ph.prefix + " " + text;
1849
1856
  const block = { type: "paragraph", text, style: info.style ?? void 0, pageNumber: ctx.sectionNum };
1850
1857
  if (info.href) block.href = info.href;
@@ -1957,7 +1964,8 @@ function extractParagraphInfo(para2, styleMap, ctx) {
1957
1964
  }
1958
1965
  }
1959
1966
  };
1960
- const walk = (node) => {
1967
+ const walk = (node, depth = 0) => {
1968
+ if (depth > MAX_XML_DEPTH) return;
1961
1969
  const children = node.childNodes;
1962
1970
  if (!children) return;
1963
1971
  for (let i = 0; i < children.length; i++) {
@@ -1978,7 +1986,7 @@ function extractParagraphInfo(para2, styleMap, ctx) {
1978
1986
  const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1979
1987
  switch (tag) {
1980
1988
  case "t":
1981
- walk(child);
1989
+ walk(child, depth + 1);
1982
1990
  break;
1983
1991
  // 자식 순회 (tab 등 하위 요소 처리)
1984
1992
  case "tab": {
@@ -2011,7 +2019,7 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2011
2019
  const safe = sanitizeHref(url);
2012
2020
  if (safe) href = safe;
2013
2021
  }
2014
- walk(child);
2022
+ walk(child, depth + 1);
2015
2023
  break;
2016
2024
  }
2017
2025
  // 각주/미주
@@ -2082,11 +2090,11 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2082
2090
  case "r": {
2083
2091
  const runCharPr = child.getAttribute("charPrIDRef");
2084
2092
  if (runCharPr && !charPrId) charPrId = runCharPr;
2085
- walk(child);
2093
+ walk(child, depth + 1);
2086
2094
  break;
2087
2095
  }
2088
2096
  default:
2089
- walk(child);
2097
+ walk(child, depth + 1);
2090
2098
  break;
2091
2099
  }
2092
2100
  }
@@ -2097,7 +2105,7 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2097
2105
  let cleanText = text.replace(/[ \t]+/g, " ").trim();
2098
2106
  if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
2099
2107
  cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
2100
- cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
2108
+ cleanText = cleanText.replace(/^(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?$/gm, "").trim();
2101
2109
  let style;
2102
2110
  if (styleMap && charPrId) {
2103
2111
  const charProp = styleMap.charProperties.get(charPrId);
@@ -2199,641 +2207,818 @@ function gongmunDepthFromIndent(para2, left, ctx) {
2199
2207
  return depth >= 1 && depth < 8 ? depth : null;
2200
2208
  }
2201
2209
 
2202
- // src/hwpx/images.ts
2203
- function imageExtToMime(ext) {
2204
- switch (ext.toLowerCase()) {
2205
- case "jpg":
2206
- case "jpeg":
2207
- return "image/jpeg";
2208
- case "png":
2209
- return "image/png";
2210
- case "gif":
2211
- return "image/gif";
2212
- case "bmp":
2213
- return "image/bmp";
2214
- case "tif":
2215
- case "tiff":
2216
- return "image/tiff";
2217
- case "wmf":
2218
- return "image/wmf";
2219
- case "emf":
2220
- return "image/emf";
2221
- case "svg":
2222
- return "image/svg+xml";
2223
- default:
2224
- return "application/octet-stream";
2210
+ // src/hwp5/record.ts
2211
+ import { inflateRawSync, inflateSync } from "zlib";
2212
+ var TAG_PARA_HEADER = 66;
2213
+ var TAG_PARA_TEXT = 67;
2214
+ var TAG_CHAR_SHAPE = 68;
2215
+ var TAG_CTRL_HEADER = 71;
2216
+ var TAG_LIST_HEADER = 72;
2217
+ var TAG_TABLE = 77;
2218
+ var TAG_SHAPE_COMPONENT = 76;
2219
+ var TAG_SHAPE_COMPONENT_PICTURE = 85;
2220
+ var TAG_SHAPE_COMPONENT_CONTAINER = 86;
2221
+ var TAG_EQEDIT = 88;
2222
+ var TAG_BIN_DATA = 18;
2223
+ var TAG_DOC_CHAR_SHAPE = 21;
2224
+ var TAG_NUMBERING = 23;
2225
+ var TAG_BULLET = 24;
2226
+ var TAG_DOC_PARA_SHAPE = 25;
2227
+ var TAG_DOC_STYLE = 26;
2228
+ var CHAR_LINE = 0;
2229
+ var CHAR_SECTION_BREAK = 10;
2230
+ var CHAR_PARA = 13;
2231
+ var CHAR_TAB = 9;
2232
+ var CHAR_HYPHEN = 24;
2233
+ var CHAR_NBSP = 30;
2234
+ var CHAR_FIXED_WIDTH = 31;
2235
+ var FLAG_COMPRESSED = 1 << 0;
2236
+ var FLAG_ENCRYPTED = 1 << 1;
2237
+ var FLAG_DISTRIBUTION = 1 << 2;
2238
+ var FLAG_DRM = 1 << 4;
2239
+ var MAX_RECORDS = 5e5;
2240
+ function readRecords(data) {
2241
+ const records = [];
2242
+ let offset = 0;
2243
+ while (offset + 4 <= data.length && records.length < MAX_RECORDS) {
2244
+ const header = data.readUInt32LE(offset);
2245
+ offset += 4;
2246
+ const tagId = header & 1023;
2247
+ const level = header >> 10 & 1023;
2248
+ let size = header >> 20 & 4095;
2249
+ if (size === 4095) {
2250
+ if (offset + 4 > data.length) break;
2251
+ size = data.readUInt32LE(offset);
2252
+ offset += 4;
2253
+ }
2254
+ if (offset + size > data.length) break;
2255
+ records.push({ tagId, level, size, data: data.subarray(offset, offset + size) });
2256
+ offset += size;
2225
2257
  }
2258
+ return records;
2226
2259
  }
2227
- function mimeToExt(mime) {
2228
- if (mime.includes("jpeg")) return "jpg";
2229
- if (mime.includes("png")) return "png";
2230
- if (mime.includes("gif")) return "gif";
2231
- if (mime.includes("bmp")) return "bmp";
2232
- if (mime.includes("tiff")) return "tif";
2233
- if (mime.includes("wmf")) return "wmf";
2234
- if (mime.includes("emf")) return "emf";
2235
- if (mime.includes("svg")) return "svg";
2236
- return "bin";
2237
- }
2238
- function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
2239
- if (depth > MAX_XML_DEPTH) return;
2240
- for (const block of blocks) {
2241
- if (block.type === "image") {
2242
- out.push({ block, ownerCell });
2243
- } else if (block.type === "table" && block.table) {
2244
- for (const row of block.table.cells) {
2245
- for (const cell2 of row) {
2246
- if (cell2.blocks?.length) collectImageBlocks(cell2.blocks, out, cell2, depth + 1);
2247
- }
2248
- }
2260
+ var MAX_DECOMPRESS_SIZE2 = 100 * 1024 * 1024;
2261
+ function decompressStream(data) {
2262
+ const opts = { maxOutputLength: MAX_DECOMPRESS_SIZE2 };
2263
+ if (data.length >= 2 && data[0] === 120) {
2264
+ try {
2265
+ return inflateSync(data, opts);
2266
+ } catch {
2249
2267
  }
2250
2268
  }
2269
+ return inflateRawSync(data, opts);
2251
2270
  }
2252
- async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
2253
- const images = [];
2254
- let imageIndex = 0;
2255
- const imageBlocks = [];
2256
- collectImageBlocks(blocks, imageBlocks);
2257
- const resolved = /* @__PURE__ */ new Map();
2258
- for (const { block, ownerCell } of imageBlocks) {
2259
- if (block.type !== "image" || !block.text) continue;
2260
- const ref = block.text;
2261
- let img = resolved.get(ref);
2262
- if (img === void 0) {
2263
- img = null;
2264
- const candidates = [
2265
- `BinData/${ref}`,
2266
- `Contents/BinData/${ref}`,
2267
- ref
2268
- // 절대 경로일 수도 있음
2269
- ];
2270
- let resolvedPath = null;
2271
- if (!ref.includes(".")) {
2272
- const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
2273
- for (const prefix of prefixes) {
2274
- const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
2275
- if (match.length > 0) {
2276
- resolvedPath = match[0].name;
2277
- break;
2278
- }
2271
+ function parseFileHeader(data) {
2272
+ if (data.length < 40) throw new KordocError("FileHeader\uAC00 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (\uCD5C\uC18C 40\uBC14\uC774\uD2B8)");
2273
+ const sig = data.subarray(0, 32).toString("utf8").replace(/\0+$/, "");
2274
+ return {
2275
+ signature: sig,
2276
+ versionMajor: data[35],
2277
+ flags: data.readUInt32LE(36)
2278
+ };
2279
+ }
2280
+ function readHwpString(data, offset) {
2281
+ if (offset + 2 > data.length) return { value: "", next: data.length };
2282
+ const len = data.readUInt16LE(offset);
2283
+ const start = offset + 2;
2284
+ const end = start + len * 2;
2285
+ if (len === 0 || end > data.length) return { value: "", next: start };
2286
+ return { value: data.subarray(start, end).toString("utf16le"), next: end };
2287
+ }
2288
+ function parseDocInfo(records) {
2289
+ const charShapes = [];
2290
+ const paraShapes = [];
2291
+ const styles = [];
2292
+ const binData = [];
2293
+ const numberings = [];
2294
+ const bullets = [];
2295
+ for (const rec of records) {
2296
+ if (rec.tagId === TAG_DOC_PARA_SHAPE && rec.data.length >= 4) {
2297
+ const attr1 = rec.data.readUInt32LE(0);
2298
+ const headType = attr1 >>> 23 & 3;
2299
+ const paraLevel = attr1 >>> 25 & 7;
2300
+ const numberingId = rec.data.length >= 32 ? rec.data.readUInt16LE(30) : 0;
2301
+ paraShapes.push({ headType, paraLevel, numberingId });
2302
+ }
2303
+ if (rec.tagId === TAG_BIN_DATA && rec.data.length >= 2) {
2304
+ const attr = rec.data.readUInt16LE(0);
2305
+ const typeBits = attr & 15;
2306
+ if (typeBits === 0) {
2307
+ binData.push({ kind: "link", storageId: 0, extension: "" });
2308
+ } else {
2309
+ const storageId = rec.data.length >= 4 ? rec.data.readUInt16LE(2) : 0;
2310
+ const { value: extension } = readHwpString(rec.data, 4);
2311
+ binData.push({ kind: typeBits === 2 ? "storage" : "embed", storageId, extension });
2312
+ }
2313
+ }
2314
+ if (rec.tagId === TAG_NUMBERING && rec.data.length >= 14) {
2315
+ const levelFormats = [];
2316
+ const numberFormats = [];
2317
+ const startNumbers = [1, 1, 1, 1, 1, 1, 1];
2318
+ let offset = 0;
2319
+ for (let level = 0; level < 7; level++) {
2320
+ if (offset + 12 > rec.data.length) {
2321
+ levelFormats.push("");
2322
+ numberFormats.push(0);
2323
+ continue;
2279
2324
  }
2325
+ const attr = rec.data.readUInt32LE(offset);
2326
+ numberFormats.push(attr >>> 5 & 15);
2327
+ offset += 12;
2328
+ const { value, next } = readHwpString(rec.data, offset);
2329
+ levelFormats.push(value);
2330
+ offset = next;
2280
2331
  }
2281
- const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
2282
- for (const path of allCandidates) {
2283
- if (isPathTraversal(path)) continue;
2284
- const file = zip.file(path);
2285
- if (!file) continue;
2286
- try {
2287
- const data = await file.async("uint8array");
2288
- decompressed.total += data.length;
2289
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2290
- const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
2291
- const mimeType = imageExtToMime(ext);
2292
- imageIndex++;
2293
- const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
2294
- img = { filename, data, mimeType };
2295
- images.push(img);
2296
- break;
2297
- } catch (err) {
2298
- if (err instanceof KordocError) throw err;
2332
+ let baseStart = 1;
2333
+ if (offset + 2 <= rec.data.length) {
2334
+ baseStart = rec.data.readUInt16LE(offset) || 1;
2335
+ offset += 2;
2336
+ }
2337
+ for (let level = 0; level < 7; level++) {
2338
+ if (offset + 4 <= rec.data.length) {
2339
+ startNumbers[level] = rec.data.readUInt32LE(offset) || 1;
2340
+ offset += 4;
2341
+ } else {
2342
+ startNumbers[level] = baseStart;
2299
2343
  }
2300
2344
  }
2301
- if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
2302
- resolved.set(ref, img);
2345
+ numberings.push({ levelFormats, numberFormats, startNumbers });
2303
2346
  }
2304
- if (!img) {
2305
- block.type = "paragraph";
2306
- block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
2307
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\uC774\uBBF8\uC9C0: ${ref}]`);
2308
- continue;
2347
+ if (rec.tagId === TAG_BULLET && rec.data.length >= 14) {
2348
+ const code = rec.data.readUInt16LE(12);
2349
+ bullets.push({ char: code > 0 ? String.fromCharCode(code) : "\u2022" });
2309
2350
  }
2310
- block.text = img.filename;
2311
- block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
2312
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `![image](${img.filename})`);
2313
- }
2314
- return images;
2315
- }
2316
-
2317
- // src/hwpx/metadata.ts
2318
- import JSZip from "jszip";
2319
-
2320
- // src/hwpx/zip-sections.ts
2321
- import { inflateRawSync } from "zlib";
2322
- function extractFromBrokenZip(buffer) {
2323
- const data = new Uint8Array(buffer);
2324
- const view = new DataView(buffer);
2325
- let pos = 0;
2326
- const blocks = [];
2327
- const warnings = [
2328
- { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
2329
- ];
2330
- let totalDecompressed = 0;
2331
- let entryCount = 0;
2332
- let sectionNum = 0;
2333
- const shared = createSectionShared();
2334
- while (pos < data.length - 30) {
2335
- if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
2336
- pos++;
2337
- while (pos < data.length - 30) {
2338
- if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
2339
- pos++;
2351
+ if (rec.tagId === TAG_DOC_CHAR_SHAPE && rec.data.length >= 18) {
2352
+ if (rec.data.length >= 50) {
2353
+ const fontSize = rec.data.readUInt32LE(42);
2354
+ const attrFlags = rec.data.readUInt32LE(46);
2355
+ charShapes.push({ fontSize, attrFlags });
2356
+ } else {
2357
+ charShapes.push({ fontSize: 0, attrFlags: 0 });
2340
2358
  }
2341
- continue;
2342
- }
2343
- if (++entryCount > MAX_ZIP_ENTRIES) break;
2344
- const method = view.getUint16(pos + 8, true);
2345
- const compSize = view.getUint32(pos + 18, true);
2346
- const nameLen = view.getUint16(pos + 26, true);
2347
- const extraLen = view.getUint16(pos + 28, true);
2348
- if (nameLen > 1024 || extraLen > 65535) {
2349
- pos += 30 + nameLen + extraLen;
2350
- continue;
2351
- }
2352
- const fileStart = pos + 30 + nameLen + extraLen;
2353
- if (fileStart + compSize > data.length) break;
2354
- if (compSize === 0 && method !== 0) {
2355
- pos = fileStart;
2356
- continue;
2357
- }
2358
- const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
2359
- const name = new TextDecoder().decode(nameBytes);
2360
- if (isPathTraversal(name)) {
2361
- pos = fileStart + compSize;
2362
- continue;
2363
2359
  }
2364
- const fileData = data.slice(fileStart, fileStart + compSize);
2365
- pos = fileStart + compSize;
2366
- if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
2367
- try {
2368
- let content;
2369
- if (method === 0) {
2370
- content = new TextDecoder().decode(fileData);
2371
- } else if (method === 8) {
2372
- const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
2373
- content = new TextDecoder().decode(decompressed);
2374
- } else {
2375
- continue;
2360
+ if (rec.tagId === TAG_DOC_STYLE && rec.data.length >= 8) {
2361
+ try {
2362
+ let offset = 0;
2363
+ const nameLen = rec.data.readUInt16LE(offset);
2364
+ offset += 2;
2365
+ const nameBytes = nameLen * 2;
2366
+ const name = nameBytes > 0 && offset + nameBytes <= rec.data.length ? rec.data.subarray(offset, offset + nameBytes).toString("utf16le") : "";
2367
+ offset += nameBytes;
2368
+ let nameKo = "";
2369
+ if (offset + 2 <= rec.data.length) {
2370
+ const nameKoLen = rec.data.readUInt16LE(offset);
2371
+ offset += 2;
2372
+ const nameKoBytes = nameKoLen * 2;
2373
+ if (nameKoBytes > 0 && offset + nameKoBytes <= rec.data.length) {
2374
+ nameKo = rec.data.subarray(offset, offset + nameKoBytes).toString("utf16le");
2375
+ }
2376
+ offset += nameKoBytes;
2377
+ }
2378
+ const type = offset < rec.data.length ? rec.data.readUInt8(offset) : 0;
2379
+ offset += 1;
2380
+ offset += 1;
2381
+ offset += 2;
2382
+ const paraShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2383
+ offset += 2;
2384
+ const charShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2385
+ styles.push({ name, nameKo, charShapeId, paraShapeId, type });
2386
+ } catch {
2376
2387
  }
2377
- totalDecompressed += content.length * 2;
2378
- if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
2379
- sectionNum++;
2380
- blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
2381
- } catch {
2382
- continue;
2383
2388
  }
2384
2389
  }
2385
- 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");
2386
- applyPageText(blocks, shared);
2387
- const markdown = blocksToMarkdown(blocks);
2388
- return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
2390
+ return { charShapes, paraShapes, styles, binData, numberings, bullets };
2389
2391
  }
2390
- async function readKordocLayout(zip) {
2391
- const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
2392
- if (!file) return null;
2393
- try {
2394
- const xml = await file.async("text");
2395
- if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
2396
- return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
2397
- } catch {
2398
- return null;
2399
- }
2392
+ function createParaTextState() {
2393
+ return { text: "", ctrlIdx: 0, fieldStack: [], fieldRanges: [] };
2400
2394
  }
2401
- async function resolveSectionPaths(zip) {
2402
- const manifestPaths = ["Contents/content.hpf", "content.hpf"];
2403
- for (const mp of manifestPaths) {
2404
- const mpLower = mp.toLowerCase();
2405
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
2406
- if (!file) continue;
2407
- const xml = await file.async("text");
2408
- const paths = parseSectionPathsFromManifest(xml);
2409
- if (paths.length > 0) return paths;
2410
- }
2411
- const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
2412
- return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
2395
+ function isExtendedOnlyCtrlChar(ch) {
2396
+ return ch >= 1 && ch <= 3 || ch >= 11 && ch <= 12 || ch >= 14 && ch <= 18 || ch >= 21 && ch <= 23;
2413
2397
  }
2414
- function parseSectionPathsFromManifest(xml) {
2415
- const parser = createXmlParser();
2416
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2417
- const items = doc.getElementsByTagName("opf:item");
2418
- const spine = doc.getElementsByTagName("opf:itemref");
2419
- const idToHref = /* @__PURE__ */ new Map();
2420
- for (let i = 0; i < items.length; i++) {
2421
- const item = items[i];
2422
- const id = item.getAttribute("id") || "";
2423
- const href = normalizeSectionHref(item.getAttribute("href") || "");
2424
- if (id && href) idToHref.set(id, href);
2425
- }
2426
- if (spine.length > 0) {
2427
- const ordered = [];
2428
- for (let i = 0; i < spine.length; i++) {
2429
- const href = idToHref.get(spine[i].getAttribute("idref") || "");
2430
- if (href) ordered.push(href);
2398
+ function appendParaText(state, data, resolveControl) {
2399
+ let result = "";
2400
+ let i = 0;
2401
+ const base = state.text.length;
2402
+ const resolveAt = (byteOffset, extended) => {
2403
+ const ctrlId = data.readUInt32LE(byteOffset);
2404
+ const idx = extended ? state.ctrlIdx : -1;
2405
+ const replacement = resolveControl?.(idx, ctrlId);
2406
+ if (replacement) result += replacement;
2407
+ if (extended) state.ctrlIdx++;
2408
+ };
2409
+ while (i + 1 < data.length) {
2410
+ const ch = data.readUInt16LE(i);
2411
+ i += 2;
2412
+ switch (ch) {
2413
+ // ── char 타입 (2바이트만, 확장 데이터 없음) ──
2414
+ case CHAR_LINE:
2415
+ result += "\n";
2416
+ break;
2417
+ case CHAR_SECTION_BREAK: {
2418
+ if (i + 16 <= data.length && data.readUInt16LE(i) === 11) {
2419
+ resolveAt(i + 2, true);
2420
+ i += 16;
2421
+ break;
2422
+ }
2423
+ result += "\n";
2424
+ break;
2425
+ }
2426
+ case CHAR_PARA:
2427
+ break;
2428
+ // 문단 끝
2429
+ case CHAR_HYPHEN:
2430
+ result += "-";
2431
+ break;
2432
+ case CHAR_NBSP:
2433
+ result += "\xA0";
2434
+ break;
2435
+ // 진짜 NBSP
2436
+ case CHAR_FIXED_WIDTH:
2437
+ result += " ";
2438
+ break;
2439
+ // 고정폭 공백
2440
+ // ── inline 타입 (2바이트 + 14바이트 확장) ──
2441
+ case CHAR_TAB:
2442
+ result += " ";
2443
+ if (i + 14 <= data.length) i += 14;
2444
+ break;
2445
+ default:
2446
+ if (ch >= 1 && ch <= 31) {
2447
+ const isExtended = isExtendedOnlyCtrlChar(ch);
2448
+ const isInline = ch >= 4 && ch <= 9 || ch >= 19 && ch <= 20;
2449
+ if ((isExtended || isInline) && i + 14 <= data.length) {
2450
+ if (ch === 3) {
2451
+ state.fieldStack.push({ start: base + result.length, ctrlIdx: state.ctrlIdx });
2452
+ } else if (ch === 4) {
2453
+ const open = state.fieldStack.pop();
2454
+ if (open) {
2455
+ state.fieldRanges.push({ start: open.start, end: base + result.length, ctrlIdx: open.ctrlIdx });
2456
+ }
2457
+ }
2458
+ resolveAt(i, isExtended);
2459
+ i += 14;
2460
+ }
2461
+ } else if (ch >= 32) {
2462
+ if (ch >= 55296 && ch <= 56319 && i + 1 < data.length) {
2463
+ const lo = data.readUInt16LE(i);
2464
+ if (lo >= 56320 && lo <= 57343) {
2465
+ i += 2;
2466
+ const codePoint = (ch - 55296 << 10) + (lo - 56320) + 65536;
2467
+ result += String.fromCodePoint(codePoint);
2468
+ break;
2469
+ }
2470
+ }
2471
+ result += String.fromCharCode(ch);
2472
+ }
2473
+ break;
2431
2474
  }
2432
- if (ordered.length > 0) return ordered;
2433
2475
  }
2434
- return Array.from(idToHref.values()).sort(compareSectionPaths);
2476
+ state.text += result;
2477
+ }
2478
+ function extractEquationText(data) {
2479
+ if (data.length < 6) return null;
2480
+ const scriptLength = data.readUInt16LE(4);
2481
+ const scriptStart = 6;
2482
+ const scriptEnd = scriptStart + scriptLength * 2;
2483
+ if (scriptLength <= 0 || scriptEnd > data.length) return null;
2484
+ const equation = data.subarray(scriptStart, scriptEnd).toString("utf16le").replace(/\0+/g, "").trim();
2485
+ return equation || null;
2435
2486
  }
2436
2487
 
2437
- // src/hwpx/metadata.ts
2438
- async function extractHwpxMetadata(zip, metadata, decompressed) {
2488
+ // src/hwp5/images.ts
2489
+ function detectImageMime(data) {
2490
+ if (data.length < 4) return null;
2491
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
2492
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
2493
+ if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
2494
+ if (data[0] === 66 && data[1] === 77) return "image/bmp";
2495
+ if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
2496
+ if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
2497
+ return null;
2498
+ }
2499
+ function normalizeBinPayload(data) {
2500
+ if (detectImageMime(data)) return data;
2439
2501
  try {
2440
- const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
2441
- for (const mp of metaPaths) {
2442
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
2443
- if (!file) continue;
2444
- const xml = await file.async("text");
2445
- if (decompressed) {
2446
- decompressed.total += xml.length * 2;
2447
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2502
+ const inflated = decompressStream(data);
2503
+ if (inflated.length > 0) return inflated;
2504
+ } catch {
2505
+ }
2506
+ return data;
2507
+ }
2508
+ var BIN_ENTRY_RE = /(?:^|\/)BIN([0-9A-Fa-f]{4,8})(?:\.[^./\\]*)?$/;
2509
+ function collectImageBlocks(blocks, out) {
2510
+ for (const b of blocks) {
2511
+ if (b.type === "image") out.push(b);
2512
+ if (b.table) {
2513
+ for (const row of b.table.cells) {
2514
+ for (const cell2 of row) {
2515
+ if (cell2.blocks) collectImageBlocks(cell2.blocks, out);
2516
+ }
2448
2517
  }
2449
- parseDublinCoreMetadata(xml, metadata);
2450
- if (metadata.title || metadata.author) return;
2451
2518
  }
2452
- } catch {
2519
+ if (b.children) collectImageBlocks(b.children, out);
2453
2520
  }
2454
2521
  }
2455
- function parseDublinCoreMetadata(xml, metadata) {
2456
- const parser = createXmlParser();
2457
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2458
- if (!doc.documentElement) return;
2459
- const getText = (tagNames) => {
2460
- for (const tag of tagNames) {
2461
- const els = doc.getElementsByTagName(tag);
2462
- if (els.length > 0) {
2463
- const text = els[0].textContent?.trim();
2464
- if (text) return text;
2522
+ function forEachTableCell(blocks, fn) {
2523
+ for (const b of blocks) {
2524
+ if (b.table) {
2525
+ for (const row of b.table.cells) {
2526
+ for (const cell2 of row) {
2527
+ fn(cell2);
2528
+ if (cell2.blocks) forEachTableCell(cell2.blocks, fn);
2529
+ }
2465
2530
  }
2466
2531
  }
2467
- return void 0;
2468
- };
2469
- metadata.title = metadata.title || getText(["dc:title", "title"]);
2470
- metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
2471
- metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
2472
- metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
2473
- metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
2474
- const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
2475
- if (keywords && !metadata.keywords) {
2476
- metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
2532
+ if (b.children) forEachTableCell(b.children, fn);
2477
2533
  }
2478
2534
  }
2479
- async function extractHwpxMetadataOnly(buffer) {
2480
- let zip;
2481
- try {
2482
- zip = await JSZip.loadAsync(buffer);
2483
- } catch {
2484
- throw new KordocError("HWPX ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
2485
- }
2486
- const metadata = {};
2487
- await extractHwpxMetadata(zip, metadata);
2488
- const sectionPaths = await resolveSectionPaths(zip);
2489
- metadata.pageCount = sectionPaths.length;
2490
- return metadata;
2535
+ var CELL_IMAGE_SENTINEL_RE = /!\[image\]\(hwp5bin:(\d+)\)/g;
2536
+ function resolveCellImageSentinels(blocks, renamed) {
2537
+ forEachTableCell(blocks, (cell2) => {
2538
+ if (!cell2.text.includes("hwp5bin:")) return;
2539
+ cell2.text = cell2.text.replace(CELL_IMAGE_SENTINEL_RE, (_m, idStr) => {
2540
+ const filename = renamed.get(Number(idStr));
2541
+ return filename ? `![image](${filename})` : "[\uC774\uBBF8\uC9C0]";
2542
+ });
2543
+ });
2491
2544
  }
2492
-
2493
- // src/hwpx/parser.ts
2494
- async function parseHwpxDocument(buffer, options) {
2495
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
2496
- let zip;
2497
- try {
2498
- zip = await JSZip2.loadAsync(buffer);
2499
- } catch {
2500
- return extractFromBrokenZip(buffer);
2501
- }
2502
- const actualEntryCount = Object.keys(zip.files).length;
2503
- if (actualEntryCount > MAX_ZIP_ENTRIES) {
2504
- throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2505
- }
2506
- const manifestFile = zip.file("META-INF/manifest.xml");
2507
- if (manifestFile) {
2508
- const manifestXml = await manifestFile.async("text");
2509
- if (isEncryptedHwpx(manifestXml)) {
2510
- if (isComFallbackAvailable() && options?.filePath) {
2511
- const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
2512
- if (pages.some((p) => p && p.trim().length > 0)) {
2513
- return comResultToParseResult(pages, pageCount, warnings2);
2545
+ function resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced) {
2546
+ const imageBlocks = [];
2547
+ collectImageBlocks(blocks, imageBlocks);
2548
+ const images = [];
2549
+ const renamed = /* @__PURE__ */ new Map();
2550
+ const resolved = /* @__PURE__ */ new Map();
2551
+ let imageIndex = 0;
2552
+ for (const block of imageBlocks) {
2553
+ if (!block.text) continue;
2554
+ const storageId = parseInt(block.text, 10);
2555
+ if (isNaN(storageId)) continue;
2556
+ let img = resolved.get(storageId);
2557
+ if (img === void 0) {
2558
+ const bin = binDataMap.get(storageId);
2559
+ if (!bin) {
2560
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
2561
+ resolved.set(storageId, null);
2562
+ } else {
2563
+ const mime = detectImageMime(bin.data);
2564
+ if (!mime) {
2565
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
2566
+ resolved.set(storageId, null);
2567
+ } else {
2568
+ imageIndex++;
2569
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
2570
+ img = { filename: `image_${String(imageIndex).padStart(3, "0")}.${ext}`, data: new Uint8Array(bin.data), mime };
2571
+ resolved.set(storageId, img);
2572
+ images.push({ filename: img.filename, data: img.data, mimeType: img.mime });
2573
+ renamed.set(storageId, img.filename);
2514
2574
  }
2515
2575
  }
2516
- 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.");
2576
+ img = resolved.get(storageId);
2577
+ }
2578
+ if (!img) {
2579
+ const bin = binDataMap.get(storageId);
2580
+ block.type = "paragraph";
2581
+ block.text = bin ? `[\uC774\uBBF8\uC9C0: ${bin.name}]` : `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
2582
+ continue;
2517
2583
  }
2584
+ block.text = img.filename;
2585
+ block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
2518
2586
  }
2519
- const decompressed = { total: 0 };
2520
- const metadata = {};
2521
- await extractHwpxMetadata(zip, metadata, decompressed);
2522
- const styleMap = await extractHwpxStyles(zip, decompressed);
2523
- const warnings = [];
2524
- const sectionPaths = await resolveSectionPaths(zip);
2525
- if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
2526
- metadata.pageCount = sectionPaths.length;
2527
- const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
2528
- const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
2529
- const blocks = [];
2530
- const shared = createSectionShared();
2531
- shared.kordocLayout = await readKordocLayout(zip);
2532
- let parsedSections = 0;
2533
- for (let si = 0; si < sectionPaths.length; si++) {
2534
- if (pageFilter && !pageFilter.has(si + 1)) continue;
2535
- const file = zip.file(sectionPaths[si]);
2536
- if (!file) continue;
2537
- try {
2538
- const xml = await file.async("text");
2539
- decompressed.total += xml.length * 2;
2540
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2541
- blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
2542
- parsedSections++;
2543
- options?.onProgress?.(parsedSections, totalTarget);
2544
- } catch (secErr) {
2545
- if (secErr instanceof KordocError) throw secErr;
2546
- 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" });
2587
+ if (sweepUnreferenced) {
2588
+ for (const [storageId, bin] of [...binDataMap.entries()].sort((a, b) => a[0] - b[0])) {
2589
+ if (resolved.has(storageId)) continue;
2590
+ const mime = detectImageMime(bin.data);
2591
+ if (!mime) continue;
2592
+ imageIndex++;
2593
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
2594
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
2595
+ const data = new Uint8Array(bin.data);
2596
+ images.push({ filename, data, mimeType: mime });
2597
+ blocks.push({ type: "image", text: filename, imageData: { data, mimeType: mime, filename: bin.name } });
2547
2598
  }
2548
2599
  }
2549
- applyPageText(blocks, shared);
2550
- const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
2551
- detectHwpxHeadings(blocks, styleMap);
2552
- const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
2553
- const markdown = blocksToMarkdown(blocks);
2554
- 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 };
2600
+ resolveCellImageSentinels(blocks, renamed);
2601
+ return images;
2555
2602
  }
2556
-
2557
- // src/hwp5/record.ts
2558
- import { inflateRawSync as inflateRawSync2, inflateSync } from "zlib";
2559
- var TAG_PARA_HEADER = 66;
2560
- var TAG_PARA_TEXT = 67;
2561
- var TAG_CHAR_SHAPE = 68;
2562
- var TAG_CTRL_HEADER = 71;
2563
- var TAG_LIST_HEADER = 72;
2564
- var TAG_TABLE = 77;
2565
- var TAG_SHAPE_COMPONENT = 76;
2566
- var TAG_SHAPE_COMPONENT_PICTURE = 85;
2567
- var TAG_SHAPE_COMPONENT_CONTAINER = 86;
2568
- var TAG_EQEDIT = 88;
2569
- var TAG_BIN_DATA = 18;
2570
- var TAG_DOC_CHAR_SHAPE = 21;
2571
- var TAG_NUMBERING = 23;
2572
- var TAG_BULLET = 24;
2573
- var TAG_DOC_PARA_SHAPE = 25;
2574
- var TAG_DOC_STYLE = 26;
2575
- var CHAR_LINE = 0;
2576
- var CHAR_SECTION_BREAK = 10;
2577
- var CHAR_PARA = 13;
2578
- var CHAR_TAB = 9;
2579
- var CHAR_HYPHEN = 30;
2580
- var CHAR_NBSP = 31;
2581
- var CHAR_FIXED_NBSP = 24;
2582
- var CHAR_FIXED_WIDTH = 25;
2583
- var FLAG_COMPRESSED = 1 << 0;
2584
- var FLAG_ENCRYPTED = 1 << 1;
2585
- var FLAG_DISTRIBUTION = 1 << 2;
2586
- var FLAG_DRM = 1 << 4;
2587
- var MAX_RECORDS = 5e5;
2588
- function readRecords(data) {
2589
- const records = [];
2590
- let offset = 0;
2591
- while (offset + 4 <= data.length && records.length < MAX_RECORDS) {
2592
- const header = data.readUInt32LE(offset);
2593
- offset += 4;
2594
- const tagId = header & 1023;
2595
- const level = header >> 10 & 1023;
2596
- let size = header >> 20 & 4095;
2597
- if (size === 4095) {
2598
- if (offset + 4 > data.length) break;
2599
- size = data.readUInt32LE(offset);
2600
- offset += 4;
2603
+ function extractHwp5Images(fileIndex, blocks, warnings, sweepUnreferenced) {
2604
+ const binDataMap = /* @__PURE__ */ new Map();
2605
+ if (fileIndex) {
2606
+ for (const entry of fileIndex) {
2607
+ if (!entry?.name || !entry.content) continue;
2608
+ const match = entry.name.match(BIN_ENTRY_RE);
2609
+ if (!match) continue;
2610
+ const idx = parseInt(match[1], 16);
2611
+ const data = normalizeBinPayload(Buffer.from(entry.content));
2612
+ binDataMap.set(idx, { data, name: entry.name });
2601
2613
  }
2602
- if (offset + size > data.length) break;
2603
- records.push({ tagId, level, size, data: data.subarray(offset, offset + size) });
2604
- offset += size;
2605
2614
  }
2606
- return records;
2615
+ if (binDataMap.size === 0) {
2616
+ resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
2617
+ return [];
2618
+ }
2619
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
2607
2620
  }
2608
- var MAX_DECOMPRESS_SIZE2 = 100 * 1024 * 1024;
2609
- function decompressStream(data) {
2610
- const opts = { maxOutputLength: MAX_DECOMPRESS_SIZE2 };
2611
- if (data.length >= 2 && data[0] === 120) {
2612
- try {
2613
- return inflateSync(data, opts);
2614
- } catch {
2615
- }
2621
+ function extractHwp5ImagesLenient(lcfb, blocks, warnings, sweepUnreferenced) {
2622
+ const binDataMap = /* @__PURE__ */ new Map();
2623
+ const binRe = /^BIN([0-9A-Fa-f]{4,8})(?:\.|$)/;
2624
+ for (const e of lcfb.entries()) {
2625
+ const match = e.name.match(binRe);
2626
+ if (!match) continue;
2627
+ const idx = parseInt(match[1], 16);
2628
+ const raw = lcfb.findStream(e.name);
2629
+ if (!raw) continue;
2630
+ binDataMap.set(idx, { data: normalizeBinPayload(raw), name: e.name });
2616
2631
  }
2617
- return inflateRawSync2(data, opts);
2632
+ if (binDataMap.size === 0) {
2633
+ resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
2634
+ return [];
2635
+ }
2636
+ return resolveImageBlocks(binDataMap, blocks, warnings, sweepUnreferenced);
2618
2637
  }
2619
- function parseFileHeader(data) {
2620
- if (data.length < 40) throw new KordocError("FileHeader\uAC00 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (\uCD5C\uC18C 40\uBC14\uC774\uD2B8)");
2621
- const sig = data.subarray(0, 32).toString("utf8").replace(/\0+$/, "");
2622
- return {
2623
- signature: sig,
2624
- versionMajor: data[35],
2625
- flags: data.readUInt32LE(36)
2626
- };
2638
+
2639
+ // src/hwpx/images.ts
2640
+ function imageExtToMime(ext) {
2641
+ switch (ext.toLowerCase()) {
2642
+ case "jpg":
2643
+ case "jpeg":
2644
+ return "image/jpeg";
2645
+ case "png":
2646
+ return "image/png";
2647
+ case "gif":
2648
+ return "image/gif";
2649
+ case "bmp":
2650
+ return "image/bmp";
2651
+ case "tif":
2652
+ case "tiff":
2653
+ return "image/tiff";
2654
+ case "wmf":
2655
+ return "image/wmf";
2656
+ case "emf":
2657
+ return "image/emf";
2658
+ case "svg":
2659
+ return "image/svg+xml";
2660
+ default:
2661
+ return "application/octet-stream";
2662
+ }
2627
2663
  }
2628
- function readHwpString(data, offset) {
2629
- if (offset + 2 > data.length) return { value: "", next: data.length };
2630
- const len = data.readUInt16LE(offset);
2631
- const start = offset + 2;
2632
- const end = start + len * 2;
2633
- if (len === 0 || end > data.length) return { value: "", next: start };
2634
- return { value: data.subarray(start, end).toString("utf16le"), next: end };
2664
+ function mimeToExt(mime) {
2665
+ if (mime.includes("jpeg")) return "jpg";
2666
+ if (mime.includes("png")) return "png";
2667
+ if (mime.includes("gif")) return "gif";
2668
+ if (mime.includes("bmp")) return "bmp";
2669
+ if (mime.includes("tiff")) return "tif";
2670
+ if (mime.includes("wmf")) return "wmf";
2671
+ if (mime.includes("emf")) return "emf";
2672
+ if (mime.includes("svg")) return "svg";
2673
+ return "bin";
2635
2674
  }
2636
- function parseDocInfo(records) {
2637
- const charShapes = [];
2638
- const paraShapes = [];
2639
- const styles = [];
2640
- const binData = [];
2641
- const numberings = [];
2642
- const bullets = [];
2643
- for (const rec of records) {
2644
- if (rec.tagId === TAG_DOC_PARA_SHAPE && rec.data.length >= 4) {
2645
- const attr1 = rec.data.readUInt32LE(0);
2646
- const headType = attr1 >>> 23 & 3;
2647
- const paraLevel = attr1 >>> 25 & 7;
2648
- const numberingId = rec.data.length >= 32 ? rec.data.readUInt16LE(30) : 0;
2649
- paraShapes.push({ headType, paraLevel, numberingId });
2650
- }
2651
- if (rec.tagId === TAG_BIN_DATA && rec.data.length >= 2) {
2652
- const attr = rec.data.readUInt16LE(0);
2653
- const typeBits = attr & 15;
2654
- if (typeBits === 0) {
2655
- binData.push({ kind: "link", storageId: 0, extension: "" });
2656
- } else {
2657
- const storageId = rec.data.length >= 4 ? rec.data.readUInt16LE(2) : 0;
2658
- const { value: extension } = readHwpString(rec.data, 4);
2659
- binData.push({ kind: typeBits === 2 ? "storage" : "embed", storageId, extension });
2675
+ function collectImageBlocks2(blocks, out, ownerCell, depth = 0) {
2676
+ if (depth > MAX_XML_DEPTH) return;
2677
+ for (const block of blocks) {
2678
+ if (block.type === "image") {
2679
+ out.push({ block, ownerCell });
2680
+ } else if (block.type === "table" && block.table) {
2681
+ for (const row of block.table.cells) {
2682
+ for (const cell2 of row) {
2683
+ if (cell2.blocks?.length) collectImageBlocks2(cell2.blocks, out, cell2, depth + 1);
2684
+ }
2660
2685
  }
2661
2686
  }
2662
- if (rec.tagId === TAG_NUMBERING && rec.data.length >= 14) {
2663
- const levelFormats = [];
2664
- const numberFormats = [];
2665
- const startNumbers = [1, 1, 1, 1, 1, 1, 1];
2666
- let offset = 0;
2667
- for (let level = 0; level < 7; level++) {
2668
- if (offset + 12 > rec.data.length) {
2669
- levelFormats.push("");
2670
- numberFormats.push(0);
2671
- continue;
2687
+ }
2688
+ }
2689
+ async function extractImagesFromZip(zip, blocks, decompressed, warnings, sweepUnreferenced) {
2690
+ const images = [];
2691
+ let imageIndex = 0;
2692
+ const usedPaths = /* @__PURE__ */ new Set();
2693
+ const imageBlocks = [];
2694
+ collectImageBlocks2(blocks, imageBlocks);
2695
+ const resolved = /* @__PURE__ */ new Map();
2696
+ for (const { block, ownerCell } of imageBlocks) {
2697
+ if (block.type !== "image" || !block.text) continue;
2698
+ const ref = block.text;
2699
+ let img = resolved.get(ref);
2700
+ if (img === void 0) {
2701
+ img = null;
2702
+ const candidates = [
2703
+ `BinData/${ref}`,
2704
+ `Contents/BinData/${ref}`,
2705
+ ref
2706
+ // 절대 경로일 수도 있음
2707
+ ];
2708
+ let resolvedPath = null;
2709
+ if (!ref.includes(".")) {
2710
+ const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
2711
+ for (const prefix of prefixes) {
2712
+ const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
2713
+ if (match.length > 0) {
2714
+ resolvedPath = match[0].name;
2715
+ break;
2716
+ }
2672
2717
  }
2673
- const attr = rec.data.readUInt32LE(offset);
2674
- numberFormats.push(attr >>> 5 & 15);
2675
- offset += 12;
2676
- const { value, next } = readHwpString(rec.data, offset);
2677
- levelFormats.push(value);
2678
- offset = next;
2679
2718
  }
2680
- let baseStart = 1;
2681
- if (offset + 2 <= rec.data.length) {
2682
- baseStart = rec.data.readUInt16LE(offset) || 1;
2683
- offset += 2;
2719
+ const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
2720
+ for (const path of allCandidates) {
2721
+ if (isPathTraversal(path)) continue;
2722
+ const file = zip.file(path);
2723
+ if (!file) continue;
2724
+ try {
2725
+ const data = await file.async("uint8array");
2726
+ decompressed.total += data.length;
2727
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2728
+ const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
2729
+ const mimeType = imageExtToMime(ext);
2730
+ imageIndex++;
2731
+ const filename2 = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
2732
+ img = { filename: filename2, data, mimeType };
2733
+ images.push(img);
2734
+ usedPaths.add(path);
2735
+ break;
2736
+ } catch (err) {
2737
+ if (err instanceof KordocError) throw err;
2738
+ }
2684
2739
  }
2685
- for (let level = 0; level < 7; level++) {
2686
- if (offset + 4 <= rec.data.length) {
2687
- startNumbers[level] = rec.data.readUInt32LE(offset) || 1;
2688
- offset += 4;
2689
- } else {
2690
- startNumbers[level] = baseStart;
2740
+ if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
2741
+ resolved.set(ref, img);
2742
+ }
2743
+ if (!img) {
2744
+ block.type = "paragraph";
2745
+ block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
2746
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, () => `[\uC774\uBBF8\uC9C0: ${ref}]`);
2747
+ continue;
2748
+ }
2749
+ const filename = img.filename;
2750
+ block.text = filename;
2751
+ block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
2752
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, () => `![image](${filename})`);
2753
+ }
2754
+ if (sweepUnreferenced) {
2755
+ const binEntries = zip.file(/(?:^|\/)BinData\//i);
2756
+ for (const file of binEntries) {
2757
+ if (file.dir || usedPaths.has(file.name) || isPathTraversal(file.name)) continue;
2758
+ try {
2759
+ const data = await file.async("uint8array");
2760
+ decompressed.total += data.length;
2761
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2762
+ const ext = file.name.includes(".") ? file.name.split(".").pop() || "" : "";
2763
+ let mimeType = imageExtToMime(ext);
2764
+ if (mimeType === "application/octet-stream") {
2765
+ const sniffed = detectImageMime(data);
2766
+ if (!sniffed) continue;
2767
+ mimeType = sniffed;
2691
2768
  }
2769
+ imageIndex++;
2770
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
2771
+ const img = { filename, data, mimeType };
2772
+ images.push(img);
2773
+ blocks.push({ type: "image", text: filename, imageData: { data, mimeType, filename: file.name } });
2774
+ } catch (err) {
2775
+ if (err instanceof KordocError) throw err;
2776
+ warnings?.push({ message: `\uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC2E4\uD328: ${file.name}`, code: "SKIPPED_IMAGE" });
2692
2777
  }
2693
- numberings.push({ levelFormats, numberFormats, startNumbers });
2694
2778
  }
2695
- if (rec.tagId === TAG_BULLET && rec.data.length >= 14) {
2696
- const code = rec.data.readUInt16LE(12);
2697
- bullets.push({ char: code > 0 ? String.fromCharCode(code) : "\u2022" });
2779
+ }
2780
+ return images;
2781
+ }
2782
+
2783
+ // src/hwpx/metadata.ts
2784
+ import JSZip from "jszip";
2785
+
2786
+ // src/hwpx/zip-sections.ts
2787
+ import { inflateRawSync as inflateRawSync2 } from "zlib";
2788
+ function extractFromBrokenZip(buffer) {
2789
+ const data = new Uint8Array(buffer);
2790
+ const view = new DataView(buffer);
2791
+ let pos = 0;
2792
+ const blocks = [];
2793
+ const warnings = [
2794
+ { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
2795
+ ];
2796
+ let totalDecompressed = 0;
2797
+ let entryCount = 0;
2798
+ let sectionNum = 0;
2799
+ const shared = createSectionShared();
2800
+ while (pos < data.length - 30) {
2801
+ if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
2802
+ pos++;
2803
+ while (pos < data.length - 30) {
2804
+ if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
2805
+ pos++;
2806
+ }
2807
+ continue;
2698
2808
  }
2699
- if (rec.tagId === TAG_DOC_CHAR_SHAPE && rec.data.length >= 18) {
2700
- if (rec.data.length >= 50) {
2701
- const fontSize = rec.data.readUInt32LE(42);
2702
- const attrFlags = rec.data.readUInt32LE(46);
2703
- charShapes.push({ fontSize, attrFlags });
2809
+ if (++entryCount > MAX_ZIP_ENTRIES) break;
2810
+ const method = view.getUint16(pos + 8, true);
2811
+ const compSize = view.getUint32(pos + 18, true);
2812
+ const nameLen = view.getUint16(pos + 26, true);
2813
+ const extraLen = view.getUint16(pos + 28, true);
2814
+ if (nameLen > 1024 || extraLen > 65535) {
2815
+ pos += 30 + nameLen + extraLen;
2816
+ continue;
2817
+ }
2818
+ const fileStart = pos + 30 + nameLen + extraLen;
2819
+ if (fileStart + compSize > data.length) break;
2820
+ if (compSize === 0 && method !== 0) {
2821
+ pos = fileStart;
2822
+ continue;
2823
+ }
2824
+ const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
2825
+ const name = new TextDecoder().decode(nameBytes);
2826
+ if (isPathTraversal(name)) {
2827
+ pos = fileStart + compSize;
2828
+ continue;
2829
+ }
2830
+ const fileData = data.slice(fileStart, fileStart + compSize);
2831
+ pos = fileStart + compSize;
2832
+ if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
2833
+ try {
2834
+ let content;
2835
+ if (method === 0) {
2836
+ content = new TextDecoder().decode(fileData);
2837
+ } else if (method === 8) {
2838
+ const decompressed = inflateRawSync2(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
2839
+ content = new TextDecoder().decode(decompressed);
2704
2840
  } else {
2705
- charShapes.push({ fontSize: 0, attrFlags: 0 });
2841
+ continue;
2706
2842
  }
2843
+ totalDecompressed += content.length * 2;
2844
+ if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
2845
+ sectionNum++;
2846
+ blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
2847
+ } catch {
2848
+ continue;
2707
2849
  }
2708
- if (rec.tagId === TAG_DOC_STYLE && rec.data.length >= 8) {
2709
- try {
2710
- let offset = 0;
2711
- const nameLen = rec.data.readUInt16LE(offset);
2712
- offset += 2;
2713
- const nameBytes = nameLen * 2;
2714
- const name = nameBytes > 0 && offset + nameBytes <= rec.data.length ? rec.data.subarray(offset, offset + nameBytes).toString("utf16le") : "";
2715
- offset += nameBytes;
2716
- let nameKo = "";
2717
- if (offset + 2 <= rec.data.length) {
2718
- const nameKoLen = rec.data.readUInt16LE(offset);
2719
- offset += 2;
2720
- const nameKoBytes = nameKoLen * 2;
2721
- if (nameKoBytes > 0 && offset + nameKoBytes <= rec.data.length) {
2722
- nameKo = rec.data.subarray(offset, offset + nameKoBytes).toString("utf16le");
2723
- }
2724
- offset += nameKoBytes;
2725
- }
2726
- const type = offset < rec.data.length ? rec.data.readUInt8(offset) : 0;
2727
- offset += 1;
2728
- offset += 1;
2729
- offset += 2;
2730
- const paraShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2731
- offset += 2;
2732
- const charShapeId = offset + 2 <= rec.data.length ? rec.data.readUInt16LE(offset) : 0;
2733
- styles.push({ name, nameKo, charShapeId, paraShapeId, type });
2734
- } catch {
2850
+ }
2851
+ 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");
2852
+ applyPageText(blocks, shared);
2853
+ const markdown = blocksToMarkdown(blocks);
2854
+ return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
2855
+ }
2856
+ async function readKordocLayout(zip) {
2857
+ const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
2858
+ if (!file) return null;
2859
+ try {
2860
+ const xml = await file.async("text");
2861
+ if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
2862
+ return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
2863
+ } catch {
2864
+ return null;
2865
+ }
2866
+ }
2867
+ async function resolveSectionPaths(zip) {
2868
+ const manifestPaths = ["Contents/content.hpf", "content.hpf"];
2869
+ for (const mp of manifestPaths) {
2870
+ const mpLower = mp.toLowerCase();
2871
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
2872
+ if (!file) continue;
2873
+ const xml = await file.async("text");
2874
+ const paths = parseSectionPathsFromManifest(xml);
2875
+ if (paths.length > 0) return paths;
2876
+ }
2877
+ const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
2878
+ return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
2879
+ }
2880
+ function parseSectionPathsFromManifest(xml) {
2881
+ const parser = createXmlParser();
2882
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2883
+ const items = doc.getElementsByTagName("opf:item");
2884
+ const spine = doc.getElementsByTagName("opf:itemref");
2885
+ const idToHref = /* @__PURE__ */ new Map();
2886
+ for (let i = 0; i < items.length; i++) {
2887
+ const item = items[i];
2888
+ const id = item.getAttribute("id") || "";
2889
+ const href = normalizeSectionHref(item.getAttribute("href") || "");
2890
+ if (id && href) idToHref.set(id, href);
2891
+ }
2892
+ if (spine.length > 0) {
2893
+ const ordered = [];
2894
+ for (let i = 0; i < spine.length; i++) {
2895
+ const href = idToHref.get(spine[i].getAttribute("idref") || "");
2896
+ if (href) ordered.push(href);
2897
+ }
2898
+ if (ordered.length > 0) return ordered;
2899
+ }
2900
+ return Array.from(idToHref.values()).sort(compareSectionPaths);
2901
+ }
2902
+
2903
+ // src/hwpx/metadata.ts
2904
+ async function extractHwpxMetadata(zip, metadata, decompressed) {
2905
+ try {
2906
+ const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
2907
+ for (const mp of metaPaths) {
2908
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
2909
+ if (!file) continue;
2910
+ const xml = await file.async("text");
2911
+ if (decompressed) {
2912
+ decompressed.total += xml.length * 2;
2913
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2735
2914
  }
2915
+ parseDublinCoreMetadata(xml, metadata);
2916
+ if (metadata.title || metadata.author) return;
2736
2917
  }
2918
+ } catch {
2737
2919
  }
2738
- return { charShapes, paraShapes, styles, binData, numberings, bullets };
2739
2920
  }
2740
- function createParaTextState() {
2741
- return { text: "", ctrlIdx: 0, fieldStack: [], fieldRanges: [] };
2921
+ function parseDublinCoreMetadata(xml, metadata) {
2922
+ const parser = createXmlParser();
2923
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
2924
+ if (!doc.documentElement) return;
2925
+ const getText = (tagNames) => {
2926
+ for (const tag of tagNames) {
2927
+ const els = doc.getElementsByTagName(tag);
2928
+ if (els.length > 0) {
2929
+ const text = els[0].textContent?.trim();
2930
+ if (text) return text;
2931
+ }
2932
+ }
2933
+ return void 0;
2934
+ };
2935
+ metadata.title = metadata.title || getText(["dc:title", "title"]);
2936
+ metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
2937
+ metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
2938
+ metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
2939
+ metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
2940
+ const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
2941
+ if (keywords && !metadata.keywords) {
2942
+ metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
2943
+ }
2742
2944
  }
2743
- function isExtendedOnlyCtrlChar(ch) {
2744
- return ch >= 1 && ch <= 3 || ch >= 11 && ch <= 12 || ch >= 14 && ch <= 18 || ch >= 21 && ch <= 23;
2945
+ async function extractHwpxMetadataOnly(buffer) {
2946
+ let zip;
2947
+ try {
2948
+ zip = await JSZip.loadAsync(buffer);
2949
+ } catch {
2950
+ throw new KordocError("HWPX ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
2951
+ }
2952
+ const metadata = {};
2953
+ await extractHwpxMetadata(zip, metadata);
2954
+ const sectionPaths = await resolveSectionPaths(zip);
2955
+ metadata.pageCount = sectionPaths.length;
2956
+ return metadata;
2745
2957
  }
2746
- function appendParaText(state, data, resolveControl) {
2747
- let result = "";
2748
- let i = 0;
2749
- const base = state.text.length;
2750
- const resolveAt = (byteOffset, extended) => {
2751
- const ctrlId = data.readUInt32LE(byteOffset);
2752
- const idx = extended ? state.ctrlIdx : -1;
2753
- const replacement = resolveControl?.(idx, ctrlId);
2754
- if (replacement) result += replacement;
2755
- if (extended) state.ctrlIdx++;
2756
- };
2757
- while (i + 1 < data.length) {
2758
- const ch = data.readUInt16LE(i);
2759
- i += 2;
2760
- switch (ch) {
2761
- // ── char 타입 (2바이트만, 확장 데이터 없음) ──
2762
- case CHAR_LINE:
2763
- result += "\n";
2764
- break;
2765
- case CHAR_SECTION_BREAK: {
2766
- if (i + 16 <= data.length && data.readUInt16LE(i) === 11) {
2767
- resolveAt(i + 2, true);
2768
- i += 16;
2769
- break;
2770
- }
2771
- result += "\n";
2772
- break;
2773
- }
2774
- case CHAR_PARA:
2775
- break;
2776
- // 문단 끝
2777
- case CHAR_HYPHEN:
2778
- result += "-";
2779
- break;
2780
- case CHAR_NBSP:
2781
- result += " ";
2782
- break;
2783
- case CHAR_FIXED_NBSP:
2784
- result += "\xA0";
2785
- break;
2786
- // 진짜 NBSP
2787
- case CHAR_FIXED_WIDTH:
2788
- result += " ";
2789
- break;
2790
- // 고정폭 공백
2791
- // ── inline 타입 (2바이트 + 14바이트 확장) ──
2792
- case CHAR_TAB:
2793
- result += " ";
2794
- if (i + 14 <= data.length) i += 14;
2795
- break;
2796
- default:
2797
- if (ch >= 1 && ch <= 31) {
2798
- const isExtended = isExtendedOnlyCtrlChar(ch);
2799
- const isInline = ch >= 4 && ch <= 9 || ch >= 19 && ch <= 20;
2800
- if ((isExtended || isInline) && i + 14 <= data.length) {
2801
- if (ch === 3) {
2802
- state.fieldStack.push({ start: base + result.length, ctrlIdx: state.ctrlIdx });
2803
- } else if (ch === 4) {
2804
- const open = state.fieldStack.pop();
2805
- if (open) {
2806
- state.fieldRanges.push({ start: open.start, end: base + result.length, ctrlIdx: open.ctrlIdx });
2807
- }
2808
- }
2809
- resolveAt(i, isExtended);
2810
- i += 14;
2811
- }
2812
- } else if (ch >= 32) {
2813
- if (ch >= 55296 && ch <= 56319 && i + 1 < data.length) {
2814
- const lo = data.readUInt16LE(i);
2815
- if (lo >= 56320 && lo <= 57343) {
2816
- i += 2;
2817
- const codePoint = (ch - 55296 << 10) + (lo - 56320) + 65536;
2818
- result += String.fromCodePoint(codePoint);
2819
- break;
2820
- }
2821
- }
2822
- result += String.fromCharCode(ch);
2958
+
2959
+ // src/hwpx/parser.ts
2960
+ async function parseHwpxDocument(buffer, options) {
2961
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
2962
+ let zip;
2963
+ try {
2964
+ zip = await JSZip2.loadAsync(buffer);
2965
+ } catch {
2966
+ return extractFromBrokenZip(buffer);
2967
+ }
2968
+ const actualEntryCount = Object.keys(zip.files).length;
2969
+ if (actualEntryCount > MAX_ZIP_ENTRIES) {
2970
+ throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
2971
+ }
2972
+ const manifestFile = zip.file("META-INF/manifest.xml");
2973
+ if (manifestFile) {
2974
+ const manifestXml = await manifestFile.async("text");
2975
+ if (isEncryptedHwpx(manifestXml)) {
2976
+ if (isComFallbackAvailable() && options?.filePath) {
2977
+ const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
2978
+ if (pages.some((p) => p && p.trim().length > 0)) {
2979
+ return comResultToParseResult(pages, pageCount, warnings2);
2823
2980
  }
2824
- break;
2981
+ }
2982
+ 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.");
2825
2983
  }
2826
2984
  }
2827
- state.text += result;
2828
- }
2829
- function extractEquationText(data) {
2830
- if (data.length < 6) return null;
2831
- const scriptLength = data.readUInt16LE(4);
2832
- const scriptStart = 6;
2833
- const scriptEnd = scriptStart + scriptLength * 2;
2834
- if (scriptLength <= 0 || scriptEnd > data.length) return null;
2835
- const equation = data.subarray(scriptStart, scriptEnd).toString("utf16le").replace(/\0+/g, "").trim();
2836
- return equation || null;
2985
+ const decompressed = { total: 0 };
2986
+ const metadata = {};
2987
+ await extractHwpxMetadata(zip, metadata, decompressed);
2988
+ const styleMap = await extractHwpxStyles(zip, decompressed);
2989
+ const warnings = [];
2990
+ const sectionPaths = await resolveSectionPaths(zip);
2991
+ if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
2992
+ metadata.pageCount = sectionPaths.length;
2993
+ const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
2994
+ const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
2995
+ const blocks = [];
2996
+ const shared = createSectionShared();
2997
+ shared.kordocLayout = await readKordocLayout(zip);
2998
+ shared.keepTrailingEmptyCols = options?.keepTrailingEmptyCols;
2999
+ let parsedSections = 0;
3000
+ for (let si = 0; si < sectionPaths.length; si++) {
3001
+ if (pageFilter && !pageFilter.has(si + 1)) continue;
3002
+ const file = zip.file(sectionPaths[si]);
3003
+ if (!file) continue;
3004
+ try {
3005
+ const xml = await file.async("text");
3006
+ decompressed.total += xml.length * 2;
3007
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new ZipBombError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
3008
+ blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
3009
+ parsedSections++;
3010
+ options?.onProgress?.(parsedSections, totalTarget);
3011
+ } catch (secErr) {
3012
+ if (secErr instanceof ZipBombError) throw secErr;
3013
+ 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" });
3014
+ }
3015
+ }
3016
+ applyPageText(blocks, shared);
3017
+ const images = await extractImagesFromZip(zip, blocks, decompressed, warnings, !pageFilter);
3018
+ detectHwpxHeadings(blocks, styleMap);
3019
+ const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
3020
+ const markdown = blocksToMarkdown(blocks);
3021
+ 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 };
2837
3022
  }
2838
3023
 
2839
3024
  // src/hwp5/numbering.ts
@@ -2911,383 +3096,107 @@ function shapeFormatToNumFmt(code) {
2911
3096
  return "digit";
2912
3097
  }
2913
3098
  }
2914
- var CIRCLED_DIGITS = "\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468\u2469\u246A\u246B\u246C\u246D\u246E\u246F\u2470\u2471\u2472\u2473";
2915
- var GANADA = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
2916
- var CIRCLED_GANADA = "\u326E\u326F\u3270\u3271\u3272\u3273\u3274\u3275\u3276\u3277\u3278\u3279\u327A\u327B";
2917
- var JAMO = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
2918
- var CIRCLED_JAMO = "\u3260\u3261\u3262\u3263\u3264\u3265\u3266\u3267\u3268\u3269\u326A\u326B\u326C\u326D";
2919
- function fromTable(n, table2) {
2920
- return n >= 1 && n <= table2.length ? table2[n - 1] : String(n);
2921
- }
2922
- function formatRoman(n, upper) {
2923
- if (n <= 0 || n > 3999) return String(n);
2924
- const values = [1e3, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
2925
- const symbols = upper ? ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"] : ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"];
2926
- let result = "";
2927
- let num2 = n;
2928
- for (let i = 0; i < values.length; i++) {
2929
- while (num2 >= values[i]) {
2930
- result += symbols[i];
2931
- num2 -= values[i];
2932
- }
2933
- }
2934
- return result;
2935
- }
2936
- function formatLatin(n, upper) {
2937
- if (n <= 0) return "";
2938
- let result = "";
2939
- let num2 = n;
2940
- while (num2 > 0) {
2941
- num2--;
2942
- result = String.fromCharCode((upper ? 65 : 97) + num2 % 26) + result;
2943
- num2 = Math.floor(num2 / 26);
2944
- }
2945
- return result;
2946
- }
2947
- function formatEastAsianNumber(n, digits, units, zero) {
2948
- if (n === 0) return zero;
2949
- if (n < 0 || n > 99999) return String(n);
2950
- let result = "";
2951
- let num2 = n;
2952
- let unit = 0;
2953
- while (num2 > 0) {
2954
- const d = num2 % 10;
2955
- if (d > 0) {
2956
- const digitStr = d === 1 && unit > 0 ? "" : digits[d];
2957
- result = digitStr + units[unit] + result;
2958
- }
2959
- num2 = Math.floor(num2 / 10);
2960
- unit++;
2961
- }
2962
- return result;
2963
- }
2964
- var HANGUL_DIGITS = ["", "\uC77C", "\uC774", "\uC0BC", "\uC0AC", "\uC624", "\uC721", "\uCE60", "\uD314", "\uAD6C"];
2965
- var HANGUL_UNITS = ["", "\uC2ED", "\uBC31", "\uCC9C", "\uB9CC"];
2966
- var HANJA_DIGITS = ["", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u4E03", "\u516B", "\u4E5D"];
2967
- var HANJA_UNITS = ["", "\u5341", "\u767E", "\u5343", "\u842C"];
2968
- function formatNumber(n, fmt) {
2969
- switch (fmt) {
2970
- case "circled":
2971
- return fromTable(n, CIRCLED_DIGITS);
2972
- case "romanUpper":
2973
- return formatRoman(n, true);
2974
- case "romanLower":
2975
- return formatRoman(n, false);
2976
- case "latinUpper":
2977
- return formatLatin(n, true) || String(n);
2978
- case "latinLower":
2979
- return formatLatin(n, false) || String(n);
2980
- case "ganada":
2981
- return fromTable(n, GANADA);
2982
- case "circledGanada":
2983
- return fromTable(n, CIRCLED_GANADA);
2984
- case "jamo":
2985
- return fromTable(n, JAMO);
2986
- case "circledJamo":
2987
- return fromTable(n, CIRCLED_JAMO);
2988
- case "hangulNum":
2989
- return formatEastAsianNumber(n, HANGUL_DIGITS, HANGUL_UNITS, "\uC601");
2990
- case "hanjaNum":
2991
- return formatEastAsianNumber(n, HANJA_DIGITS, HANJA_UNITS, "\u96F6");
2992
- default:
2993
- return String(n);
2994
- }
2995
- }
2996
- function expandNumberingFormat(formatStr, counters, numbering) {
2997
- let result = "";
2998
- let i = 0;
2999
- while (i < formatStr.length) {
3000
- const ch = formatStr[i];
3001
- if (ch === "^" && i + 1 < formatStr.length && formatStr[i + 1] >= "1" && formatStr[i + 1] <= "7") {
3002
- const levelRef = formatStr.charCodeAt(i + 1) - 48;
3003
- const idx = levelRef - 1;
3004
- const counterVal = counters[idx] ?? 0;
3005
- const start = numbering.startNumbers[idx] ?? 1;
3006
- const num2 = counterVal > 0 ? start - 1 + counterVal : start;
3007
- result += formatNumber(num2, headFormatToNumFmt(numbering.numberFormats[idx] ?? 0));
3008
- i += 2;
3009
- continue;
3010
- }
3011
- result += ch;
3012
- i++;
3013
- }
3014
- return result;
3015
- }
3016
-
3017
- // src/hwp5/images.ts
3018
- function detectImageMime(data) {
3019
- if (data.length < 4) return null;
3020
- if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
3021
- if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
3022
- if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
3023
- if (data[0] === 66 && data[1] === 77) return "image/bmp";
3024
- if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
3025
- if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
3026
- return null;
3027
- }
3028
- function normalizeBinPayload(data) {
3029
- if (detectImageMime(data)) return data;
3030
- try {
3031
- const inflated = decompressStream(data);
3032
- if (inflated.length > 0) return inflated;
3033
- } catch {
3034
- }
3035
- return data;
3036
- }
3037
- var BIN_ENTRY_RE = /(?:^|\/)BIN([0-9A-Fa-f]{4,8})(?:\.[^./\\]*)?$/;
3038
- function collectImageBlocks2(blocks, out) {
3039
- for (const b of blocks) {
3040
- if (b.type === "image") out.push(b);
3041
- if (b.table) {
3042
- for (const row of b.table.cells) {
3043
- for (const cell2 of row) {
3044
- if (cell2.blocks) collectImageBlocks2(cell2.blocks, out);
3045
- }
3046
- }
3047
- }
3048
- if (b.children) collectImageBlocks2(b.children, out);
3049
- }
3050
- }
3051
- function forEachTableCell(blocks, fn) {
3052
- for (const b of blocks) {
3053
- if (b.table) {
3054
- for (const row of b.table.cells) {
3055
- for (const cell2 of row) {
3056
- fn(cell2);
3057
- if (cell2.blocks) forEachTableCell(cell2.blocks, fn);
3058
- }
3059
- }
3060
- }
3061
- if (b.children) forEachTableCell(b.children, fn);
3062
- }
3063
- }
3064
- var CELL_IMAGE_SENTINEL_RE = /!\[image\]\(hwp5bin:(\d+)\)/g;
3065
- function resolveCellImageSentinels(blocks, renamed) {
3066
- forEachTableCell(blocks, (cell2) => {
3067
- if (!cell2.text.includes("hwp5bin:")) return;
3068
- cell2.text = cell2.text.replace(CELL_IMAGE_SENTINEL_RE, (_m, idStr) => {
3069
- const filename = renamed.get(Number(idStr));
3070
- return filename ? `![image](${filename})` : "[\uC774\uBBF8\uC9C0]";
3071
- });
3072
- });
3073
- }
3074
- function resolveImageBlocks(binDataMap, blocks, warnings) {
3075
- const imageBlocks = [];
3076
- collectImageBlocks2(blocks, imageBlocks);
3077
- if (imageBlocks.length === 0) return [];
3078
- const images = [];
3079
- const renamed = /* @__PURE__ */ new Map();
3080
- const resolved = /* @__PURE__ */ new Map();
3081
- let imageIndex = 0;
3082
- for (const block of imageBlocks) {
3083
- if (!block.text) continue;
3084
- const storageId = parseInt(block.text, 10);
3085
- if (isNaN(storageId)) continue;
3086
- let img = resolved.get(storageId);
3087
- if (img === void 0) {
3088
- const bin = binDataMap.get(storageId);
3089
- if (!bin) {
3090
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
3091
- resolved.set(storageId, null);
3092
- } else {
3093
- const mime = detectImageMime(bin.data);
3094
- if (!mime) {
3095
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
3096
- resolved.set(storageId, null);
3097
- } else {
3098
- imageIndex++;
3099
- const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
3100
- img = { filename: `image_${String(imageIndex).padStart(3, "0")}.${ext}`, data: new Uint8Array(bin.data), mime };
3101
- resolved.set(storageId, img);
3102
- images.push({ filename: img.filename, data: img.data, mimeType: img.mime });
3103
- renamed.set(storageId, img.filename);
3104
- }
3105
- }
3106
- img = resolved.get(storageId);
3107
- }
3108
- if (!img) {
3109
- const bin = binDataMap.get(storageId);
3110
- block.type = "paragraph";
3111
- block.text = bin ? `[\uC774\uBBF8\uC9C0: ${bin.name}]` : `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
3112
- continue;
3113
- }
3114
- block.text = img.filename;
3115
- block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
3116
- }
3117
- resolveCellImageSentinels(blocks, renamed);
3118
- return images;
3119
- }
3120
- function extractHwp5Images(fileIndex, blocks, warnings) {
3121
- const binDataMap = /* @__PURE__ */ new Map();
3122
- if (fileIndex) {
3123
- for (const entry of fileIndex) {
3124
- if (!entry?.name || !entry.content) continue;
3125
- const match = entry.name.match(BIN_ENTRY_RE);
3126
- if (!match) continue;
3127
- const idx = parseInt(match[1], 16);
3128
- const data = normalizeBinPayload(Buffer.from(entry.content));
3129
- binDataMap.set(idx, { data, name: entry.name });
3099
+ var CIRCLED_DIGITS = "\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468\u2469\u246A\u246B\u246C\u246D\u246E\u246F\u2470\u2471\u2472\u2473";
3100
+ var GANADA = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
3101
+ var CIRCLED_GANADA = "\u326E\u326F\u3270\u3271\u3272\u3273\u3274\u3275\u3276\u3277\u3278\u3279\u327A\u327B";
3102
+ var JAMO = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
3103
+ var CIRCLED_JAMO = "\u3260\u3261\u3262\u3263\u3264\u3265\u3266\u3267\u3268\u3269\u326A\u326B\u326C\u326D";
3104
+ function fromTable(n, table2) {
3105
+ return n >= 1 && n <= table2.length ? table2[n - 1] : String(n);
3106
+ }
3107
+ function formatRoman(n, upper) {
3108
+ if (n <= 0 || n > 3999) return String(n);
3109
+ const values = [1e3, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
3110
+ const symbols = upper ? ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"] : ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"];
3111
+ let result = "";
3112
+ let num2 = n;
3113
+ for (let i = 0; i < values.length; i++) {
3114
+ while (num2 >= values[i]) {
3115
+ result += symbols[i];
3116
+ num2 -= values[i];
3130
3117
  }
3131
3118
  }
3132
- if (binDataMap.size === 0) {
3133
- resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3134
- return [];
3135
- }
3136
- return resolveImageBlocks(binDataMap, blocks, warnings);
3119
+ return result;
3137
3120
  }
3138
- function extractHwp5ImagesLenient(lcfb, blocks, warnings) {
3139
- const binDataMap = /* @__PURE__ */ new Map();
3140
- const binRe = /^BIN([0-9A-Fa-f]{4,8})(?:\.|$)/;
3141
- for (const e of lcfb.entries()) {
3142
- const match = e.name.match(binRe);
3143
- if (!match) continue;
3144
- const idx = parseInt(match[1], 16);
3145
- const raw = lcfb.findStream(e.name);
3146
- if (!raw) continue;
3147
- binDataMap.set(idx, { data: normalizeBinPayload(raw), name: e.name });
3148
- }
3149
- if (binDataMap.size === 0) {
3150
- resolveCellImageSentinels(blocks, /* @__PURE__ */ new Map());
3151
- return [];
3121
+ function formatLatin(n, upper) {
3122
+ if (n <= 0) return "";
3123
+ let result = "";
3124
+ let num2 = n;
3125
+ while (num2 > 0) {
3126
+ num2--;
3127
+ result = String.fromCharCode((upper ? 65 : 97) + num2 % 26) + result;
3128
+ num2 = Math.floor(num2 / 26);
3152
3129
  }
3153
- return resolveImageBlocks(binDataMap, blocks, warnings);
3130
+ return result;
3154
3131
  }
3155
-
3156
- // src/image/transcode.ts
3157
- import { deflateSync } from "zlib";
3158
- var CRC_TABLE = (() => {
3159
- const table2 = new Uint32Array(256);
3160
- for (let n = 0; n < 256; n++) {
3161
- let c = n;
3162
- for (let k = 0; k < 8; k++) {
3163
- c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
3132
+ function formatEastAsianNumber(n, digits, units, zero) {
3133
+ if (n === 0) return zero;
3134
+ if (n < 0 || n > 99999) return String(n);
3135
+ let result = "";
3136
+ let num2 = n;
3137
+ let unit = 0;
3138
+ while (num2 > 0) {
3139
+ const d = num2 % 10;
3140
+ if (d > 0) {
3141
+ const digitStr = d === 1 && unit > 0 ? "" : digits[d];
3142
+ result = digitStr + units[unit] + result;
3164
3143
  }
3165
- table2[n] = c >>> 0;
3144
+ num2 = Math.floor(num2 / 10);
3145
+ unit++;
3166
3146
  }
3167
- return table2;
3168
- })();
3169
- function crc32(bytes) {
3170
- let c = 4294967295;
3171
- for (let i = 0; i < bytes.length; i++) {
3172
- c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
3173
- }
3174
- return (c ^ 4294967295) >>> 0;
3175
- }
3176
- var BI_RGB = 0;
3177
- var MAX_DIM = 32767;
3178
- var MAX_PIXELS = 36e6;
3179
- function bmpToPng(bmp) {
3180
- if (bmp.length < 54) return null;
3181
- if (bmp[0] !== 66 || bmp[1] !== 77) return null;
3182
- const dv = new DataView(bmp.buffer, bmp.byteOffset, bmp.byteLength);
3183
- const dataOffset = dv.getUint32(10, true);
3184
- const headerSize = dv.getUint32(14, true);
3185
- if (headerSize < 40) return null;
3186
- const width = dv.getInt32(18, true);
3187
- const rawHeight = dv.getInt32(22, true);
3188
- const bitCount = dv.getUint16(28, true);
3189
- const compression = dv.getUint32(30, true);
3190
- if (compression !== BI_RGB) return null;
3191
- if (bitCount !== 24 && bitCount !== 32) return null;
3192
- if (width <= 0 || rawHeight === 0) return null;
3193
- if (width > MAX_DIM || Math.abs(rawHeight) > MAX_DIM) return null;
3194
- const topDown = rawHeight < 0;
3195
- const height = Math.abs(rawHeight);
3196
- if (width * height > MAX_PIXELS) return null;
3197
- const bytesPerPixel = bitCount >> 3;
3198
- const rowStride = width * bytesPerPixel + 3 & ~3;
3199
- if (dataOffset + rowStride * height > bmp.length) return null;
3200
- const rgba = new Uint8Array(width * height * 4);
3201
- let anyAlpha = 0;
3202
- for (let y = 0; y < height; y++) {
3203
- const srcRow = topDown ? y : height - 1 - y;
3204
- let src = dataOffset + srcRow * rowStride;
3205
- let dst = y * width * 4;
3206
- for (let x = 0; x < width; x++) {
3207
- rgba[dst] = bmp[src + 2];
3208
- rgba[dst + 1] = bmp[src + 1];
3209
- rgba[dst + 2] = bmp[src];
3210
- const a = bitCount === 32 ? bmp[src + 3] : 255;
3211
- rgba[dst + 3] = a;
3212
- anyAlpha |= a;
3213
- src += bytesPerPixel;
3214
- dst += 4;
3215
- }
3216
- }
3217
- if (bitCount === 32 && anyAlpha === 0) {
3218
- for (let i = 3; i < rgba.length; i += 4) rgba[i] = 255;
3219
- }
3220
- return encodePng(width, height, rgba);
3221
- }
3222
- var PNG_SIGNATURE = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
3223
- function chunk(type, data) {
3224
- const body = new Uint8Array(4 + data.length);
3225
- body[0] = type.charCodeAt(0);
3226
- body[1] = type.charCodeAt(1);
3227
- body[2] = type.charCodeAt(2);
3228
- body[3] = type.charCodeAt(3);
3229
- body.set(data, 4);
3230
- const out = new Uint8Array(8 + data.length + 4);
3231
- const dv = new DataView(out.buffer);
3232
- dv.setUint32(0, data.length, false);
3233
- out.set(body, 4);
3234
- dv.setUint32(8 + data.length, crc32(body), false);
3235
- return out;
3147
+ return result;
3236
3148
  }
3237
- function encodePng(width, height, rgba) {
3238
- const ihdr = new Uint8Array(13);
3239
- const iv = new DataView(ihdr.buffer);
3240
- iv.setUint32(0, width, false);
3241
- iv.setUint32(4, height, false);
3242
- ihdr[8] = 8;
3243
- ihdr[9] = 6;
3244
- const stride = width * 4;
3245
- const raw = new Uint8Array((stride + 1) * height);
3246
- for (let y = 0; y < height; y++) {
3247
- const rowStart = y * (stride + 1);
3248
- raw[rowStart] = 0;
3249
- raw.set(rgba.subarray(y * stride, y * stride + stride), rowStart + 1);
3250
- }
3251
- const idat = deflateSync(raw);
3252
- const ihdrChunk = chunk("IHDR", ihdr);
3253
- const idatChunk = chunk("IDAT", idat);
3254
- const iendChunk = chunk("IEND", new Uint8Array(0));
3255
- const out = new Uint8Array(PNG_SIGNATURE.length + ihdrChunk.length + idatChunk.length + iendChunk.length);
3256
- let o = 0;
3257
- out.set(PNG_SIGNATURE, o);
3258
- o += PNG_SIGNATURE.length;
3259
- out.set(ihdrChunk, o);
3260
- o += ihdrChunk.length;
3261
- out.set(idatChunk, o);
3262
- o += idatChunk.length;
3263
- out.set(iendChunk, o);
3264
- return out;
3149
+ var HANGUL_DIGITS = ["", "\uC77C", "\uC774", "\uC0BC", "\uC0AC", "\uC624", "\uC721", "\uCE60", "\uD314", "\uAD6C"];
3150
+ var HANGUL_UNITS = ["", "\uC2ED", "\uBC31", "\uCC9C", "\uB9CC"];
3151
+ var HANJA_DIGITS = ["", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u4E03", "\u516B", "\u4E5D"];
3152
+ var HANJA_UNITS = ["", "\u5341", "\u767E", "\u5343", "\u842C"];
3153
+ function formatNumber(n, fmt) {
3154
+ switch (fmt) {
3155
+ case "circled":
3156
+ return fromTable(n, CIRCLED_DIGITS);
3157
+ case "romanUpper":
3158
+ return formatRoman(n, true);
3159
+ case "romanLower":
3160
+ return formatRoman(n, false);
3161
+ case "latinUpper":
3162
+ return formatLatin(n, true) || String(n);
3163
+ case "latinLower":
3164
+ return formatLatin(n, false) || String(n);
3165
+ case "ganada":
3166
+ return fromTable(n, GANADA);
3167
+ case "circledGanada":
3168
+ return fromTable(n, CIRCLED_GANADA);
3169
+ case "jamo":
3170
+ return fromTable(n, JAMO);
3171
+ case "circledJamo":
3172
+ return fromTable(n, CIRCLED_JAMO);
3173
+ case "hangulNum":
3174
+ return formatEastAsianNumber(n, HANGUL_DIGITS, HANGUL_UNITS, "\uC601");
3175
+ case "hanjaNum":
3176
+ return formatEastAsianNumber(n, HANJA_DIGITS, HANJA_UNITS, "\u96F6");
3177
+ default:
3178
+ return String(n);
3179
+ }
3265
3180
  }
3266
- function escapeRegExp(s) {
3267
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3268
- }
3269
- function inlineImagesIntoMarkdown(markdown, images, opts) {
3270
- const compress = opts?.compress !== false;
3271
- let out = markdown;
3272
- for (const img of images) {
3273
- let bytes = img.data;
3274
- let mime = img.mimeType;
3275
- if (compress && mime === "image/bmp") {
3276
- const png = bmpToPng(img.data);
3277
- if (png) {
3278
- bytes = png;
3279
- mime = "image/png";
3280
- }
3181
+ function expandNumberingFormat(formatStr, counters, numbering) {
3182
+ let result = "";
3183
+ let i = 0;
3184
+ while (i < formatStr.length) {
3185
+ const ch = formatStr[i];
3186
+ if (ch === "^" && i + 1 < formatStr.length && formatStr[i + 1] >= "1" && formatStr[i + 1] <= "7") {
3187
+ const levelRef = formatStr.charCodeAt(i + 1) - 48;
3188
+ const idx = levelRef - 1;
3189
+ const counterVal = counters[idx] ?? 0;
3190
+ const start = numbering.startNumbers[idx] ?? 1;
3191
+ const num2 = counterVal > 0 ? start - 1 + counterVal : start;
3192
+ result += formatNumber(num2, headFormatToNumFmt(numbering.numberFormats[idx] ?? 0));
3193
+ i += 2;
3194
+ continue;
3281
3195
  }
3282
- const base64 = Buffer.from(bytes).toString("base64");
3283
- const dataUri = `data:${mime};base64,${base64}`;
3284
- const name = escapeRegExp(img.filename);
3285
- const mdRe = new RegExp(`!\\[image\\]\\((?:images/)?${name}\\)`, "g");
3286
- out = out.replace(mdRe, () => `![image](${dataUri})`);
3287
- const imgTagRe = new RegExp(`(<img\\b[^>]*\\bsrc=")(?:images/)?${name}(")`, "g");
3288
- out = out.replace(imgTagRe, (_m, pre, post) => `${pre}${dataUri}${post}`);
3196
+ result += ch;
3197
+ i++;
3289
3198
  }
3290
- return out;
3199
+ return result;
3291
3200
  }
3292
3201
 
3293
3202
  // src/hwp5/aes.ts
@@ -4317,6 +4226,7 @@ function parseHwp5Document(buffer, options) {
4317
4226
  const totalTarget = pageFilter ? pageFilter.size : sections.length;
4318
4227
  const bodyBlocks = [];
4319
4228
  const doc = createHwp5DocState();
4229
+ doc.keepTrailingEmptyCols = options?.keepTrailingEmptyCols;
4320
4230
  let totalDecompressed = 0;
4321
4231
  let parsedSections = 0;
4322
4232
  for (let si = 0; si < sections.length; si++) {
@@ -4337,7 +4247,7 @@ function parseHwp5Document(buffer, options) {
4337
4247
  }
4338
4248
  }
4339
4249
  const blocks = [...doc.headerBlocks, ...bodyBlocks, ...doc.footerBlocks];
4340
- const images = cfb ? extractHwp5Images(cfb.FileIndex, blocks, warnings) : extractHwp5ImagesLenient(lenientCfb, blocks, warnings);
4250
+ const images = cfb ? extractHwp5Images(cfb.FileIndex, blocks, warnings, !pageFilter) : extractHwp5ImagesLenient(lenientCfb, blocks, warnings, !pageFilter);
4341
4251
  let flatBlocks = flattenLayoutTables(blocks);
4342
4252
  if (options?.dedupeRunningHeaders) {
4343
4253
  const deduped = dedupeRunningHeaders(flatBlocks);
@@ -4932,7 +4842,7 @@ function parseTableControl(ctrl, records, ctx) {
4932
4842
  return table3;
4933
4843
  }
4934
4844
  const cellRows = arrangeCells(rows, cols, cells);
4935
- const table2 = buildTable(cellRows);
4845
+ const table2 = buildTable(cellRows, { keepAnchoredEmptyCols: ctx.doc.keepTrailingEmptyCols });
4936
4846
  if (caption && table2.rows > 0) table2.caption = caption;
4937
4847
  return table2.rows > 0 ? table2 : null;
4938
4848
  }
@@ -17126,6 +17036,7 @@ function readHeader(reader) {
17126
17036
  }
17127
17037
 
17128
17038
  // src/hwp3/parser.ts
17039
+ var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
17129
17040
  var PARA_SHAPE_SIZE = 187;
17130
17041
  var LINE_INFO_SIZE = 14;
17131
17042
  var INLINE_CHAR_SHAPE_SIZE = 31;
@@ -17161,8 +17072,11 @@ function parseHwp3Document(buffer, _options) {
17161
17072
  const warnings = [];
17162
17073
  if (header.compressed !== 0) {
17163
17074
  try {
17164
- body = inflateRawSync3(tail);
17075
+ body = inflateRawSync3(tail, { maxOutputLength: MAX_DECOMPRESS_SIZE3 });
17165
17076
  } catch (err) {
17077
+ if (err?.code === "ERR_BUFFER_TOO_LARGE") {
17078
+ 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`);
17079
+ }
17166
17080
  const msg2 = err instanceof Error ? err.message : String(err);
17167
17081
  throw new Error(`HWP3 \uC555\uCD95 \uD574\uC81C \uC2E4\uD328: ${msg2}`);
17168
17082
  }
@@ -17359,7 +17273,7 @@ function isDistributionSentinel(markdown) {
17359
17273
  import JSZip3 from "jszip";
17360
17274
  import { DOMParser } from "@xmldom/xmldom";
17361
17275
  var MAX_SHEETS = 100;
17362
- var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
17276
+ var MAX_DECOMPRESS_SIZE4 = 100 * 1024 * 1024;
17363
17277
  var MAX_ROWS2 = 1e4;
17364
17278
  var MAX_COLS2 = 200;
17365
17279
  function cleanNumericValue(raw) {
@@ -17399,16 +17313,87 @@ function getTextContent(el) {
17399
17313
  function parseXml(text) {
17400
17314
  return new DOMParser().parseFromString(stripDtd(text), "text/xml");
17401
17315
  }
17316
+ function collectRichText(root) {
17317
+ let out = "";
17318
+ const walk = (node) => {
17319
+ const children = node.childNodes;
17320
+ for (let i = 0; i < children.length; i++) {
17321
+ if (children[i].nodeType !== 1) continue;
17322
+ const el = children[i];
17323
+ const local = el.localName || el.tagName?.replace(/^[^:]+:/, "") || "";
17324
+ if (local === "rPh") continue;
17325
+ if (local === "t") out += el.textContent ?? "";
17326
+ else walk(el);
17327
+ }
17328
+ };
17329
+ walk(root);
17330
+ return out;
17331
+ }
17402
17332
  function parseSharedStrings(xml) {
17403
17333
  const doc = parseXml(xml);
17404
17334
  const strings = [];
17405
17335
  const siList = getElements(doc.documentElement, "si");
17406
17336
  for (const si of siList) {
17407
- const tElements = getElements(si, "t");
17408
- strings.push(tElements.map((t) => t.textContent ?? "").join(""));
17337
+ strings.push(collectRichText(si));
17409
17338
  }
17410
17339
  return strings;
17411
17340
  }
17341
+ var BUILTIN_DATE_FMT = /* @__PURE__ */ new Map([
17342
+ [14, "date"],
17343
+ [15, "date"],
17344
+ [16, "date"],
17345
+ [17, "date"],
17346
+ [18, "datetime"],
17347
+ [19, "datetime"],
17348
+ [20, "datetime"],
17349
+ [21, "datetime"],
17350
+ [22, "datetime"],
17351
+ [45, "datetime"],
17352
+ [46, "datetime"],
17353
+ [47, "datetime"]
17354
+ ]);
17355
+ function classifyDateFormat(code) {
17356
+ const stripped = code.replace(/"[^"]*"/g, "").replace(/\[[^\]]*\]/g, "").replace(/\\./g, "");
17357
+ if (!/[ymdh]/i.test(stripped)) return null;
17358
+ return /[hs]/i.test(stripped) ? "datetime" : "date";
17359
+ }
17360
+ function dateKindOfFmt(fmtId, customFormats) {
17361
+ const builtin = BUILTIN_DATE_FMT.get(fmtId);
17362
+ if (builtin) return builtin;
17363
+ const code = customFormats.get(fmtId);
17364
+ return code !== void 0 ? classifyDateFormat(code) : null;
17365
+ }
17366
+ function dateSerialToIso(serial, date1904, kind) {
17367
+ if (!isFinite(serial) || serial < 0) return null;
17368
+ let days = serial;
17369
+ if (date1904) days += 1462;
17370
+ else if (days < 60) days += 1;
17371
+ const ms = Math.round((days - 25569) * 864e5);
17372
+ const d = new Date(ms);
17373
+ if (isNaN(d.getTime()) || d.getUTCFullYear() > 9999) return null;
17374
+ const iso = d.toISOString();
17375
+ return kind === "datetime" ? iso.slice(0, 19) : iso.slice(0, 10);
17376
+ }
17377
+ function parseStyleDateXfs(xml) {
17378
+ const doc = parseXml(xml);
17379
+ const customFormats = /* @__PURE__ */ new Map();
17380
+ for (const el of getElements(doc.documentElement, "numFmt")) {
17381
+ const id = parseInt(el.getAttribute("numFmtId") ?? "", 10);
17382
+ if (isNaN(id)) continue;
17383
+ customFormats.set(id, el.getAttribute("formatCode") ?? "");
17384
+ }
17385
+ const dateXfs = /* @__PURE__ */ new Map();
17386
+ const cellXfsEls = getElements(doc.documentElement, "cellXfs");
17387
+ if (cellXfsEls.length === 0) return dateXfs;
17388
+ const xfs = getElements(cellXfsEls[0], "xf");
17389
+ for (let i = 0; i < xfs.length; i++) {
17390
+ const fmtId = parseInt(xfs[i].getAttribute("numFmtId") ?? "", 10);
17391
+ if (isNaN(fmtId)) continue;
17392
+ const kind = dateKindOfFmt(fmtId, customFormats);
17393
+ if (kind) dateXfs.set(i, kind);
17394
+ }
17395
+ return dateXfs;
17396
+ }
17412
17397
  function parseWorkbook(xml) {
17413
17398
  const doc = parseXml(xml);
17414
17399
  const sheets = [];
@@ -17420,7 +17405,9 @@ function parseWorkbook(xml) {
17420
17405
  rId: el.getAttribute("r:id") ?? ""
17421
17406
  });
17422
17407
  }
17423
- return sheets;
17408
+ const prEls = getElements(doc.documentElement, "workbookPr");
17409
+ const d1904 = prEls.length > 0 ? prEls[0].getAttribute("date1904") : null;
17410
+ return { sheets, date1904: d1904 === "1" || d1904 === "true" };
17424
17411
  }
17425
17412
  function parseRels(xml) {
17426
17413
  const doc = parseXml(xml);
@@ -17433,21 +17420,25 @@ function parseRels(xml) {
17433
17420
  }
17434
17421
  return map;
17435
17422
  }
17436
- function parseWorksheet(xml, sharedStrings) {
17423
+ function parseWorksheet(xml, sharedStrings, dateXfs, date1904) {
17437
17424
  const doc = parseXml(xml);
17438
17425
  const grid = [];
17439
17426
  let maxRow = 0;
17440
17427
  let maxCol = 0;
17441
17428
  const rows = getElements(doc.documentElement, "row");
17429
+ let prevRow = -1;
17442
17430
  for (const rowEl of rows) {
17443
- const rowNum = parseInt(rowEl.getAttribute("r") ?? "0", 10) - 1;
17431
+ const rAttr = rowEl.getAttribute("r");
17432
+ const rowNum = rAttr !== null ? parseInt(rAttr, 10) - 1 : prevRow + 1;
17444
17433
  if (rowNum < 0 || rowNum >= MAX_ROWS2) continue;
17434
+ if (Number.isFinite(rowNum)) prevRow = rowNum;
17445
17435
  const cells = getElements(rowEl, "c");
17436
+ let prevCol = -1;
17446
17437
  for (const cellEl of cells) {
17447
17438
  const ref = cellEl.getAttribute("r");
17448
- if (!ref) continue;
17449
- const pos = parseCellRef(ref);
17450
- if (!pos || pos.col >= MAX_COLS2) continue;
17439
+ const pos = ref !== null ? parseCellRef(ref) : { col: prevCol + 1, row: rowNum };
17440
+ if (!pos || !Number.isFinite(pos.row) || pos.row < 0 || pos.row >= MAX_ROWS2 || pos.col >= MAX_COLS2) continue;
17441
+ prevCol = pos.col;
17451
17442
  const type = cellEl.getAttribute("t");
17452
17443
  const vElements = getElements(cellEl, "v");
17453
17444
  const fElements = getElements(cellEl, "f");
@@ -17461,12 +17452,19 @@ function parseWorksheet(xml, sharedStrings) {
17461
17452
  value = raw === "1" ? "TRUE" : "FALSE";
17462
17453
  } else {
17463
17454
  value = cleanNumericValue(raw);
17455
+ if (type === null || type === "n") {
17456
+ const sAttr = cellEl.getAttribute("s");
17457
+ const kind = sAttr !== null ? dateXfs.get(parseInt(sAttr, 10)) : void 0;
17458
+ if (kind) {
17459
+ const iso = dateSerialToIso(parseFloat(raw), date1904, kind);
17460
+ if (iso) value = iso;
17461
+ }
17462
+ }
17464
17463
  }
17465
17464
  } else if (type === "inlineStr") {
17466
17465
  const isEl = getElements(cellEl, "is");
17467
17466
  if (isEl.length > 0) {
17468
- const tElements = getElements(isEl[0], "t");
17469
- value = tElements.map((t) => t.textContent ?? "").join("");
17467
+ value = collectRichText(isEl[0]);
17470
17468
  }
17471
17469
  }
17472
17470
  if (!value && fElements.length > 0) {
@@ -17485,7 +17483,14 @@ function parseWorksheet(xml, sharedStrings) {
17485
17483
  const ref = el.getAttribute("ref");
17486
17484
  if (!ref) continue;
17487
17485
  const m = parseMergeRef(ref);
17488
- if (m) merges.push(m);
17486
+ if (m) {
17487
+ merges.push({
17488
+ startCol: Math.min(m.startCol, MAX_COLS2 - 1),
17489
+ startRow: Math.min(m.startRow, MAX_ROWS2 - 1),
17490
+ endCol: Math.min(m.endCol, MAX_COLS2 - 1),
17491
+ endRow: Math.min(m.endRow, MAX_ROWS2 - 1)
17492
+ });
17493
+ }
17489
17494
  }
17490
17495
  return { grid, merges, maxRow, maxCol };
17491
17496
  }
@@ -17549,7 +17554,7 @@ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex) {
17549
17554
  return blocks;
17550
17555
  }
17551
17556
  async function parseXlsxDocument(buffer, options) {
17552
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE3);
17557
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
17553
17558
  const zip = await JSZip3.loadAsync(buffer);
17554
17559
  const warnings = [];
17555
17560
  const workbookFile = zip.file("xl/workbook.xml");
@@ -17561,10 +17566,18 @@ async function parseXlsxDocument(buffer, options) {
17561
17566
  if (ssFile) {
17562
17567
  sharedStrings = parseSharedStrings(await ssFile.async("text"));
17563
17568
  }
17564
- const sheets = parseWorkbook(await workbookFile.async("text"));
17569
+ const { sheets, date1904 } = parseWorkbook(await workbookFile.async("text"));
17565
17570
  if (sheets.length === 0) {
17566
17571
  throw new KordocError("XLSX \uD30C\uC77C\uC5D0 \uC2DC\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4");
17567
17572
  }
17573
+ let dateXfs = /* @__PURE__ */ new Map();
17574
+ const stylesFile = zip.file("xl/styles.xml");
17575
+ if (stylesFile) {
17576
+ try {
17577
+ dateXfs = parseStyleDateXfs(await stylesFile.async("text"));
17578
+ } catch {
17579
+ }
17580
+ }
17568
17581
  let relsMap = /* @__PURE__ */ new Map();
17569
17582
  const relsFile = zip.file("xl/_rels/workbook.xml.rels");
17570
17583
  if (relsFile) {
@@ -17572,7 +17585,7 @@ async function parseXlsxDocument(buffer, options) {
17572
17585
  }
17573
17586
  let pageFilter = null;
17574
17587
  if (options?.pages) {
17575
- const { parsePageRange: parsePageRange2 } = await import("./page-range-OF5I4PQY.js");
17588
+ const { parsePageRange: parsePageRange2 } = await import("./page-range-737B4EZW.js");
17576
17589
  pageFilter = parsePageRange2(options.pages, sheets.length);
17577
17590
  }
17578
17591
  const blocks = [];
@@ -17602,7 +17615,7 @@ async function parseXlsxDocument(buffer, options) {
17602
17615
  }
17603
17616
  try {
17604
17617
  const sheetXml = await sheetFile.async("text");
17605
- const { grid, merges, maxRow, maxCol } = parseWorksheet(sheetXml, sharedStrings);
17618
+ const { grid, merges, maxRow, maxCol } = parseWorksheet(sheetXml, sharedStrings, dateXfs, date1904);
17606
17619
  const sheetBlocks = sheetToBlocks(sheet.name, grid, merges, maxRow, maxCol, i);
17607
17620
  blocks.push(...sheetBlocks);
17608
17621
  } catch (err) {
@@ -17646,7 +17659,10 @@ var OP_CONTINUE = 60;
17646
17659
  var OP_BOUNDSHEET8 = 133;
17647
17660
  var OP_SST = 252;
17648
17661
  var OP_CODEPAGE = 66;
17662
+ var OP_DATE1904 = 34;
17649
17663
  var OP_FILEPASS = 47;
17664
+ var OP_FORMAT = 1054;
17665
+ var OP_XF = 224;
17650
17666
  var OP_NUMBER = 515;
17651
17667
  var OP_RK = 638;
17652
17668
  var OP_MULRK = 189;
@@ -17658,6 +17674,8 @@ var OP_BOOLERR = 517;
17658
17674
  var OP_BLANK = 513;
17659
17675
  var OP_MULBLANK = 190;
17660
17676
  var OP_MERGECELLS = 229;
17677
+ var OP_SHRFMLA = 1212;
17678
+ var OP_ARRAY = 545;
17661
17679
  var DT_GLOBALS = 5;
17662
17680
  var DT_WORKSHEET = 16;
17663
17681
  var MAX_RECORDS2 = 1e6;
@@ -17753,7 +17771,7 @@ function decodeUtf16Le(buf) {
17753
17771
  }
17754
17772
 
17755
17773
  // src/xls/sst.ts
17756
- function parseString(buf, offset, segments) {
17774
+ function parseString(buf, offset, segments, segCursor) {
17757
17775
  if (offset + 3 > buf.length) return null;
17758
17776
  const cch = buf.readUInt16LE(offset);
17759
17777
  let flags = buf.readUInt8(offset + 2);
@@ -17776,7 +17794,15 @@ function parseString(buf, offset, segments) {
17776
17794
  const charBytes = [];
17777
17795
  let charsRead = 0;
17778
17796
  while (charsRead < cch) {
17779
- const nextBoundary = segments.find((s) => s > off) ?? buf.length;
17797
+ while (segCursor.idx < segments.length && segments[segCursor.idx] < off) segCursor.idx++;
17798
+ if (segCursor.idx < segments.length && segments[segCursor.idx] === off) {
17799
+ if (off >= buf.length) return null;
17800
+ flags = buf.readUInt8(off);
17801
+ highByte = (flags & 1) !== 0;
17802
+ off += 1;
17803
+ segCursor.idx++;
17804
+ }
17805
+ const nextBoundary = segCursor.idx < segments.length ? segments[segCursor.idx] : buf.length;
17780
17806
  const remainChars = cch - charsRead;
17781
17807
  const bytesPerChar = highByte ? 2 : 1;
17782
17808
  const bytesAvail = nextBoundary - off;
@@ -17787,12 +17813,10 @@ function parseString(buf, offset, segments) {
17787
17813
  charBytes.push(highByte ? slice : padToUtf16(slice));
17788
17814
  off += bytesToRead;
17789
17815
  charsRead += charsInThisRun;
17790
- }
17791
- if (charsRead < cch) {
17792
- if (off >= buf.length) return null;
17793
- flags = buf.readUInt8(off);
17794
- highByte = (flags & 1) !== 0;
17795
- off += 1;
17816
+ } else if (nextBoundary < buf.length) {
17817
+ off = nextBoundary;
17818
+ } else {
17819
+ return null;
17796
17820
  }
17797
17821
  }
17798
17822
  const text = decodeUtf16Le(Buffer.concat(charBytes));
@@ -17817,8 +17841,9 @@ function decodeSST(records) {
17817
17841
  const cstUnique = combined.readUInt32LE(4);
17818
17842
  const strings = [];
17819
17843
  let off = 8;
17844
+ const segCursor = { idx: 0 };
17820
17845
  for (let i = 0; i < cstUnique && off < combined.length; i++) {
17821
- const r = parseString(combined, off, segments);
17846
+ const r = parseString(combined, off, segments, segCursor);
17822
17847
  if (!r) break;
17823
17848
  strings.push(r.text);
17824
17849
  off += r.consumed;
@@ -17893,7 +17918,7 @@ function decodeFormulaStringRecord(data) {
17893
17918
  return decodeUtf16Le(padded);
17894
17919
  }
17895
17920
  }
17896
- function extractSheetCells(records, bofIndex, sst) {
17921
+ function extractSheetCells(records, bofIndex, sst, convertNum) {
17897
17922
  const cells = [];
17898
17923
  const merges = [];
17899
17924
  const bofOffset = records[bofIndex].offset;
@@ -17911,14 +17936,16 @@ function extractSheetCells(records, bofIndex, sst) {
17911
17936
  case OP_NUMBER: {
17912
17937
  const h = readCellHeader(rec.data);
17913
17938
  if (h && rec.data.length >= 14) {
17914
- cells.push({ row: h.row, col: h.col, value: rec.data.readDoubleLE(6) });
17939
+ const n = rec.data.readDoubleLE(6);
17940
+ cells.push({ row: h.row, col: h.col, value: convertNum ? convertNum(n, h.ixfe) : n });
17915
17941
  }
17916
17942
  break;
17917
17943
  }
17918
17944
  case OP_RK: {
17919
17945
  const h = readCellHeader(rec.data);
17920
17946
  if (h && rec.data.length >= 10) {
17921
- cells.push({ row: h.row, col: h.col, value: decodeRk(rec.data.readInt32LE(6)) });
17947
+ const n = decodeRk(rec.data.readInt32LE(6));
17948
+ cells.push({ row: h.row, col: h.col, value: convertNum ? convertNum(n, h.ixfe) : n });
17922
17949
  }
17923
17950
  break;
17924
17951
  }
@@ -17926,7 +17953,7 @@ function extractSheetCells(records, bofIndex, sst) {
17926
17953
  const m = decodeMulRk(rec.data);
17927
17954
  if (m) {
17928
17955
  for (const c of m.cells) {
17929
- cells.push({ row: m.row, col: c.col, value: c.value });
17956
+ cells.push({ row: m.row, col: c.col, value: convertNum ? convertNum(c.value, c.ixfe) : c.value });
17930
17957
  }
17931
17958
  }
17932
17959
  break;
@@ -17951,19 +17978,26 @@ function extractSheetCells(records, bofIndex, sst) {
17951
17978
  if (h && rec.data.length >= 14) {
17952
17979
  const result = decodeFormulaResult(rec.data.subarray(6, 14));
17953
17980
  if (result.kind === "stringRef") {
17954
- const next = records[i + 1];
17981
+ let j = i + 1;
17982
+ while (j < records.length && (records[j].opcode === OP_SHRFMLA || records[j].opcode === OP_ARRAY)) j++;
17983
+ const next = records[j];
17955
17984
  if (next && next.opcode === OP_STRING) {
17956
17985
  cells.push({
17957
17986
  row: h.row,
17958
17987
  col: h.col,
17959
17988
  value: decodeFormulaStringRecord(next.data)
17960
17989
  });
17961
- i++;
17990
+ i = j;
17962
17991
  } else {
17963
17992
  cells.push({ row: h.row, col: h.col, value: "" });
17964
17993
  }
17965
17994
  } else {
17966
- cells.push({ row: h.row, col: h.col, value: result.value });
17995
+ const v = result.value;
17996
+ cells.push({
17997
+ row: h.row,
17998
+ col: h.col,
17999
+ value: convertNum && typeof v === "number" ? convertNum(v, h.ixfe) : v
18000
+ });
17967
18001
  }
17968
18002
  }
17969
18003
  break;
@@ -18013,7 +18047,7 @@ function extractSheetCells(records, bofIndex, sst) {
18013
18047
 
18014
18048
  // src/xls/parser.ts
18015
18049
  var MAX_SHEETS2 = 100;
18016
- var MAX_ROWS3 = 1e5;
18050
+ var MAX_ROWS3 = 65536;
18017
18051
  var MAX_COLS3 = 1e3;
18018
18052
  function decodeBoundSheet(data) {
18019
18053
  if (data.length < 8) return null;
@@ -18036,10 +18070,33 @@ function decodeBoundSheet(data) {
18036
18070
  }
18037
18071
  return { name, lbPlyPos, dt };
18038
18072
  }
18073
+ function decodeFormatRecord(data) {
18074
+ if (data.length < 5) return null;
18075
+ const ifmt = data.readUInt16LE(0);
18076
+ const cch = data.readUInt16LE(2);
18077
+ const flags = data.readUInt8(4);
18078
+ const highByte = (flags & 1) !== 0;
18079
+ const start = 5;
18080
+ let code;
18081
+ if (highByte) {
18082
+ const end = Math.min(start + cch * 2, data.length);
18083
+ code = decodeUtf16Le(data.subarray(start, end));
18084
+ } else {
18085
+ const end = Math.min(start + cch, data.length);
18086
+ const slice = data.subarray(start, end);
18087
+ const padded = Buffer.alloc(slice.length * 2);
18088
+ for (let i = 0; i < slice.length; i++) padded[i * 2] = slice[i];
18089
+ code = decodeUtf16Le(padded);
18090
+ }
18091
+ return { ifmt, code };
18092
+ }
18039
18093
  function processGlobals(records) {
18040
18094
  const sheets = [];
18041
18095
  let codePage = 1200;
18042
18096
  let encrypted = false;
18097
+ let date1904 = false;
18098
+ const customFormats = /* @__PURE__ */ new Map();
18099
+ const xfFmtIds = [];
18043
18100
  const firstBof = records[0];
18044
18101
  if (!firstBof || firstBof.opcode !== OP_BOF) {
18045
18102
  throw new KordocError("XLS: \uCCAB \uB808\uCF54\uB4DC\uAC00 BOF\uAC00 \uC544\uB2D8");
@@ -18062,21 +18119,41 @@ function processGlobals(records) {
18062
18119
  codePage = r.data.readUInt16LE(0);
18063
18120
  } else if (r.opcode === OP_FILEPASS) {
18064
18121
  encrypted = true;
18122
+ } else if (r.opcode === OP_DATE1904 && r.data.length >= 2) {
18123
+ date1904 = r.data.readUInt16LE(0) === 1;
18124
+ } else if (r.opcode === OP_FORMAT) {
18125
+ const f = decodeFormatRecord(r.data);
18126
+ if (f) customFormats.set(f.ifmt, f.code);
18127
+ } else if (r.opcode === OP_XF && r.data.length >= 4) {
18128
+ xfFmtIds.push(r.data.readUInt16LE(2));
18065
18129
  }
18066
18130
  i++;
18067
18131
  }
18132
+ const dateXfs = /* @__PURE__ */ new Map();
18133
+ for (let k = 0; k < xfFmtIds.length; k++) {
18134
+ const kind = dateKindOfFmt(xfFmtIds[k], customFormats);
18135
+ if (kind) dateXfs.set(k, kind);
18136
+ }
18068
18137
  const globalsRecords = records.slice(0, i);
18069
18138
  const sst = decodeSST(globalsRecords);
18070
- return { sheets, sst, codePage, encrypted, endIndex: i };
18139
+ return { sheets, sst, codePage, encrypted, dateXfs, date1904, endIndex: i };
18071
18140
  }
18072
18141
  function findSheetBofIndex(records, lbPlyPos) {
18073
18142
  const exact = records.findIndex(
18074
18143
  (r) => r.opcode === OP_BOF && r.offset === lbPlyPos
18075
18144
  );
18076
18145
  if (exact >= 0) return exact;
18077
- const bofIndices = records.map((r, idx) => r.opcode === OP_BOF ? idx : -1).filter((idx) => idx >= 0);
18078
- if (bofIndices.length === 0) return -1;
18079
- return bofIndices.length > 1 ? bofIndices[1] : -1;
18146
+ let best = -1;
18147
+ let bestOffset = Infinity;
18148
+ for (let idx = 1; idx < records.length; idx++) {
18149
+ const r = records[idx];
18150
+ if (r.opcode !== OP_BOF) continue;
18151
+ if (r.offset >= lbPlyPos && r.offset < bestOffset) {
18152
+ best = idx;
18153
+ bestOffset = r.offset;
18154
+ }
18155
+ }
18156
+ return best;
18080
18157
  }
18081
18158
  function cellValueToText(v) {
18082
18159
  if (v === null || v === void 0) return "";
@@ -18207,10 +18284,18 @@ async function parseXlsDocument(buffer, options) {
18207
18284
  ]
18208
18285
  };
18209
18286
  }
18287
+ const convertNum = globals.dateXfs.size > 0 ? (n, ixfe) => {
18288
+ const kind = globals.dateXfs.get(ixfe);
18289
+ if (kind) {
18290
+ const iso = dateSerialToIso(n, globals.date1904, kind);
18291
+ if (iso) return iso;
18292
+ }
18293
+ return n;
18294
+ } : void 0;
18210
18295
  const totalSheets = Math.min(globals.sheets.length, MAX_SHEETS2);
18211
18296
  let pageFilter = null;
18212
18297
  if (options?.pages) {
18213
- const { parsePageRange: parsePageRange2 } = await import("./page-range-OF5I4PQY.js");
18298
+ const { parsePageRange: parsePageRange2 } = await import("./page-range-737B4EZW.js");
18214
18299
  pageFilter = parsePageRange2(options.pages, totalSheets);
18215
18300
  }
18216
18301
  const allBlocks = [];
@@ -18233,7 +18318,7 @@ async function parseXlsDocument(buffer, options) {
18233
18318
  continue;
18234
18319
  }
18235
18320
  try {
18236
- const { sheet } = extractSheetCells(records, bofIdx, globals.sst);
18321
+ const { sheet } = extractSheetCells(records, bofIdx, globals.sst, convertNum);
18237
18322
  const blocks = sheetToBlocks2(meta.name, sheet, i);
18238
18323
  allBlocks.push(...blocks);
18239
18324
  } catch (e) {
@@ -18361,6 +18446,9 @@ var NARY_MAP = {
18361
18446
  "\u2A02": "\\bigotimes",
18362
18447
  "\u2A00": "\\bigodot"
18363
18448
  };
18449
+ function onOffVal(v) {
18450
+ return v !== "0" && v !== "false" && v !== "off";
18451
+ }
18364
18452
  function mapDelim(ch, isLeft) {
18365
18453
  const l = {
18366
18454
  "(": "(",
@@ -18387,10 +18475,23 @@ function mapDelim(ch, isLeft) {
18387
18475
  const map = isLeft ? l : r;
18388
18476
  return map[ch] ?? ch;
18389
18477
  }
18478
+ function isBalancedWrap(s) {
18479
+ let depth = 0;
18480
+ for (let i = 0; i < s.length; i++) {
18481
+ const ch = s[i];
18482
+ if (ch === "{" && s[i - 1] !== "\\") depth++;
18483
+ else if (ch === "}" && s[i - 1] !== "\\") {
18484
+ depth--;
18485
+ if (depth === 0) return i === s.length - 1;
18486
+ if (depth < 0) return false;
18487
+ }
18488
+ }
18489
+ return false;
18490
+ }
18390
18491
  function grp(body) {
18391
18492
  const s = body.trim();
18392
18493
  if (s.length === 0) return "{}";
18393
- if (s.startsWith("{") && s.endsWith("}")) return s;
18494
+ if (s.startsWith("{") && s.endsWith("}") && isBalancedWrap(s)) return s;
18394
18495
  return "{" + s + "}";
18395
18496
  }
18396
18497
  function childrenToLatex(parent) {
@@ -18484,8 +18585,8 @@ function nodeToLatex(el) {
18484
18585
  }
18485
18586
  const sh = firstKid(naryPr, "subHide");
18486
18587
  const ph = firstKid(naryPr, "supHide");
18487
- if (sh) subHide = (sh.getAttribute("m:val") ?? sh.getAttribute("val")) !== "0";
18488
- if (ph) supHide = (ph.getAttribute("m:val") ?? ph.getAttribute("val")) !== "0";
18588
+ if (sh) subHide = onOffVal(sh.getAttribute("m:val") ?? sh.getAttribute("val"));
18589
+ if (ph) supHide = onOffVal(ph.getAttribute("m:val") ?? ph.getAttribute("val"));
18489
18590
  const ll = firstKid(naryPr, "limLoc");
18490
18591
  if (ll) limLoc = ll.getAttribute("m:val") ?? ll.getAttribute("val") ?? "";
18491
18592
  }
@@ -18632,7 +18733,7 @@ function isDisplayMath(el) {
18632
18733
  }
18633
18734
 
18634
18735
  // src/docx/parser.ts
18635
- var MAX_DECOMPRESS_SIZE4 = 100 * 1024 * 1024;
18736
+ var MAX_DECOMPRESS_SIZE5 = 100 * 1024 * 1024;
18636
18737
  function matchesLocal(el, localName2) {
18637
18738
  return el.localName === localName2 || (el.tagName?.endsWith(`:${localName2}`) ?? false);
18638
18739
  }
@@ -18650,6 +18751,8 @@ function effectiveChildElements(parent) {
18650
18751
  result.push(...effectiveChildElements(c));
18651
18752
  }
18652
18753
  }
18754
+ } else if (matchesLocal(el, "ins") || matchesLocal(el, "smartTag")) {
18755
+ result.push(...effectiveChildElements(el));
18653
18756
  } else {
18654
18757
  result.push(el);
18655
18758
  }
@@ -18714,6 +18817,21 @@ function parseStyles(xml) {
18714
18817
  }
18715
18818
  styles.set(styleId, { name, basedOn, outlineLevel });
18716
18819
  }
18820
+ for (const [styleId, info] of styles) {
18821
+ if (info.outlineLevel !== void 0) continue;
18822
+ const seen = /* @__PURE__ */ new Set([styleId]);
18823
+ let cur = info.basedOn;
18824
+ while (cur && !seen.has(cur)) {
18825
+ seen.add(cur);
18826
+ const parent = styles.get(cur);
18827
+ if (!parent) break;
18828
+ if (parent.outlineLevel !== void 0) {
18829
+ info.outlineLevel = parent.outlineLevel;
18830
+ break;
18831
+ }
18832
+ cur = parent.basedOn;
18833
+ }
18834
+ }
18717
18835
  return styles;
18718
18836
  }
18719
18837
  function parseNumbering(xml) {
@@ -18801,8 +18919,12 @@ function collectOmmlRoots(p) {
18801
18919
  return out;
18802
18920
  }
18803
18921
  function extractRun(r) {
18804
- const tElements = getChildElements(r, "t");
18805
- const text = tElements.map((t) => t.textContent ?? "").join("");
18922
+ let text = "";
18923
+ for (const el of effectiveChildElements(r)) {
18924
+ if (matchesLocal(el, "t")) text += el.textContent ?? "";
18925
+ else if (matchesLocal(el, "br") || matchesLocal(el, "cr")) text += "\n";
18926
+ else if (matchesLocal(el, "tab")) text += " ";
18927
+ }
18806
18928
  let bold = false;
18807
18929
  let italic = false;
18808
18930
  const rPrEls = getChildElements(r, "rPr");
@@ -18957,7 +19079,7 @@ function collectTextboxParagraphs(node, inTxbx = false, out = [], depth = 0) {
18957
19079
  }
18958
19080
  return out;
18959
19081
  }
18960
- function parseTable(tbl2, styles, numbering, footnotes, rels) {
19082
+ function parseTable(tbl2, styles, numbering, footnotes, rels, keepEmptyCols) {
18961
19083
  const trElements = getChildElements(tbl2, "tr");
18962
19084
  if (trElements.length === 0) return null;
18963
19085
  const rawRows = [];
@@ -19019,7 +19141,7 @@ ${cell2.text}` : cell2.text;
19019
19141
  return { text: cell2.text, colSpan: cell2.colSpan, rowSpan, colAddr: cell2.col, rowAddr: r };
19020
19142
  })
19021
19143
  );
19022
- const table2 = buildTable(cellRows);
19144
+ const table2 = buildTable(cellRows, { keepAnchoredEmptyCols: keepEmptyCols });
19023
19145
  if (table2.rows === 0 || table2.cols === 0) return null;
19024
19146
  return { type: "table", table: table2 };
19025
19147
  }
@@ -19048,9 +19170,17 @@ async function buildImageMap(zip, rels, doc, warnings) {
19048
19170
  const imageMap = /* @__PURE__ */ new Map();
19049
19171
  const images = [];
19050
19172
  let imgIdx = 0;
19173
+ const imageRefs = [];
19051
19174
  for (const blip of findElements(doc.documentElement, "blip")) {
19052
19175
  const embedId = getAttr(blip, "embed");
19053
- if (!embedId || imageMap.has(embedId)) continue;
19176
+ if (embedId) imageRefs.push(embedId);
19177
+ }
19178
+ for (const imagedata of collectNonFallbackImagedata(doc.documentElement)) {
19179
+ const embedId = getAttr(imagedata, "id");
19180
+ if (embedId) imageRefs.push(embedId);
19181
+ }
19182
+ for (const embedId of imageRefs) {
19183
+ if (imageMap.has(embedId)) continue;
19054
19184
  const target = rels.get(embedId);
19055
19185
  if (!target) continue;
19056
19186
  const imgPath = target.startsWith("/") ? target.slice(1) : target.startsWith("word/") ? target : `word/${target}`;
@@ -19085,14 +19215,26 @@ function collectParagraphBlips(node, out = [], depth = 0) {
19085
19215
  if (depth > 40) return out;
19086
19216
  for (const el of effectiveChildElements(node)) {
19087
19217
  if (matchesLocal(el, "txbxContent") || matchesLocal(el, "Fallback")) continue;
19088
- if (matchesLocal(el, "blip")) out.push(el);
19218
+ if (matchesLocal(el, "blip") || matchesLocal(el, "imagedata")) out.push(el);
19089
19219
  else collectParagraphBlips(el, out, depth + 1);
19090
19220
  }
19091
19221
  return out;
19092
19222
  }
19223
+ function collectNonFallbackImagedata(node, out = [], depth = 0) {
19224
+ if (depth > 60) return out;
19225
+ const children = node.childNodes;
19226
+ for (let i = 0; i < children.length; i++) {
19227
+ if (children[i].nodeType !== 1) continue;
19228
+ const el = children[i];
19229
+ if (matchesLocal(el, "Fallback")) continue;
19230
+ if (matchesLocal(el, "imagedata")) out.push(el);
19231
+ else collectNonFallbackImagedata(el, out, depth + 1);
19232
+ }
19233
+ return out;
19234
+ }
19093
19235
  function emitParagraphImages(p, imageMap, linked, out) {
19094
19236
  for (const blip of collectParagraphBlips(p)) {
19095
- const embedId = getAttr(blip, "embed");
19237
+ const embedId = getAttr(blip, "embed") ?? getAttr(blip, "id");
19096
19238
  if (!embedId) continue;
19097
19239
  const filename = imageMap.get(embedId);
19098
19240
  if (!filename) continue;
@@ -19101,7 +19243,7 @@ function emitParagraphImages(p, imageMap, linked, out) {
19101
19243
  }
19102
19244
  }
19103
19245
  async function parseDocxDocument(buffer, options) {
19104
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
19246
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE5);
19105
19247
  const zip = await JSZip4.loadAsync(buffer);
19106
19248
  const warnings = [];
19107
19249
  const docFile = zip.file("word/document.xml");
@@ -19172,7 +19314,7 @@ async function parseDocxDocument(buffer, options) {
19172
19314
  if (imageMap.size > 0) emitParagraphImages(tp, imageMap, linkedImages, blocks);
19173
19315
  }
19174
19316
  } else if (localName2 === "tbl") {
19175
- const block = parseTable(el, styles, numbering, footnotes, rels);
19317
+ const block = parseTable(el, styles, numbering, footnotes, rels, options?.keepTrailingEmptyCols);
19176
19318
  if (block) blocks.push(block);
19177
19319
  }
19178
19320
  }
@@ -19264,7 +19406,7 @@ function parseHwpmlDocument(buffer, options) {
19264
19406
  if (localName(el) !== "SECTION") continue;
19265
19407
  sectionIdx++;
19266
19408
  if (pageFilter && !pageFilter.has(sectionIdx)) continue;
19267
- parseSection2(el, blocks, paraShapeMap, sectionIdx, warnings);
19409
+ parseSection2(el, blocks, paraShapeMap, sectionIdx, warnings, options?.keepTrailingEmptyCols ?? false);
19268
19410
  }
19269
19411
  const outline = blocks.filter((b) => b.type === "heading" && b.text).map((b) => ({ level: b.level ?? 1, text: b.text, pageNumber: b.pageNumber }));
19270
19412
  const markdown = blocksToMarkdown(blocks);
@@ -19300,10 +19442,10 @@ function buildParaShapeMap(root) {
19300
19442
  }
19301
19443
  return map;
19302
19444
  }
19303
- function parseSection2(section, blocks, paraShapeMap, sectionNum, warnings) {
19304
- walkContent(section, blocks, paraShapeMap, sectionNum, warnings, false);
19445
+ function parseSection2(section, blocks, paraShapeMap, sectionNum, warnings, keep) {
19446
+ walkContent(section, blocks, paraShapeMap, sectionNum, warnings, false, keep);
19305
19447
  }
19306
- function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, depth = 0) {
19448
+ function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, keep, depth = 0) {
19307
19449
  if (depth > MAX_XML_DEPTH2) return;
19308
19450
  const children = node.childNodes;
19309
19451
  for (let i = 0; i < children.length; i++) {
@@ -19316,24 +19458,24 @@ function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderF
19316
19458
  if (tag === "P") {
19317
19459
  if (!inHeaderFooter) {
19318
19460
  parseParagraph3(el, blocks, paraShapeMap, sectionNum);
19319
- walkTablesInP(el, blocks, paraShapeMap, sectionNum, warnings);
19461
+ walkTablesInP(el, blocks, paraShapeMap, sectionNum, warnings, keep);
19320
19462
  }
19321
19463
  continue;
19322
19464
  }
19323
19465
  if (tag === "TABLE") {
19324
19466
  if (!inHeaderFooter) {
19325
- parseTable2(el, blocks, paraShapeMap, sectionNum, warnings);
19467
+ parseTable2(el, blocks, paraShapeMap, sectionNum, warnings, keep);
19326
19468
  }
19327
19469
  continue;
19328
19470
  }
19329
19471
  if (tag === "PARALIST" || tag === "SECTION" || tag === "COLDEF") {
19330
- walkContent(el, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, depth + 1);
19472
+ walkContent(el, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, keep, depth + 1);
19331
19473
  continue;
19332
19474
  }
19333
- walkContent(el, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, depth + 1);
19475
+ walkContent(el, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, keep, depth + 1);
19334
19476
  }
19335
19477
  }
19336
- function walkTablesInP(node, blocks, paraShapeMap, sectionNum, warnings, depth = 0) {
19478
+ function walkTablesInP(node, blocks, paraShapeMap, sectionNum, warnings, keep, depth = 0) {
19337
19479
  if (depth > MAX_XML_DEPTH2) return;
19338
19480
  const children = node.childNodes;
19339
19481
  for (let i = 0; i < children.length; i++) {
@@ -19341,11 +19483,11 @@ function walkTablesInP(node, blocks, paraShapeMap, sectionNum, warnings, depth =
19341
19483
  if (el.nodeType !== 1) continue;
19342
19484
  const tag = localName(el);
19343
19485
  if (tag === "TABLE") {
19344
- parseTable2(el, blocks, paraShapeMap, sectionNum, warnings);
19486
+ parseTable2(el, blocks, paraShapeMap, sectionNum, warnings, keep);
19345
19487
  continue;
19346
19488
  }
19347
19489
  if (tag === "FOOTNOTE" || tag === "ENDNOTE" || tag === "HEADER" || tag === "FOOTER") continue;
19348
- walkTablesInP(el, blocks, paraShapeMap, sectionNum, warnings, depth + 1);
19490
+ walkTablesInP(el, blocks, paraShapeMap, sectionNum, warnings, keep, depth + 1);
19349
19491
  }
19350
19492
  }
19351
19493
  function parseParagraph3(el, blocks, paraShapeMap, sectionNum) {
@@ -19381,7 +19523,7 @@ function collectCharText(node, parts, depth = 0) {
19381
19523
  }
19382
19524
  }
19383
19525
  }
19384
- function parseTable2(el, blocks, paraShapeMap, sectionNum, warnings) {
19526
+ function parseTable2(el, blocks, paraShapeMap, sectionNum, warnings, keep) {
19385
19527
  const cells = [];
19386
19528
  const rowCount = parseInt(el.getAttribute("RowCount") ?? "0", 10);
19387
19529
  const colCount = parseInt(el.getAttribute("ColCount") ?? "0", 10);
@@ -19425,7 +19567,7 @@ function parseTable2(el, blocks, paraShapeMap, sectionNum, warnings) {
19425
19567
  const cellRows = grid.map(
19426
19568
  (row) => row.map((cell2) => cell2 ?? { text: "", colSpan: 1, rowSpan: 1 })
19427
19569
  );
19428
- const table2 = buildTable(cellRows);
19570
+ const table2 = buildTable(cellRows, { keepAnchoredEmptyCols: keep });
19429
19571
  const caption = extractShapeCaption(el);
19430
19572
  if (caption.text && caption.before) {
19431
19573
  blocks.push({ type: "paragraph", text: caption.text, pageNumber: sectionNum });
@@ -19661,7 +19803,7 @@ function findMatchingKey(cellLabel, values) {
19661
19803
  bestKey = key;
19662
19804
  }
19663
19805
  } else if (key.startsWith(cellLabel)) {
19664
- if (cellLabel.length >= key.length * 0.6 && cellLabel.length > bestLen) {
19806
+ if (cellLabel.length >= key.length * 0.75 && cellLabel.length > bestLen) {
19665
19807
  bestLen = cellLabel.length;
19666
19808
  bestKey = key;
19667
19809
  }
@@ -19701,7 +19843,7 @@ function fillInCellPatterns(cellText, values, matchedLabels, blockedLabels) {
19701
19843
  const matchKey = values.available(normalizedKw) ? normalizedKw : void 0;
19702
19844
  if (matchKey === void 0) return match;
19703
19845
  const val = values.peek(matchKey);
19704
- const isTruthy = ["\u2611", "\u2713", "\u2714", "v", "V", "true", "1", "yes", "o", "O"].includes(val.trim()) || val.trim() === "";
19846
+ const isTruthy = ["\u2611", "\u2713", "\u2714", "v", "V", "true", "1", "yes", "o", "O"].includes(val.trim());
19705
19847
  if (!isTruthy) return match;
19706
19848
  values.consume(matchKey);
19707
19849
  matchedLabels.add(matchKey);
@@ -19723,14 +19865,20 @@ function fillInCellPatterns(cellText, values, matchedLabels, blockedLabels) {
19723
19865
  );
19724
19866
  return matches.length > 0 ? { text, matches } : null;
19725
19867
  }
19726
- var INLINE_LABEL_RE = /([가-힣A-Za-z]{2,10})\s*[::]/g;
19868
+ var INLINE_LABEL_RE = /((?:[가-힣A-Za-z]{1,10} )?)([가-힣A-Za-z]{2,10})\s*[::]/g;
19727
19869
  function scanInlineSegments(text) {
19728
19870
  const labels = [];
19729
19871
  INLINE_LABEL_RE.lastIndex = 0;
19730
19872
  let m;
19731
19873
  while ((m = INLINE_LABEL_RE.exec(text)) !== null) {
19732
19874
  if (text[INLINE_LABEL_RE.lastIndex] === "/") continue;
19733
- labels.push({ label: m[1], start: m.index, end: INLINE_LABEL_RE.lastIndex });
19875
+ labels.push({
19876
+ label: m[2],
19877
+ ext: m[1] ? m[1] + m[2] : void 0,
19878
+ extStart: m[1] ? m.index : void 0,
19879
+ start: m.index + m[1].length,
19880
+ end: INLINE_LABEL_RE.lastIndex
19881
+ });
19734
19882
  }
19735
19883
  const segments = [];
19736
19884
  for (let i = 0; i < labels.length; i++) {
@@ -19748,21 +19896,44 @@ function scanInlineSegments(text) {
19748
19896
  labelStart: cur.start,
19749
19897
  valueStart: vs,
19750
19898
  valueEnd: ve,
19751
- value: text.slice(vs, ve)
19899
+ value: text.slice(vs, ve),
19900
+ ...cur.ext !== void 0 ? { extLabel: cur.ext, extStart: cur.extStart } : {}
19752
19901
  });
19753
19902
  }
19754
19903
  return segments;
19755
19904
  }
19905
+ function matchInlineSegment(seg, values, blockedLabels) {
19906
+ const nlabel = normalizeLabel(seg.label);
19907
+ if (seg.extLabel !== void 0) {
19908
+ const nExt = normalizeLabel(seg.extLabel);
19909
+ if (nExt !== nlabel && !blockedLabels?.has(nExt) && values.has(nExt)) {
19910
+ return { key: nExt, label: seg.extLabel, viaExt: true };
19911
+ }
19912
+ }
19913
+ if (blockedLabels?.has(nlabel)) return void 0;
19914
+ const key = findMatchingKey(nlabel, values);
19915
+ return key !== void 0 ? { key, label: seg.label, viaExt: false } : void 0;
19916
+ }
19917
+ function clampSegmentEnd(text, seg, next, nextViaExt) {
19918
+ let ve = seg.valueEnd;
19919
+ if (nextViaExt && next?.extStart !== void 0 && next.extStart < ve) ve = next.extStart;
19920
+ while (ve > seg.valueStart && /\s/.test(text[ve - 1])) ve--;
19921
+ return ve;
19922
+ }
19756
19923
  function padInsertion(text, pos, value) {
19757
19924
  const lead = pos > 0 && !/\s/.test(text[pos - 1]) ? " " : "";
19758
19925
  const trail = pos < text.length && !/\s/.test(text[pos]) ? " " : "";
19759
19926
  return lead + value + trail;
19760
19927
  }
19761
- function normalizeValues(values) {
19928
+ function normalizeValues(values, warnings) {
19762
19929
  const map = /* @__PURE__ */ new Map();
19763
19930
  for (const [label, raw] of Object.entries(values)) {
19764
19931
  const { value, format } = typeof raw === "object" && !Array.isArray(raw) ? raw : { value: raw, format: void 0 };
19765
- map.set(normalizeLabel(label), Array.isArray(value) ? value.map((v) => formatFillValue(v, format)) : formatFillValue(value, format));
19932
+ const key = normalizeLabel(label);
19933
+ if (map.has(key)) {
19934
+ 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`);
19935
+ }
19936
+ map.set(key, Array.isArray(value) ? value.map((v) => formatFillValue(v, format)) : formatFillValue(value, format));
19766
19937
  }
19767
19938
  return map;
19768
19939
  }
@@ -19951,10 +20122,10 @@ function extractFromTable(table2) {
19951
20122
  }
19952
20123
  if (fields.length === 0 && table2.rows >= 2 && table2.cols >= 2) {
19953
20124
  const headerRow = table2.cells[0];
19954
- const allLabels = headerRow.every((cell2) => {
19955
- const t = cell2.text.trim();
20125
+ const allLabels = headerRow?.every((cell2) => {
20126
+ const t = cell2?.text.trim() ?? "";
19956
20127
  return t.length > 0 && t.length <= 20;
19957
- });
20128
+ }) ?? false;
19958
20129
  if (allLabels) {
19959
20130
  for (let r = 1; r < table2.rows; r++) {
19960
20131
  for (let c = 0; c < table2.cols; c++) {
@@ -20045,7 +20216,8 @@ function fillFormFields(blocks, values, blockedLabels) {
20045
20216
  const cloned = structuredClone(blocks);
20046
20217
  const filled = [];
20047
20218
  const matchedLabels = /* @__PURE__ */ new Set();
20048
- const normalizedValues = normalizeValues(values);
20219
+ const warnings = [];
20220
+ const normalizedValues = normalizeValues(values, warnings);
20049
20221
  const cursor = new ValueCursor(normalizedValues);
20050
20222
  const allTables = collectIRTables(cloned, 0);
20051
20223
  const patternFilledCells = /* @__PURE__ */ new Set();
@@ -20074,7 +20246,7 @@ function fillFormFields(blocks, values, blockedLabels) {
20074
20246
  if (newText !== block.text) block.text = newText;
20075
20247
  }
20076
20248
  const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
20077
- return { blocks: cloned, filled, unmatched };
20249
+ return { blocks: cloned, filled, unmatched, ...warnings.length > 0 ? { warnings } : {} };
20078
20250
  }
20079
20251
  function collectIRTables(blocks, depth) {
20080
20252
  if (depth > 16) return [];
@@ -20108,13 +20280,31 @@ function coveredPositions(table2) {
20108
20280
  }
20109
20281
  return covered;
20110
20282
  }
20283
+ function isHeaderDataTable(table2, covered) {
20284
+ if (table2.rows < 2) return false;
20285
+ const headerRow = table2.cells[0];
20286
+ if (!headerRow?.length) return false;
20287
+ const allLabels = headerRow.every((cell2) => {
20288
+ const t = cell2?.text.trim() ?? "";
20289
+ return t.length > 0 && t.length <= 20 && isLabelCell(t);
20290
+ });
20291
+ if (!allLabels) return false;
20292
+ for (let c = 0; c < table2.cols; c++) {
20293
+ if (covered.has(`1,${c}`)) continue;
20294
+ const cell2 = table2.cells[1]?.[c];
20295
+ if (!cell2) break;
20296
+ return !isLabelCell(cell2.text);
20297
+ }
20298
+ return true;
20299
+ }
20111
20300
  function fillTable(table2, values, filled, matchedLabels, patternFilledCells, blockedLabels) {
20112
20301
  if (table2.cols < 2) return;
20113
20302
  const covered = coveredPositions(table2);
20114
- for (let r = 0; r < table2.rows; r++) {
20303
+ const skipHeaderRow = isHeaderDataTable(table2, covered);
20304
+ for (let r = skipHeaderRow ? 1 : 0; r < table2.rows; r++) {
20115
20305
  for (let c = 0; c < table2.cols; c++) {
20116
20306
  if (covered.has(`${r},${c}`)) continue;
20117
- const labelCell = table2.cells[r][c];
20307
+ const labelCell = table2.cells[r]?.[c];
20118
20308
  if (!labelCell) continue;
20119
20309
  if (!isLabelCell(labelCell.text)) continue;
20120
20310
  let vc = c + labelCell.colSpan;
@@ -20147,10 +20337,10 @@ function fillTable(table2, values, filled, matchedLabels, patternFilledCells, bl
20147
20337
  }
20148
20338
  if (table2.rows >= 2 && table2.cols >= 2) {
20149
20339
  const headerRow = table2.cells[0];
20150
- const allLabels = headerRow.every((cell2) => {
20151
- const t = cell2.text.trim();
20340
+ const allLabels = headerRow?.every((cell2) => {
20341
+ const t = cell2?.text.trim() ?? "";
20152
20342
  return t.length > 0 && t.length <= 20 && isLabelCell(t);
20153
- });
20343
+ }) ?? false;
20154
20344
  if (!allLabels) return;
20155
20345
  for (let r = 1; r < table2.rows; r++) {
20156
20346
  for (let c = 0; c < table2.cols; c++) {
@@ -20165,7 +20355,11 @@ function fillTable(table2, values, filled, matchedLabels, patternFilledCells, bl
20165
20355
  if (!values.isArray(matchKey) && matchedLabels.has(matchKey)) continue;
20166
20356
  const newValue = values.consume(matchKey);
20167
20357
  if (newValue === void 0) continue;
20168
- valueCell.text = newValue;
20358
+ if (patternFilledCells?.has(valueCell)) {
20359
+ valueCell.text = newValue + " " + valueCell.text;
20360
+ } else {
20361
+ valueCell.text = newValue;
20362
+ }
20169
20363
  matchedLabels.add(matchKey);
20170
20364
  filled.push({
20171
20365
  label: headerCell.text.trim(),
@@ -20181,20 +20375,22 @@ function fillTable(table2, values, filled, matchedLabels, patternFilledCells, bl
20181
20375
  function fillInlineFields(text, values, filled, matchedLabels, blockedLabels) {
20182
20376
  const segments = scanInlineSegments(text);
20183
20377
  if (segments.length === 0) return text;
20378
+ const matches = segments.map((seg) => matchInlineSegment(seg, values, blockedLabels));
20184
20379
  let out = "";
20185
20380
  let pos = 0;
20186
- for (const seg of segments) {
20187
- const nlabel = normalizeLabel(seg.label);
20188
- if (blockedLabels?.has(nlabel)) continue;
20189
- const matchKey = findMatchingKey(nlabel, values);
20190
- if (matchKey === void 0) continue;
20381
+ for (let i = 0; i < segments.length; i++) {
20382
+ const seg = segments[i];
20383
+ const matched = matches[i];
20384
+ if (matched === void 0) continue;
20385
+ const matchKey = matched.key;
20191
20386
  const newValue = values.consume(matchKey);
20192
20387
  if (newValue === void 0) continue;
20193
20388
  matchedLabels.add(matchKey);
20194
- filled.push({ label: seg.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20389
+ filled.push({ label: matched.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20390
+ const ve = clampSegmentEnd(text, seg, segments[i + 1], matches[i + 1]?.viaExt ?? false);
20195
20391
  out += text.slice(pos, seg.valueStart);
20196
- out += seg.valueStart === seg.valueEnd ? padInsertion(text, seg.valueStart, newValue) : newValue;
20197
- pos = seg.valueEnd;
20392
+ out += seg.valueStart === ve ? padInsertion(text, seg.valueStart, newValue) : newValue;
20393
+ pos = ve;
20198
20394
  }
20199
20395
  out += text.slice(pos);
20200
20396
  return out;
@@ -20209,7 +20405,8 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20209
20405
  if (sectionPaths.length === 0) {
20210
20406
  throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
20211
20407
  }
20212
- const normalizedValues = normalizeValues(values);
20408
+ const warnings = [];
20409
+ const normalizedValues = normalizeValues(values, warnings);
20213
20410
  const cursor = new ValueCursor(normalizedValues);
20214
20411
  const matchedLabels = /* @__PURE__ */ new Set();
20215
20412
  const filled = [];
@@ -20271,7 +20468,17 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20271
20468
  }
20272
20469
  }
20273
20470
  for (const table2 of allTables) {
20274
- for (let rowIdx = 0; rowIdx < table2.rows.length; rowIdx++) {
20471
+ const skipHeaderRow = table2.rows.length >= 2 && (() => {
20472
+ const first = table2.rows[0];
20473
+ const allLabels = first.length > 0 && first.every((cell2) => {
20474
+ const t = cellLabelText(cell2).trim();
20475
+ return t.length > 0 && t.length <= 20 && isLabelCell(t);
20476
+ });
20477
+ if (!allLabels) return false;
20478
+ const d0 = table2.rows[1][0];
20479
+ return d0 === void 0 || !isLabelCell(cellLabelText(d0));
20480
+ })();
20481
+ for (let rowIdx = skipHeaderRow ? 1 : 0; rowIdx < table2.rows.length; rowIdx++) {
20275
20482
  const cells = table2.rows[rowIdx];
20276
20483
  for (let colIdx = 0; colIdx < cells.length - 1; colIdx++) {
20277
20484
  const labelText = cellLabelText(cells[colIdx]);
@@ -20342,9 +20549,30 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20342
20549
  const matchKey = findMatchingKey(headerLabel, cursor);
20343
20550
  if (matchKey === void 0) continue;
20344
20551
  if (!cursor.isArray(matchKey) && matchedLabels.has(matchKey)) continue;
20552
+ const dataCell = dataCells[colIdx];
20553
+ if (patternApplied.has(dataCell)) {
20554
+ const target = dataCell.paragraphs.find((p) => p.tRanges.length > 0) ?? dataCell.paragraphs[0];
20555
+ if (!target) continue;
20556
+ const l = led(target);
20557
+ if (l.fullText !== void 0) continue;
20558
+ const newValue2 = cursor.consume(matchKey);
20559
+ if (newValue2 === void 0) continue;
20560
+ l.ranges.push({ start: 0, end: 0, replacement: newValue2 + " " });
20561
+ l.filledIdx.push(filled.length);
20562
+ l.matchKeys.push(matchKey);
20563
+ matchedLabels.add(matchKey);
20564
+ filled.push({
20565
+ label: cellLabelText(headerCells[colIdx]).trim(),
20566
+ value: newValue2,
20567
+ row: rowIdx,
20568
+ col: colIdx,
20569
+ key: matchKey
20570
+ });
20571
+ continue;
20572
+ }
20345
20573
  const newValue = cursor.consume(matchKey);
20346
20574
  if (newValue === void 0) continue;
20347
- const paras = dataCells[colIdx].paragraphs;
20575
+ const paras = dataCell.paragraphs;
20348
20576
  if (paras.length === 0) continue;
20349
20577
  const l0 = led(paras[0]);
20350
20578
  l0.fullText = newValue;
@@ -20373,20 +20601,23 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20373
20601
  const existing = ledger.get(para2);
20374
20602
  if (existing?.fullText !== void 0) continue;
20375
20603
  const text = matchText(para2);
20376
- for (const seg of scanInlineSegments(text)) {
20377
- const nlabel = normalizeLabel(seg.label);
20378
- if (blockedLabels?.has(nlabel)) continue;
20379
- const matchKey = findMatchingKey(nlabel, cursor);
20380
- if (matchKey === void 0) continue;
20604
+ const segments = scanInlineSegments(text);
20605
+ const matches = segments.map((seg) => matchInlineSegment(seg, cursor, blockedLabels));
20606
+ for (let i = 0; i < segments.length; i++) {
20607
+ const seg = segments[i];
20608
+ const matched = matches[i];
20609
+ if (matched === void 0) continue;
20610
+ const matchKey = matched.key;
20381
20611
  const newValue = cursor.consume(matchKey);
20382
20612
  if (newValue === void 0) continue;
20383
- const replacement = seg.valueStart === seg.valueEnd ? padInsertion(text, seg.valueStart, newValue) : newValue;
20613
+ const ve = clampSegmentEnd(text, seg, segments[i + 1], matches[i + 1]?.viaExt ?? false);
20614
+ const replacement = seg.valueStart === ve ? padInsertion(text, seg.valueStart, newValue) : newValue;
20384
20615
  const l = led(para2);
20385
- l.ranges.push({ start: seg.valueStart, end: seg.valueEnd, replacement });
20616
+ l.ranges.push({ start: seg.valueStart, end: ve, replacement });
20386
20617
  matchedLabels.add(matchKey);
20387
20618
  l.filledIdx.push(filled.length);
20388
20619
  l.matchKeys.push(matchKey);
20389
- filled.push({ label: seg.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20620
+ filled.push({ label: matched.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20390
20621
  }
20391
20622
  }
20392
20623
  const splices = [];
@@ -20439,7 +20670,7 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20439
20670
  }
20440
20671
  }
20441
20672
  for (const k of failedKeys) {
20442
- if (!succeededKeys.has(k)) matchedLabels.delete(k);
20673
+ if (!succeededKeys.has(k) || cursor.isArray(k)) matchedLabels.delete(k);
20443
20674
  }
20444
20675
  const cleanFilled = filled.filter((f) => f !== null);
20445
20676
  const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
@@ -20447,7 +20678,8 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20447
20678
  return {
20448
20679
  buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
20449
20680
  filled: cleanFilled,
20450
- unmatched
20681
+ unmatched,
20682
+ ...warnings.length > 0 ? { warnings } : {}
20451
20683
  };
20452
20684
  }
20453
20685
 
@@ -20724,27 +20956,39 @@ function parseMarkdownToBlocks(md2) {
20724
20956
  if (/^<table[\s>]/i.test(line.trimStart())) {
20725
20957
  const htmlLines = [];
20726
20958
  let depth = 0;
20727
- while (i < lines.length) {
20728
- const l = lines[i];
20959
+ let closed = false;
20960
+ let j = i;
20961
+ while (j < lines.length) {
20962
+ const l = lines[j];
20729
20963
  htmlLines.push(l);
20730
20964
  depth += (l.match(/<table[\s>]/gi) ?? []).length;
20731
20965
  depth -= (l.match(/<\/table>/gi) ?? []).length;
20732
- i++;
20733
- if (depth <= 0) break;
20966
+ j++;
20967
+ if (depth <= 0) {
20968
+ closed = true;
20969
+ break;
20970
+ }
20971
+ }
20972
+ if (closed) {
20973
+ blocks.push({ type: "html_table", text: htmlLines.join("\n") });
20974
+ i = j;
20975
+ continue;
20734
20976
  }
20735
- blocks.push({ type: "html_table", text: htmlLines.join("\n") });
20736
- continue;
20737
20977
  }
20738
20978
  if (line.trimStart().startsWith("|")) {
20739
20979
  const tableRows = [];
20980
+ let sepSeen = false;
20740
20981
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
20741
20982
  const row = lines[i];
20742
- const sepCells = row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
20743
- if (sepCells.every((c) => /^\s*:?-+:?\s*$/.test(c))) {
20744
- i++;
20745
- continue;
20983
+ if (tableRows.length === 1 && !sepSeen) {
20984
+ const sepCells = row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
20985
+ if (sepCells.every((c) => /^\s*:?-+:?\s*$/.test(c))) {
20986
+ sepSeen = true;
20987
+ i++;
20988
+ continue;
20989
+ }
20746
20990
  }
20747
- const cells = row.split("|").slice(1, -1).map((c) => c.trim());
20991
+ const cells = row.split(/(?<!\\)\|/).slice(1, -1).map((c) => c.trim().replace(/\\\|/g, "|"));
20748
20992
  if (cells.length > 0) tableRows.push(cells);
20749
20993
  i++;
20750
20994
  }
@@ -21693,13 +21937,26 @@ function normalize(s) {
21693
21937
  return s.replace(/\s+/g, " ").trim();
21694
21938
  }
21695
21939
  var MAX_LEVENSHTEIN_LEN = 1e4;
21940
+ function approxDistance(a, b) {
21941
+ const bigramCounts = (s) => {
21942
+ const m = /* @__PURE__ */ new Map();
21943
+ for (let i = 0; i < s.length - 1; i++) {
21944
+ const g = s.slice(i, i + 2);
21945
+ m.set(g, (m.get(g) ?? 0) + 1);
21946
+ }
21947
+ return m;
21948
+ };
21949
+ const ca = bigramCounts(a);
21950
+ const cb = bigramCounts(b);
21951
+ let inter = 0;
21952
+ for (const [g, n] of ca) inter += Math.min(n, cb.get(g) ?? 0);
21953
+ const total = Math.max(a.length - 1, 0) + Math.max(b.length - 1, 0);
21954
+ const dice = total > 0 ? 2 * inter / total : 1;
21955
+ return Math.round(Math.max(a.length, b.length) * (1 - dice));
21956
+ }
21696
21957
  function levenshtein(a, b) {
21697
21958
  if (a.length + b.length > MAX_LEVENSHTEIN_LEN) {
21698
- const sampleLen = Math.min(500, a.length, b.length);
21699
- let diffs = 0;
21700
- for (let i = 0; i < sampleLen; i++) if (a[i] !== b[i]) diffs++;
21701
- const sampleRate = sampleLen > 0 ? diffs / sampleLen : 1;
21702
- return Math.abs(a.length - b.length) + Math.round(Math.min(a.length, b.length) * sampleRate);
21959
+ return approxDistance(a, b);
21703
21960
  }
21704
21961
  if (a.length > b.length) [a, b] = [b, a];
21705
21962
  const m = a.length;
@@ -21851,9 +22108,16 @@ function bestSimInRange(arr, from, to, target) {
21851
22108
  return best;
21852
22109
  }
21853
22110
  function escapeGfm(text) {
21854
- return text.replace(/([~*])/g, "\\$1");
22111
+ const NUL = String.fromCharCode(0);
22112
+ const spans = [];
22113
+ const masked = text.replace(/!\[[^\]]*\]\([^)\n]*\)|\]\((?:https?:|mailto:|tel:|#)[^)\n]*\)/gi, (m) => {
22114
+ spans.push(m);
22115
+ return NUL + (spans.length - 1) + NUL;
22116
+ });
22117
+ const escaped = masked.replace(/([~*_`])/g, "\\$1");
22118
+ return escaped.replace(new RegExp(NUL + "(\\d+)" + NUL, "g"), (_, n) => spans[Number(n)]);
21855
22119
  }
21856
- var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
22120
+ var HWP_SHAPE_ALT_TEXT_RE = /^(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?$/gm;
21857
22121
  function sanitizeText(text) {
21858
22122
  let result = mapPuaText(text).replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
21859
22123
  if (result.length <= 30 && result.includes(" ")) {
@@ -21869,7 +22133,7 @@ function normForMatch(text) {
21869
22133
  return sanitizeText(text).replace(/\s+/g, " ").trim();
21870
22134
  }
21871
22135
  function unescapeGfm(text) {
21872
- return text.replace(/\\([~*])/g, "$1");
22136
+ return text.replace(/\\([~*_`])/g, "$1");
21873
22137
  }
21874
22138
  function summarize(text) {
21875
22139
  const t = text.replace(/\s+/g, " ").trim();
@@ -21936,7 +22200,7 @@ function parseGfmTable(lines) {
21936
22200
  return rows;
21937
22201
  }
21938
22202
  function unescapeGfmCell(text) {
21939
- return text.replace(/<br\s*\/?>/gi, "\n").replace(/\\\|/g, "|").replace(/\\([~*])/g, "$1");
22203
+ return text.replace(/<br\s*\/?>/gi, "\n").replace(/\\\|/g, "|").replace(/\\([~*_`])/g, "$1");
21940
22204
  }
21941
22205
  function replicateCellInnerHtml(cell2) {
21942
22206
  if (cell2.blocks?.length) {
@@ -22024,10 +22288,10 @@ function parseHtmlTable(raw) {
22024
22288
  }
22025
22289
  } else {
22026
22290
  if (!isClose) {
22027
- const cs = parseInt(attrs.match(/colspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
22028
- const rs = parseInt(attrs.match(/rowspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
22291
+ const cs = parseInt(attrs.match(/colspan\s*=\s*["']?(\d+)/i)?.[1] || "1", 10);
22292
+ const rs = parseInt(attrs.match(/rowspan\s*=\s*["']?(\d+)/i)?.[1] || "1", 10);
22029
22293
  cellStart = m.index + m[0].length;
22030
- cellInfo = { colSpan: isNaN(cs) ? 1 : cs, rowSpan: isNaN(rs) ? 1 : rs };
22294
+ cellInfo = { colSpan: clampSpan(isNaN(cs) ? 1 : cs, MAX_COLS), rowSpan: clampSpan(isNaN(rs) ? 1 : rs, MAX_ROWS) };
22031
22295
  } else if (cellStart >= 0 && cellInfo && currentRow) {
22032
22296
  currentRow.push({ inner: raw.slice(cellStart, m.index), colSpan: cellInfo.colSpan, rowSpan: cellInfo.rowSpan });
22033
22297
  cellStart = -1;
@@ -22671,8 +22935,8 @@ function layoutHtmlRows(rows) {
22671
22935
  let c = 0;
22672
22936
  for (const cell2 of rows[r].cells) {
22673
22937
  while (occupied.has(`${r},${c}`)) c++;
22674
- const colSpan = Math.max(1, cell2.colSpan);
22675
- const rowSpan = Math.max(1, cell2.rowSpan);
22938
+ const colSpan = clampSpan(cell2.colSpan, MAX_COLS);
22939
+ const rowSpan = clampSpan(cell2.rowSpan, MAX_ROWS);
22676
22940
  placed.push({ r, c, colSpan, rowSpan, inner: cell2.inner, isHeader: rows[r].tag === "th" });
22677
22941
  for (let dr = 0; dr < rowSpan; dr++) {
22678
22942
  for (let dc = 0; dc < colSpan; dc++) occupied.add(`${r + dr},${c + dc}`);
@@ -23831,12 +24095,20 @@ function diffBlocks(blocksA, blocksB) {
23831
24095
  function alignBlocks(a, b) {
23832
24096
  const m = a.length, n = b.length;
23833
24097
  if (m * n > 1e7) return fallbackAlign(a, b);
24098
+ const lenOf = (blk) => {
24099
+ const t = blk.text !== void 0 ? blk.text : blk.type === "table" && blk.table ? blk.table.cells.flat().map((c) => c?.text ?? "").join(" ") : "";
24100
+ return t.replace(/\s+/g, " ").trim().length;
24101
+ };
24102
+ const aLen = a.map(lenOf);
24103
+ const bLen = b.map(lenOf);
23834
24104
  const simCache = /* @__PURE__ */ new Map();
23835
24105
  const getSim = (i2, j2) => {
23836
24106
  const key = `${i2},${j2}`;
23837
24107
  let v = simCache.get(key);
23838
24108
  if (v === void 0) {
23839
- v = blockSimilarity(a[i2], b[j2]);
24109
+ const mx = Math.max(aLen[i2], bLen[j2]);
24110
+ const cut = a[i2].type === "table" || b[j2].type === "table" ? 6 / 7 : 1 - SIMILARITY_THRESHOLD;
24111
+ v = mx > 0 && (mx - Math.min(aLen[i2], bLen[j2])) / mx > cut ? 0 : blockSimilarity(a[i2], b[j2]);
23840
24112
  simCache.set(key, v);
23841
24113
  }
23842
24114
  return v;
@@ -23897,8 +24169,8 @@ function blockSimilarity(a, b) {
23897
24169
  }
23898
24170
  function tableSimilarity(a, b) {
23899
24171
  const dimSim = 1 - Math.abs(a.rows * a.cols - b.rows * b.cols) / Math.max(a.rows * a.cols, b.rows * b.cols, 1);
23900
- const textsA = a.cells.flat().map((c) => c.text).join(" ");
23901
- const textsB = b.cells.flat().map((c) => c.text).join(" ");
24172
+ const textsA = a.cells.flat().map((c) => c?.text ?? "").join(" ");
24173
+ const textsB = b.cells.flat().map((c) => c?.text ?? "").join(" ");
23902
24174
  const contentSim = normalizedSimilarity(textsA, textsB);
23903
24175
  return dimSim * 0.3 + contentSim * 0.7;
23904
24176
  }
@@ -23909,8 +24181,8 @@ function diffTableCells(a, b) {
23909
24181
  for (let r = 0; r < maxRows; r++) {
23910
24182
  const row = [];
23911
24183
  for (let c = 0; c < maxCols; c++) {
23912
- const cellA = r < a.rows && c < a.cols ? a.cells[r][c].text : void 0;
23913
- const cellB = r < b.rows && c < b.cols ? b.cells[r][c].text : void 0;
24184
+ const cellA = r < a.rows && c < a.cols ? a.cells[r]?.[c]?.text : void 0;
24185
+ const cellB = r < b.rows && c < b.cols ? b.cells[r]?.[c]?.text : void 0;
23914
24186
  let type;
23915
24187
  if (cellA === void 0) type = "added";
23916
24188
  else if (cellB === void 0) type = "removed";
@@ -25171,16 +25443,16 @@ function buildOrigUnits(blocks) {
25171
25443
  for (let i = 0; i < blocks.length; i++) {
25172
25444
  const block = blocks[i];
25173
25445
  let consume = 1;
25174
- let chunk2;
25446
+ let chunk;
25175
25447
  if (block.type === "paragraph" && block.text && /^\[별표\s*\d+/.test(sanitizeText(block.text))) {
25176
25448
  const next = blocks[i + 1];
25177
25449
  if (next?.type === "paragraph" && next.text && /관련\)?$/.test(next.text)) consume = 2;
25178
- chunk2 = blocksToMarkdown(blocks.slice(i, i + consume));
25450
+ chunk = blocksToMarkdown(blocks.slice(i, i + consume));
25179
25451
  } else {
25180
- chunk2 = blocksToMarkdown([block]);
25452
+ chunk = blocksToMarkdown([block]);
25181
25453
  }
25182
- if (chunk2) {
25183
- const subUnits = splitMarkdownUnits(chunk2);
25454
+ if (chunk) {
25455
+ const subUnits = splitMarkdownUnits(chunk);
25184
25456
  const isFragment = consume === 2 || (block.type === "paragraph" || block.type === "heading") && subUnits.length > 1;
25185
25457
  for (let s = 0; s < subUnits.length; s++) {
25186
25458
  const u = { ...subUnits[s], blockIdx: i, fragment: isFragment || void 0 };
@@ -25447,6 +25719,9 @@ var Surgeon = class {
25447
25719
  miniFatSectors = [];
25448
25720
  dirSectors = [];
25449
25721
  entries = [];
25722
+ /** replace()가 FREESECT로 해제한 섹터 — finish()에서 재할당 안 된 것만 0으로 지움 (데이터 잔존 방지) */
25723
+ freedSectors = [];
25724
+ freedMiniSectors = [];
25450
25725
  constructor(file) {
25451
25726
  if (file.length < SECTOR || file.readUInt32LE(0) !== 3759263696) {
25452
25727
  throw new OleSurgeonError("OLE \uC2DC\uADF8\uB2C8\uCC98\uAC00 \uC544\uB2D9\uB2C8\uB2E4");
@@ -25570,6 +25845,8 @@ var Surgeon = class {
25570
25845
  if (this.fat[i] !== FREESECT) continue;
25571
25846
  if (SECTOR + (i + 1) * SECTOR > this.buf.length) continue;
25572
25847
  this.fat[i] = ENDOFCHAIN;
25848
+ const off = this.sectorOffset(i);
25849
+ this.buf.fill(0, off, off + SECTOR);
25573
25850
  out.push(i);
25574
25851
  }
25575
25852
  while (out.length < n) {
@@ -25669,9 +25946,15 @@ var Surgeon = class {
25669
25946
  const entry = this.findEntry(path);
25670
25947
  if (entry.size > 0 && entry.start !== ENDOFCHAIN) {
25671
25948
  if (entry.size < MINI_CUTOFF) {
25672
- for (const s of this.miniChain(entry.start)) this.miniFat[s] = FREESECT;
25949
+ for (const s of this.miniChain(entry.start)) {
25950
+ this.miniFat[s] = FREESECT;
25951
+ this.freedMiniSectors.push(s);
25952
+ }
25673
25953
  } else {
25674
- for (const s of this.chain(entry.start)) this.fat[s] = FREESECT;
25954
+ for (const s of this.chain(entry.start)) {
25955
+ this.fat[s] = FREESECT;
25956
+ this.freedSectors.push(s);
25957
+ }
25675
25958
  }
25676
25959
  }
25677
25960
  if (newData.length < MINI_CUTOFF) {
@@ -25700,9 +25983,27 @@ var Surgeon = class {
25700
25983
  this.writeDirEntry(entry);
25701
25984
  }
25702
25985
  finish() {
25986
+ this.wipeFreedSectors();
25703
25987
  this.flushFat();
25704
25988
  return this.buf;
25705
25989
  }
25990
+ /** 해제 후 재할당되지 않고 남은 FREESECT 섹터의 바이트를 0으로 채움 (데이터 remanence 제거) */
25991
+ wipeFreedSectors() {
25992
+ for (const s of this.freedSectors) {
25993
+ if (this.fat[s] !== FREESECT) continue;
25994
+ const off = this.sectorOffset(s);
25995
+ this.buf.fill(0, off, off + SECTOR);
25996
+ }
25997
+ if (this.freedMiniSectors.length > 0) {
25998
+ const root = this.rootEntry();
25999
+ const rootChain = root.start === ENDOFCHAIN || root.size === 0 ? [] : this.chain(root.start);
26000
+ for (const s of this.freedMiniSectors) {
26001
+ if (this.miniFat[s] !== FREESECT) continue;
26002
+ const off = this.miniOffset(s, rootChain);
26003
+ this.buf.fill(0, off, off + MINI_SECTOR);
26004
+ }
26005
+ }
26006
+ }
25706
26007
  };
25707
26008
 
25708
26009
  // src/roundtrip/hwp5-patch.ts
@@ -27133,7 +27434,7 @@ async function parseHwp3(buffer, options) {
27133
27434
  const { markdown, blocks, metadata, outline, warnings } = parseHwp3Document(buffer, options);
27134
27435
  return { success: true, fileType: "hwp3", markdown, blocks, metadata, outline, warnings };
27135
27436
  } catch (err) {
27136
- return { success: false, fileType: "hwp3", error: err instanceof Error ? err.message : "HWP3 \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
27437
+ return { success: false, fileType: "hwp3", error: sanitizeError(err), code: classifyError(err) };
27137
27438
  }
27138
27439
  }
27139
27440
  async function parseHwpx(buffer, options) {
@@ -27141,7 +27442,7 @@ async function parseHwpx(buffer, options) {
27141
27442
  const { markdown, blocks, metadata, outline, warnings, images } = await parseHwpxDocument(buffer, options);
27142
27443
  return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
27143
27444
  } catch (err) {
27144
- return { success: false, fileType: "hwpx", error: err instanceof Error ? err.message : "HWPX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
27445
+ return { success: false, fileType: "hwpx", error: sanitizeError(err), code: classifyError(err) };
27145
27446
  }
27146
27447
  }
27147
27448
  async function parseHwp(buffer, options) {
@@ -27166,13 +27467,13 @@ async function parseHwp(buffer, options) {
27166
27467
  }
27167
27468
  return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
27168
27469
  } catch (err) {
27169
- return { success: false, fileType: "hwp", error: err instanceof Error ? err.message : "HWP \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
27470
+ return { success: false, fileType: "hwp", error: sanitizeError(err), code: classifyError(err) };
27170
27471
  }
27171
27472
  }
27172
27473
  async function parsePdf(buffer, options) {
27173
27474
  let parsePdfDocument;
27174
27475
  try {
27175
- const mod = await import("./parser-5EHWYJMC.js");
27476
+ const mod = await import("./parser-PEOYBXBO.js");
27176
27477
  parsePdfDocument = mod.parsePdfDocument;
27177
27478
  } catch {
27178
27479
  return {
@@ -27183,11 +27484,11 @@ async function parsePdf(buffer, options) {
27183
27484
  };
27184
27485
  }
27185
27486
  try {
27186
- const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary } = await parsePdfDocument(buffer, options);
27187
- return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary };
27487
+ const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images } = await parsePdfDocument(buffer, options);
27488
+ return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images };
27188
27489
  } catch (err) {
27189
27490
  const isImageBased = err instanceof Error && "isImageBased" in err ? true : void 0;
27190
- return { success: false, fileType: "pdf", error: err instanceof Error ? err.message : "PDF \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err), isImageBased };
27491
+ return { success: false, fileType: "pdf", error: sanitizeError(err), code: classifyError(err), isImageBased };
27191
27492
  }
27192
27493
  }
27193
27494
  async function parseXlsx(buffer, options) {
@@ -27195,7 +27496,7 @@ async function parseXlsx(buffer, options) {
27195
27496
  const { markdown, blocks, metadata, warnings } = await parseXlsxDocument(buffer, options);
27196
27497
  return { success: true, fileType: "xlsx", markdown, blocks, metadata, warnings };
27197
27498
  } catch (err) {
27198
- return { success: false, fileType: "xlsx", error: err instanceof Error ? err.message : "XLSX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
27499
+ return { success: false, fileType: "xlsx", error: sanitizeError(err), code: classifyError(err) };
27199
27500
  }
27200
27501
  }
27201
27502
  async function parseXls(buffer, options) {
@@ -27203,7 +27504,7 @@ async function parseXls(buffer, options) {
27203
27504
  const { markdown, blocks, metadata, warnings } = await parseXlsDocument(buffer, options);
27204
27505
  return { success: true, fileType: "xls", markdown, blocks, metadata, warnings };
27205
27506
  } catch (err) {
27206
- return { success: false, fileType: "xls", error: err instanceof Error ? err.message : "XLS \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
27507
+ return { success: false, fileType: "xls", error: sanitizeError(err), code: classifyError(err) };
27207
27508
  }
27208
27509
  }
27209
27510
  async function parseDocx(buffer, options) {
@@ -27211,7 +27512,7 @@ async function parseDocx(buffer, options) {
27211
27512
  const { markdown, blocks, metadata, outline, warnings, images } = await parseDocxDocument(buffer, options);
27212
27513
  return { success: true, fileType: "docx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
27213
27514
  } catch (err) {
27214
- return { success: false, fileType: "docx", error: err instanceof Error ? err.message : "DOCX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
27515
+ return { success: false, fileType: "docx", error: sanitizeError(err), code: classifyError(err) };
27215
27516
  }
27216
27517
  }
27217
27518
  async function parseHwpml(buffer, options) {
@@ -27219,7 +27520,7 @@ async function parseHwpml(buffer, options) {
27219
27520
  const { markdown, blocks, metadata, outline, warnings } = parseHwpmlDocument(buffer, options);
27220
27521
  return { success: true, fileType: "hwpml", markdown, blocks, metadata, outline, warnings };
27221
27522
  } catch (err) {
27222
- return { success: false, fileType: "hwpml", error: err instanceof Error ? err.message : "HWPML \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
27523
+ return { success: false, fileType: "hwpml", error: sanitizeError(err), code: classifyError(err) };
27223
27524
  }
27224
27525
  }
27225
27526
  async function fillForm(input, values, outputFormat = "markdown") {
@@ -27305,4 +27606,4 @@ export {
27305
27606
  parseHwpml,
27306
27607
  fillForm
27307
27608
  };
27308
- //# sourceMappingURL=chunk-TJJTLM76.js.map
27609
+ //# sourceMappingURL=chunk-L2XQQTOY.js.map