kordoc 1.7.2 → 1.8.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.
package/dist/index.cjs CHANGED
@@ -30,6 +30,44 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/page-range.ts
34
+ var page_range_exports = {};
35
+ __export(page_range_exports, {
36
+ parsePageRange: () => parsePageRange
37
+ });
38
+ function parsePageRange(spec, maxPages) {
39
+ const result = /* @__PURE__ */ new Set();
40
+ if (maxPages <= 0) return result;
41
+ if (Array.isArray(spec)) {
42
+ for (const n of spec) {
43
+ const page = Math.round(n);
44
+ if (page >= 1 && page <= maxPages) result.add(page);
45
+ }
46
+ return result;
47
+ }
48
+ if (typeof spec !== "string" || spec.trim() === "") return result;
49
+ const parts = spec.split(",");
50
+ for (const part of parts) {
51
+ const trimmed = part.trim();
52
+ if (!trimmed) continue;
53
+ const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
54
+ if (rangeMatch) {
55
+ const start = Math.max(1, parseInt(rangeMatch[1], 10));
56
+ const end = Math.min(maxPages, parseInt(rangeMatch[2], 10));
57
+ for (let i = start; i <= end; i++) result.add(i);
58
+ } else {
59
+ const page = parseInt(trimmed, 10);
60
+ if (!isNaN(page) && page >= 1 && page <= maxPages) result.add(page);
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ var init_page_range = __esm({
66
+ "src/page-range.ts"() {
67
+ "use strict";
68
+ }
69
+ });
70
+
33
71
  // src/ocr/provider.ts
34
72
  var provider_exports = {};
35
73
  __export(provider_exports, {
@@ -79,28 +117,36 @@ __export(index_exports, {
79
117
  blocksToMarkdown: () => blocksToMarkdown,
80
118
  compare: () => compare,
81
119
  detectFormat: () => detectFormat,
120
+ detectZipFormat: () => detectZipFormat,
82
121
  diffBlocks: () => diffBlocks,
83
122
  extractFormFields: () => extractFormFields,
84
123
  isHwpxFile: () => isHwpxFile,
85
124
  isOldHwpFile: () => isOldHwpFile,
86
125
  isPdfFile: () => isPdfFile,
126
+ isZipFile: () => isZipFile,
87
127
  markdownToHwpx: () => markdownToHwpx,
88
128
  parse: () => parse,
129
+ parseDocx: () => parseDocx,
89
130
  parseHwp: () => parseHwp,
90
131
  parseHwpx: () => parseHwpx,
91
- parsePdf: () => parsePdf
132
+ parsePdf: () => parsePdf,
133
+ parseXlsx: () => parseXlsx
92
134
  });
93
135
  module.exports = __toCommonJS(index_exports);
94
136
  var import_promises = require("fs/promises");
95
137
 
96
138
  // src/detect.ts
139
+ var import_jszip = __toESM(require("jszip"), 1);
97
140
  function magicBytes(buffer) {
98
141
  return new Uint8Array(buffer, 0, Math.min(4, buffer.byteLength));
99
142
  }
100
- function isHwpxFile(buffer) {
143
+ function isZipFile(buffer) {
101
144
  const b = magicBytes(buffer);
102
145
  return b[0] === 80 && b[1] === 75 && b[2] === 3 && b[3] === 4;
103
146
  }
147
+ function isHwpxFile(buffer) {
148
+ return isZipFile(buffer);
149
+ }
104
150
  function isOldHwpFile(buffer) {
105
151
  const b = magicBytes(buffer);
106
152
  return b[0] === 208 && b[1] === 207 && b[2] === 17 && b[3] === 224;
@@ -111,14 +157,27 @@ function isPdfFile(buffer) {
111
157
  }
112
158
  function detectFormat(buffer) {
113
159
  if (buffer.byteLength < 4) return "unknown";
114
- if (isHwpxFile(buffer)) return "hwpx";
160
+ if (isZipFile(buffer)) return "hwpx";
115
161
  if (isOldHwpFile(buffer)) return "hwp";
116
162
  if (isPdfFile(buffer)) return "pdf";
117
163
  return "unknown";
118
164
  }
165
+ async function detectZipFormat(buffer) {
166
+ try {
167
+ const zip = await import_jszip.default.loadAsync(buffer);
168
+ if (zip.file("xl/workbook.xml")) return "xlsx";
169
+ if (zip.file("word/document.xml")) return "docx";
170
+ if (zip.file("Contents/content.hpf") || zip.file("mimetype")) return "hwpx";
171
+ const hasSection = Object.keys(zip.files).some((f) => f.startsWith("Contents/"));
172
+ if (hasSection) return "hwpx";
173
+ return "unknown";
174
+ } catch {
175
+ return "unknown";
176
+ }
177
+ }
119
178
 
120
179
  // src/hwpx/parser.ts
121
- var import_jszip = __toESM(require("jszip"), 1);
180
+ var import_jszip2 = __toESM(require("jszip"), 1);
122
181
  var import_zlib = require("zlib");
123
182
  var import_xmldom = require("@xmldom/xmldom");
124
183
 
@@ -178,6 +237,16 @@ function buildTable(rows) {
178
237
  cellIdx++;
179
238
  }
180
239
  }
240
+ let effectiveCols = maxCols;
241
+ while (effectiveCols > 0) {
242
+ const colEmpty = grid.every((row) => !row[effectiveCols - 1]?.text?.trim());
243
+ if (!colEmpty) break;
244
+ effectiveCols--;
245
+ }
246
+ if (effectiveCols < maxCols && effectiveCols > 0) {
247
+ const trimmed = grid.map((row) => row.slice(0, effectiveCols));
248
+ return { rows: numRows, cols: effectiveCols, cells: trimmed, hasHeader: numRows > 1 };
249
+ }
181
250
  return { rows: numRows, cols: maxCols, cells: grid, hasHeader: numRows > 1 };
182
251
  }
183
252
  function convertTableToText(rows) {
@@ -185,13 +254,26 @@ function convertTableToText(rows) {
185
254
  (row) => row.map((c) => c.text.trim().replace(/\n/g, " ")).filter(Boolean).join(" | ")
186
255
  ).filter(Boolean).join("\n");
187
256
  }
257
+ var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
258
+ function sanitizeText(text) {
259
+ let result = text.replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
260
+ if (result.length <= 30 && result.includes(" ")) {
261
+ const tokens = result.split(" ");
262
+ const koreanSingleCharCount = tokens.filter((t) => t.length === 1 && /[\uAC00-\uD7AF\u3131-\u318E]/.test(t)).length;
263
+ if (tokens.length >= 3 && koreanSingleCharCount / tokens.length >= 0.7) {
264
+ result = tokens.join("");
265
+ }
266
+ }
267
+ return result;
268
+ }
188
269
  function blocksToMarkdown(blocks) {
189
270
  const lines = [];
190
271
  for (let i = 0; i < blocks.length; i++) {
191
272
  const block = blocks[i];
192
273
  if (block.type === "heading" && block.text) {
193
274
  const prefix = "#".repeat(Math.min(block.level || 2, 6));
194
- lines.push("", `${prefix} ${block.text}`, "");
275
+ const headingText = sanitizeText(block.text);
276
+ if (headingText) lines.push("", `${prefix} ${headingText}`, "");
195
277
  continue;
196
278
  }
197
279
  if (block.type === "image" && block.text) {
@@ -203,9 +285,11 @@ function blocksToMarkdown(blocks) {
203
285
  continue;
204
286
  }
205
287
  if (block.type === "list" && block.text) {
206
- const alreadyNumbered = block.listType === "ordered" && /^\d+\.\s/.test(block.text);
288
+ const listText = sanitizeText(block.text);
289
+ if (!listText) continue;
290
+ const alreadyNumbered = block.listType === "ordered" && /^\d+\.\s/.test(listText);
207
291
  const prefix = alreadyNumbered ? "" : block.listType === "ordered" ? "1. " : "- ";
208
- lines.push(`${prefix}${block.text}`);
292
+ lines.push(`${prefix}${listText}`);
209
293
  if (block.children) {
210
294
  for (const child of block.children) {
211
295
  const childPrefix = child.listType === "ordered" ? "1." : "-";
@@ -215,7 +299,8 @@ function blocksToMarkdown(blocks) {
215
299
  continue;
216
300
  }
217
301
  if (block.type === "paragraph" && block.text) {
218
- let text = block.text;
302
+ let text = sanitizeText(block.text);
303
+ if (!text) continue;
219
304
  if (/^\[별표\s*\d+/.test(text)) {
220
305
  const nextBlock = blocks[i + 1];
221
306
  if (nextBlock?.type === "paragraph" && nextBlock.text && /관련\)?$/.test(nextBlock.text)) {
@@ -252,7 +337,7 @@ function tableToMarkdown(table) {
252
337
  if (table.rows === 0 || table.cols === 0) return "";
253
338
  const { cells, rows: numRows, cols: numCols } = table;
254
339
  if (numRows === 1 && numCols === 1) {
255
- const content = cells[0][0].text;
340
+ const content = sanitizeText(cells[0][0].text);
256
341
  return content.split(/\n/).map((line) => {
257
342
  const trimmed = line.trim();
258
343
  if (!trimmed) return "";
@@ -261,13 +346,19 @@ function tableToMarkdown(table) {
261
346
  return trimmed;
262
347
  }).filter(Boolean).join("\n");
263
348
  }
349
+ if (numCols === 1 && numRows >= 2) {
350
+ return cells.map((row) => sanitizeText(row[0].text).replace(/\n/g, " ")).filter(Boolean).join("\n");
351
+ }
264
352
  const display = Array.from({ length: numRows }, () => Array(numCols).fill(""));
265
353
  const skip = /* @__PURE__ */ new Set();
266
354
  for (let r = 0; r < numRows; r++) {
355
+ let cellIdx = 0;
267
356
  for (let c = 0; c < numCols; c++) {
268
357
  if (skip.has(`${r},${c}`)) continue;
269
- const cell = cells[r][c];
270
- display[r][c] = cell.text.replace(/\n/g, "<br>");
358
+ const cell = cells[r]?.[cellIdx];
359
+ if (!cell) break;
360
+ cellIdx++;
361
+ display[r][c] = sanitizeText(cell.text).replace(/\n/g, "<br>");
271
362
  for (let dr = 0; dr < cell.rowSpan; dr++) {
272
363
  for (let dc = 0; dc < cell.colSpan; dc++) {
273
364
  if (dr === 0 && dc === 0) continue;
@@ -276,12 +367,28 @@ function tableToMarkdown(table) {
276
367
  }
277
368
  }
278
369
  }
370
+ c += cell.colSpan - 1;
279
371
  }
280
372
  }
281
373
  const uniqueRows = [];
282
- for (const row of display) {
374
+ let pendingFirstCol = "";
375
+ for (let r = 0; r < display.length; r++) {
376
+ const row = display[r];
283
377
  const isEmptyPlaceholder = row.every((cell) => cell === "");
284
- if (!isEmptyPlaceholder) uniqueRows.push(row);
378
+ if (isEmptyPlaceholder) continue;
379
+ const hasSkippedCols = row.some((cell, c) => cell === "" && skip.has(`${r},${c}`));
380
+ const nonEmptyCols = row.filter((cell) => cell !== "");
381
+ if (!hasSkippedCols && nonEmptyCols.length === 1 && row[0] !== "" && row.slice(1).every((c) => c === "")) {
382
+ pendingFirstCol = row[0];
383
+ continue;
384
+ }
385
+ if (pendingFirstCol && row[0] === "") {
386
+ row[0] = pendingFirstCol;
387
+ pendingFirstCol = "";
388
+ } else {
389
+ pendingFirstCol = "";
390
+ }
391
+ uniqueRows.push(row);
285
392
  }
286
393
  if (uniqueRows.length === 0) return "";
287
394
  const md = [];
@@ -293,8 +400,13 @@ function tableToMarkdown(table) {
293
400
  return md.join("\n");
294
401
  }
295
402
 
403
+ // src/types.ts
404
+ var HEADING_RATIO_H1 = 1.5;
405
+ var HEADING_RATIO_H2 = 1.3;
406
+ var HEADING_RATIO_H3 = 1.15;
407
+
296
408
  // src/utils.ts
297
- var VERSION = true ? "1.7.2" : "0.0.0-dev";
409
+ var VERSION = true ? "1.8.0" : "0.0.0-dev";
298
410
  function toArrayBuffer(buf) {
299
411
  if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
300
412
  return buf.buffer;
@@ -312,6 +424,50 @@ function isPathTraversal(name) {
312
424
  const normalized = name.replace(/\\/g, "/");
313
425
  return normalized.includes("..") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized);
314
426
  }
427
+ function precheckZipSize(buffer, maxUncompressedSize = 100 * 1024 * 1024, maxEntries = 500) {
428
+ try {
429
+ const data = new DataView(buffer);
430
+ const len = buffer.byteLength;
431
+ let eocdOffset = -1;
432
+ for (let i = len - 22; i >= Math.max(0, len - 65557); i--) {
433
+ if (data.getUint32(i, true) === 101010256) {
434
+ eocdOffset = i;
435
+ break;
436
+ }
437
+ }
438
+ if (eocdOffset < 0) return { totalUncompressed: 0, entryCount: 0 };
439
+ const entryCount = data.getUint16(eocdOffset + 10, true);
440
+ if (entryCount > maxEntries) {
441
+ throw new KordocError(`ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC: ${entryCount} (\uCD5C\uB300 ${maxEntries})`);
442
+ }
443
+ const cdSize = data.getUint32(eocdOffset + 12, true);
444
+ const cdOffset = data.getUint32(eocdOffset + 16, true);
445
+ if (cdOffset + cdSize > len) return { totalUncompressed: 0, entryCount };
446
+ let totalUncompressed = 0;
447
+ let pos = cdOffset;
448
+ for (let i = 0; i < entryCount && pos + 46 <= cdOffset + cdSize; i++) {
449
+ if (data.getUint32(pos, true) !== 33639248) break;
450
+ totalUncompressed += data.getUint32(pos + 24, true);
451
+ const nameLen = data.getUint16(pos + 28, true);
452
+ const extraLen = data.getUint16(pos + 30, true);
453
+ const commentLen = data.getUint16(pos + 32, true);
454
+ pos += 46 + nameLen + extraLen + commentLen;
455
+ }
456
+ if (totalUncompressed > maxUncompressedSize) {
457
+ throw new KordocError(`ZIP \uBE44\uC555\uCD95 \uD06C\uAE30 \uCD08\uACFC: ${(totalUncompressed / 1024 / 1024).toFixed(1)}MB (\uCD5C\uB300 ${maxUncompressedSize / 1024 / 1024}MB)`);
458
+ }
459
+ return { totalUncompressed, entryCount };
460
+ } catch (err) {
461
+ if (err instanceof KordocError) throw err;
462
+ return { totalUncompressed: 0, entryCount: 0 };
463
+ }
464
+ }
465
+ var SAFE_HREF_RE2 = /^(?:https?:|mailto:|tel:|#)/i;
466
+ function sanitizeHref2(href) {
467
+ const trimmed = href.trim();
468
+ if (!trimmed || !SAFE_HREF_RE2.test(trimmed)) return null;
469
+ return trimmed;
470
+ }
315
471
  function classifyError(err) {
316
472
  if (!(err instanceof Error)) return "PARSE_ERROR";
317
473
  const msg = err.message;
@@ -325,36 +481,8 @@ function classifyError(err) {
325
481
  return "PARSE_ERROR";
326
482
  }
327
483
 
328
- // src/page-range.ts
329
- function parsePageRange(spec, maxPages) {
330
- const result = /* @__PURE__ */ new Set();
331
- if (maxPages <= 0) return result;
332
- if (Array.isArray(spec)) {
333
- for (const n of spec) {
334
- const page = Math.round(n);
335
- if (page >= 1 && page <= maxPages) result.add(page);
336
- }
337
- return result;
338
- }
339
- if (typeof spec !== "string" || spec.trim() === "") return result;
340
- const parts = spec.split(",");
341
- for (const part of parts) {
342
- const trimmed = part.trim();
343
- if (!trimmed) continue;
344
- const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
345
- if (rangeMatch) {
346
- const start = Math.max(1, parseInt(rangeMatch[1], 10));
347
- const end = Math.min(maxPages, parseInt(rangeMatch[2], 10));
348
- for (let i = start; i <= end; i++) result.add(i);
349
- } else {
350
- const page = parseInt(trimmed, 10);
351
- if (!isNaN(page) && page >= 1 && page <= maxPages) result.add(page);
352
- }
353
- }
354
- return result;
355
- }
356
-
357
484
  // src/hwpx/parser.ts
485
+ init_page_range();
358
486
  var MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024;
359
487
  var MAX_ZIP_ENTRIES = 500;
360
488
  function clampSpan(val, max) {
@@ -446,16 +574,10 @@ function stripDtd(xml) {
446
574
  return xml.replace(/<!DOCTYPE\s[^[>]*(\[[\s\S]*?\])?\s*>/gi, "");
447
575
  }
448
576
  async function parseHwpxDocument(buffer, options) {
449
- const precheck = precheckZipSize(buffer);
450
- if (precheck.totalUncompressed > MAX_DECOMPRESS_SIZE) {
451
- throw new KordocError("ZIP \uBE44\uC555\uCD95 \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
452
- }
453
- if (precheck.entryCount > MAX_ZIP_ENTRIES) {
454
- throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
455
- }
577
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
456
578
  let zip;
457
579
  try {
458
- zip = await import_jszip.default.loadAsync(buffer);
580
+ zip = await import_jszip2.default.loadAsync(buffer);
459
581
  } catch {
460
582
  return extractFromBrokenZip(buffer);
461
583
  }
@@ -615,46 +737,17 @@ function parseDublinCoreMetadata(xml, metadata) {
615
737
  metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
616
738
  }
617
739
  }
618
- function precheckZipSize(buffer) {
619
- try {
620
- const data = new DataView(buffer);
621
- const len = buffer.byteLength;
622
- if (len < 22) return { totalUncompressed: 0, entryCount: 0 };
623
- const searchStart = Math.max(0, len - 22 - 65535);
624
- let eocdOffset = -1;
625
- for (let i = len - 22; i >= searchStart; i--) {
626
- if (data.getUint32(i, true) === 101010256) {
627
- eocdOffset = i;
628
- break;
629
- }
630
- }
631
- if (eocdOffset < 0) return { totalUncompressed: 0, entryCount: 0 };
632
- const entryCount = data.getUint16(eocdOffset + 10, true);
633
- const cdSize = data.getUint32(eocdOffset + 12, true);
634
- const cdOffset = data.getUint32(eocdOffset + 16, true);
635
- if (cdOffset + cdSize > len) return { totalUncompressed: 0, entryCount };
636
- let totalUncompressed = 0;
637
- let pos = cdOffset;
638
- for (let i = 0; i < entryCount && pos + 46 <= cdOffset + cdSize; i++) {
639
- if (data.getUint32(pos, true) !== 33639248) break;
640
- totalUncompressed += data.getUint32(pos + 24, true);
641
- const nameLen = data.getUint16(pos + 28, true);
642
- const extraLen = data.getUint16(pos + 30, true);
643
- const commentLen = data.getUint16(pos + 32, true);
644
- pos += 46 + nameLen + extraLen + commentLen;
645
- }
646
- return { totalUncompressed, entryCount };
647
- } catch {
648
- return { totalUncompressed: 0, entryCount: 0 };
649
- }
650
- }
651
740
  function extractFromBrokenZip(buffer) {
652
741
  const data = new Uint8Array(buffer);
653
742
  const view = new DataView(buffer);
654
743
  let pos = 0;
655
744
  const blocks = [];
745
+ const warnings = [
746
+ { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
747
+ ];
656
748
  let totalDecompressed = 0;
657
749
  let entryCount = 0;
750
+ let sectionNum = 0;
658
751
  while (pos < data.length - 30) {
659
752
  if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
660
753
  pos++;
@@ -700,14 +793,15 @@ function extractFromBrokenZip(buffer) {
700
793
  }
701
794
  totalDecompressed += content.length * 2;
702
795
  if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
703
- blocks.push(...parseSectionXml(content));
796
+ sectionNum++;
797
+ blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum));
704
798
  } catch {
705
799
  continue;
706
800
  }
707
801
  }
708
802
  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");
709
803
  const markdown = blocksToMarkdown(blocks);
710
- return { markdown, blocks };
804
+ return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
711
805
  }
712
806
  async function resolveSectionPaths(zip) {
713
807
  const manifestPaths = ["Contents/content.hpf", "content.hpf"];
@@ -771,9 +865,9 @@ function detectHwpxHeadings(blocks, styleMap) {
771
865
  let level = 0;
772
866
  if (baseFontSize > 0 && block.style?.fontSize) {
773
867
  const ratio = block.style.fontSize / baseFontSize;
774
- if (ratio >= 1.5) level = 1;
775
- else if (ratio >= 1.3) level = 2;
776
- else if (ratio >= 1.15) level = 3;
868
+ if (ratio >= HEADING_RATIO_H1) level = 1;
869
+ else if (ratio >= HEADING_RATIO_H2) level = 2;
870
+ else if (ratio >= HEADING_RATIO_H3) level = 3;
777
871
  }
778
872
  if (/^제\d+[조장절편]/.test(text) && text.length <= 50) {
779
873
  if (level === 0) level = 3;
@@ -905,39 +999,47 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
905
999
  if (depth > MAX_XML_DEPTH) return tableCtx;
906
1000
  const children = node.childNodes;
907
1001
  if (!children) return tableCtx;
908
- for (let i = 0; i < children.length; i++) {
909
- const el = children[i];
910
- if (el.nodeType !== 1) continue;
911
- const tag = el.tagName || el.localName || "";
912
- const localTag = tag.replace(/^[^:]+:/, "");
913
- if (localTag === "tbl") {
914
- if (tableCtx) tableStack.push(tableCtx);
915
- const newTable = { rows: [], currentRow: [], cell: null };
916
- walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
917
- if (newTable.rows.length > 0) {
918
- if (tableStack.length > 0) {
919
- const parentTable = tableStack.pop();
920
- const nestedText = convertTableToText(newTable.rows);
921
- if (parentTable.cell) {
922
- parentTable.cell.text += (parentTable.cell.text ? "\n" : "") + nestedText;
1002
+ const walkChildren = (parent, d) => {
1003
+ if (d > MAX_XML_DEPTH) return;
1004
+ const kids = parent.childNodes;
1005
+ if (!kids) return;
1006
+ for (let i = 0; i < kids.length; i++) {
1007
+ const el = kids[i];
1008
+ if (el.nodeType !== 1) continue;
1009
+ const tag = el.tagName || el.localName || "";
1010
+ const localTag = tag.replace(/^[^:]+:/, "");
1011
+ if (localTag === "tbl") {
1012
+ if (tableCtx) tableStack.push(tableCtx);
1013
+ const newTable = { rows: [], currentRow: [], cell: null };
1014
+ walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, d + 1);
1015
+ if (newTable.rows.length > 0) {
1016
+ if (tableStack.length > 0) {
1017
+ const parentTable = tableStack.pop();
1018
+ const nestedText = convertTableToText(newTable.rows);
1019
+ if (parentTable.cell) {
1020
+ parentTable.cell.text += (parentTable.cell.text ? "\n" : "") + nestedText;
1021
+ }
1022
+ tableCtx = parentTable;
1023
+ } else {
1024
+ blocks.push({ type: "table", table: buildTable(newTable.rows), pageNumber: sectionNum });
1025
+ tableCtx = null;
923
1026
  }
924
- tableCtx = parentTable;
925
1027
  } else {
926
- blocks.push({ type: "table", table: buildTable(newTable.rows), pageNumber: sectionNum });
927
- tableCtx = null;
1028
+ tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
928
1029
  }
929
- } else {
930
- tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
931
- }
932
- } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
933
- const imgRef = extractImageRef(el);
934
- if (imgRef) {
935
- blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
936
- } else if (warnings && sectionNum) {
937
- warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
1030
+ } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
1031
+ const imgRef = extractImageRef(el);
1032
+ if (imgRef) {
1033
+ blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
1034
+ } else if (warnings && sectionNum) {
1035
+ warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
1036
+ }
1037
+ } else if (localTag === "r" || localTag === "run" || localTag === "ctrl") {
1038
+ walkChildren(el, d + 1);
938
1039
  }
939
1040
  }
940
- }
1041
+ };
1042
+ walkChildren(node, depth);
941
1043
  return tableCtx;
942
1044
  }
943
1045
  function extractParagraphInfo(para, styleMap) {
@@ -976,7 +1078,10 @@ function extractParagraphInfo(para, styleMap) {
976
1078
  // 하이퍼링크
977
1079
  case "hyperlink": {
978
1080
  const url = child.getAttribute("url") || child.getAttribute("href") || "";
979
- if (url) href = url;
1081
+ if (url) {
1082
+ const safe = sanitizeHref2(url);
1083
+ if (safe) href = safe;
1084
+ }
980
1085
  walk(child);
981
1086
  break;
982
1087
  }
@@ -989,6 +1094,29 @@ function extractParagraphInfo(para, styleMap) {
989
1094
  if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
990
1095
  break;
991
1096
  }
1097
+ // 제어 요소 — 필드, 컨트롤, 매개변수 등 스킵
1098
+ case "ctrl":
1099
+ case "fieldBegin":
1100
+ case "fieldEnd":
1101
+ case "parameters":
1102
+ case "stringParam":
1103
+ case "integerParam":
1104
+ case "boolParam":
1105
+ case "floatParam":
1106
+ case "secPr":
1107
+ // 섹션 속성 (페이지 설정 등)
1108
+ case "colPr":
1109
+ // 다단 속성
1110
+ case "linesegarray":
1111
+ case "lineseg":
1112
+ // 레이아웃 정보
1113
+ // 도형/이미지 요소 — 대체텍스트("사각형입니다." 등) 누출 방지
1114
+ case "pic":
1115
+ case "shape":
1116
+ case "drawingObject":
1117
+ case "shapeComment":
1118
+ case "drawText":
1119
+ break;
992
1120
  // run 요소에서 charPrIDRef 추출
993
1121
  case "r": {
994
1122
  const runCharPr = child.getAttribute("charPrIDRef");
@@ -1003,7 +1131,10 @@ function extractParagraphInfo(para, styleMap) {
1003
1131
  }
1004
1132
  };
1005
1133
  walk(para);
1006
- const cleanText = text.replace(/[ \t]+/g, " ").trim();
1134
+ let cleanText = text.replace(/[ \t]+/g, " ").trim();
1135
+ if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
1136
+ cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
1137
+ cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|수식|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
1007
1138
  let style;
1008
1139
  if (styleMap && charPrId) {
1009
1140
  const charProp = styleMap.charProperties.get(charPrId);
@@ -1182,6 +1313,7 @@ function extractText(data) {
1182
1313
  }
1183
1314
 
1184
1315
  // src/hwp5/parser.ts
1316
+ init_page_range();
1185
1317
  var import_module = require("module");
1186
1318
  var import_meta = {};
1187
1319
  var require2 = (0, import_module.createRequire)(import_meta.url);
@@ -1282,9 +1414,9 @@ function detectHwp5Headings(blocks, docInfo) {
1282
1414
  if (/^\d+$/.test(text)) continue;
1283
1415
  const ratio = block.style.fontSize / baseFontSize;
1284
1416
  let level = 0;
1285
- if (ratio >= 1.5) level = 1;
1286
- else if (ratio >= 1.3) level = 2;
1287
- else if (ratio >= 1.15) level = 3;
1417
+ if (ratio >= HEADING_RATIO_H1) level = 1;
1418
+ else if (ratio >= HEADING_RATIO_H2) level = 2;
1419
+ else if (ratio >= HEADING_RATIO_H3) level = 3;
1288
1420
  if (/^제\d+[조장절편]/.test(text) && text.length <= 50) {
1289
1421
  if (level === 0) level = 3;
1290
1422
  }
@@ -1371,20 +1503,22 @@ function detectImageMime(data) {
1371
1503
  }
1372
1504
  function extractHwp5Images(cfb, blocks, compressed, warnings) {
1373
1505
  const binDataMap = /* @__PURE__ */ new Map();
1374
- for (let idx = 0; idx < 1e4; idx++) {
1375
- const entry = CFB.find(cfb, `/BinData/BIN${String(idx).padStart(4, "0")}`) || CFB.find(cfb, `/BinData/Bin${String(idx).padStart(4, "0")}`);
1376
- if (!entry?.content) {
1377
- if (idx > 0) break;
1378
- continue;
1379
- }
1380
- let data = Buffer.from(entry.content);
1381
- if (compressed) {
1382
- try {
1383
- data = decompressStream(data);
1384
- } catch {
1506
+ const binDataRe = /\/BinData\/[Bb][Ii][Nn](\d{4})$/;
1507
+ if (cfb.FileIndex) {
1508
+ for (const entry of cfb.FileIndex) {
1509
+ if (!entry?.name || !entry.content) continue;
1510
+ const match = entry.name.match(binDataRe);
1511
+ if (!match) continue;
1512
+ const idx = parseInt(match[1], 10);
1513
+ let data = Buffer.from(entry.content);
1514
+ if (compressed) {
1515
+ try {
1516
+ data = decompressStream(data);
1517
+ } catch {
1518
+ }
1385
1519
  }
1520
+ binDataMap.set(idx, { data, name: entry.name });
1386
1521
  }
1387
- binDataMap.set(idx, { data, name: entry.name || `BIN${idx}` });
1388
1522
  }
1389
1523
  if (binDataMap.size === 0) return [];
1390
1524
  const images = [];
@@ -1531,6 +1665,16 @@ function parseTableBlock(records, startIdx) {
1531
1665
  i++;
1532
1666
  }
1533
1667
  if (rows === 0 || cols === 0 || cells.length === 0) return { table: null, nextIdx: i };
1668
+ const hasAddr = cells.some((c) => c.colAddr !== void 0 && c.rowAddr !== void 0);
1669
+ if (hasAddr) {
1670
+ const cellRows2 = arrangeCells(rows, cols, cells);
1671
+ const irCells = cellRows2.map((row) => row.map((c) => ({
1672
+ text: c.text.trim(),
1673
+ colSpan: c.colSpan,
1674
+ rowSpan: c.rowSpan
1675
+ })));
1676
+ return { table: { rows, cols, cells: irCells, hasHeader: rows > 1 }, nextIdx: i };
1677
+ }
1534
1678
  const cellRows = arrangeCells(rows, cols, cells);
1535
1679
  return { table: buildTable(cellRows), nextIdx: i };
1536
1680
  }
@@ -1600,6 +1744,9 @@ function arrangeCells(rows, cols, cells) {
1600
1744
  return grid.map((row) => row.map((c) => c || { text: "", colSpan: 1, rowSpan: 1 }));
1601
1745
  }
1602
1746
 
1747
+ // src/pdf/parser.ts
1748
+ init_page_range();
1749
+
1603
1750
  // src/pdf/line-detector.ts
1604
1751
  var import_pdf = require("pdfjs-dist/legacy/build/pdf.mjs");
1605
1752
  var ORIENTATION_TOL = 2;
@@ -1794,7 +1941,36 @@ function buildTableGrids(horizontals, verticals) {
1794
1941
  };
1795
1942
  grids.push({ rowYs, colXs, bbox });
1796
1943
  }
1797
- return grids;
1944
+ return mergeAdjacentGrids(grids);
1945
+ }
1946
+ function mergeAdjacentGrids(grids) {
1947
+ if (grids.length <= 1) return grids;
1948
+ const sorted = [...grids].sort((a, b) => b.bbox.y2 - a.bbox.y2);
1949
+ const merged = [sorted[0]];
1950
+ for (let i = 1; i < sorted.length; i++) {
1951
+ const prev = merged[merged.length - 1];
1952
+ const curr = sorted[i];
1953
+ if (prev.colXs.length === curr.colXs.length) {
1954
+ const colMatch = prev.colXs.every((x, ci) => Math.abs(x - curr.colXs[ci]) <= COORD_MERGE_TOL * 3);
1955
+ const verticalGap = prev.bbox.y1 - curr.bbox.y2;
1956
+ if (colMatch && verticalGap >= -COORD_MERGE_TOL && verticalGap <= 20) {
1957
+ const allRowYs = [.../* @__PURE__ */ new Set([...prev.rowYs, ...curr.rowYs])].sort((a, b) => b - a);
1958
+ merged[merged.length - 1] = {
1959
+ rowYs: allRowYs,
1960
+ colXs: prev.colXs,
1961
+ bbox: {
1962
+ x1: Math.min(prev.bbox.x1, curr.bbox.x1),
1963
+ y1: Math.min(prev.bbox.y1, curr.bbox.y1),
1964
+ x2: Math.max(prev.bbox.x2, curr.bbox.x2),
1965
+ y2: Math.max(prev.bbox.y2, curr.bbox.y2)
1966
+ }
1967
+ };
1968
+ continue;
1969
+ }
1970
+ }
1971
+ merged.push(curr);
1972
+ }
1973
+ return merged;
1798
1974
  }
1799
1975
  function clusterCoordinates(values) {
1800
1976
  if (values.length === 0) return [];
@@ -1981,7 +2157,11 @@ function cellTextToString(items) {
1981
2157
  for (let j = 1; j < s.length; j++) {
1982
2158
  const gap = s[j].x - (s[j - 1].x + s[j - 1].w);
1983
2159
  const avgFs = (s[j].fontSize + s[j - 1].fontSize) / 2;
1984
- if (gap < avgFs * 0.3 && /[가-힣]$/.test(result) && /^[가-힣]/.test(s[j].text)) {
2160
+ const prevIsKorean = /[가-힣]$/.test(result);
2161
+ const currIsKorean = /^[가-힣]/.test(s[j].text);
2162
+ if (gap < avgFs * 0.15) {
2163
+ result += s[j].text;
2164
+ } else if (gap < avgFs * 0.35 && (prevIsKorean || currIsKorean)) {
1985
2165
  result += s[j].text;
1986
2166
  } else {
1987
2167
  result += " " + s[j].text;
@@ -1996,6 +2176,12 @@ function cellTextToString(items) {
1996
2176
  const curr = textLines[i];
1997
2177
  if (/[가-힣]$/.test(prev) && /^[가-힣]+$/.test(curr) && curr.length <= 8 && !curr.includes(" ")) {
1998
2178
  merged[merged.length - 1] = prev + curr;
2179
+ } else if (curr.trim().length <= 3 && /^[)\]%}]/.test(curr.trim())) {
2180
+ merged[merged.length - 1] = prev + curr.trim();
2181
+ } else if (/[,(]$/.test(prev.trim()) && curr.trim().length <= 15) {
2182
+ merged[merged.length - 1] = prev + curr.trim();
2183
+ } else if (/[\d,]$/.test(prev) && /^[\d,]+[)\]]?$/.test(curr.trim()) && curr.trim().length <= 10) {
2184
+ merged[merged.length - 1] = prev + curr.trim();
1999
2185
  } else {
2000
2186
  merged.push(curr);
2001
2187
  }
@@ -2208,21 +2394,26 @@ async function loadPdfWithTimeout(buffer) {
2208
2394
  disableFontFace: true,
2209
2395
  isEvalSupported: false
2210
2396
  });
2211
- return Promise.race([
2212
- loadingTask.promise,
2213
- new Promise(
2214
- (_, reject) => setTimeout(() => {
2215
- loadingTask.destroy();
2216
- reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
2217
- }, PDF_LOAD_TIMEOUT_MS)
2218
- )
2219
- ]);
2397
+ let timer;
2398
+ try {
2399
+ return await Promise.race([
2400
+ loadingTask.promise,
2401
+ new Promise((_, reject) => {
2402
+ timer = setTimeout(() => {
2403
+ loadingTask.destroy();
2404
+ reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
2405
+ }, PDF_LOAD_TIMEOUT_MS);
2406
+ })
2407
+ ]);
2408
+ } finally {
2409
+ if (timer !== void 0) clearTimeout(timer);
2410
+ }
2220
2411
  }
2221
2412
  async function parsePdfDocument(buffer, options) {
2222
2413
  const doc = await loadPdfWithTimeout(buffer);
2223
2414
  try {
2224
2415
  const pageCount = doc.numPages;
2225
- if (pageCount === 0) return { success: false, fileType: "pdf", pageCount: 0, error: "PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", code: "PARSE_ERROR" };
2416
+ if (pageCount === 0) throw new KordocError("PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
2226
2417
  const metadata = { pageCount };
2227
2418
  await extractPdfMetadata(doc, metadata);
2228
2419
  const blocks = [];
@@ -2275,14 +2466,14 @@ async function parsePdfDocument(buffer, options) {
2275
2466
  const ocrBlocks = await ocrPages2(doc, options.ocr, pageFilter, effectivePageCount);
2276
2467
  if (ocrBlocks.length > 0) {
2277
2468
  const ocrMarkdown = ocrBlocks.map((b) => b.text || "").filter(Boolean).join("\n\n");
2278
- return { success: true, fileType: "pdf", markdown: ocrMarkdown, pageCount: parsedPageCount, blocks: ocrBlocks, metadata, isImageBased: true, warnings };
2469
+ return { markdown: ocrMarkdown, blocks: ocrBlocks, metadata, warnings, isImageBased: true };
2279
2470
  }
2280
2471
  } catch {
2281
2472
  }
2282
2473
  }
2283
- return { success: false, fileType: "pdf", pageCount, isImageBased: true, error: `\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (${pageCount}\uD398\uC774\uC9C0, ${totalChars}\uC790)`, code: "IMAGE_BASED_PDF" };
2474
+ throw Object.assign(new KordocError(`\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (${pageCount}\uD398\uC774\uC9C0, ${totalChars}\uC790)`), { isImageBased: true });
2284
2475
  }
2285
- if (options?.removeHeaderFooter && parsedPageCount >= 3) {
2476
+ if (options?.removeHeaderFooter !== false && parsedPageCount >= 3) {
2286
2477
  const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
2287
2478
  for (let ri = removed.length - 1; ri >= 0; ri--) {
2288
2479
  blocks.splice(removed[ri], 1);
@@ -2292,9 +2483,10 @@ async function parsePdfDocument(buffer, options) {
2292
2483
  if (medianFontSize > 0) {
2293
2484
  detectHeadings(blocks, medianFontSize);
2294
2485
  }
2486
+ detectMarkerHeadings(blocks);
2295
2487
  const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
2296
2488
  let markdown = cleanPdfText(blocksToMarkdown(blocks));
2297
- return { success: true, fileType: "pdf", markdown, pageCount: parsedPageCount, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
2489
+ return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
2298
2490
  } finally {
2299
2491
  await doc.destroy().catch(() => {
2300
2492
  });
@@ -2354,12 +2546,67 @@ function detectHeadings(blocks, medianFontSize) {
2354
2546
  if (/^\d+$/.test(text)) continue;
2355
2547
  const ratio = block.style.fontSize / medianFontSize;
2356
2548
  let level = 0;
2357
- if (ratio >= 1.5) level = 1;
2358
- else if (ratio >= 1.3) level = 2;
2359
- else if (ratio >= 1.15) level = 3;
2549
+ if (ratio >= HEADING_RATIO_H1) level = 1;
2550
+ else if (ratio >= HEADING_RATIO_H2) level = 2;
2551
+ else if (ratio >= HEADING_RATIO_H3) level = 3;
2360
2552
  if (level > 0) {
2361
2553
  block.type = "heading";
2362
2554
  block.level = level;
2555
+ block.text = collapseEvenSpacing(text);
2556
+ }
2557
+ }
2558
+ }
2559
+ function collapseEvenSpacing(text) {
2560
+ const tokens = text.split(" ");
2561
+ const singleCharCount = tokens.filter((t) => t.length === 1).length;
2562
+ if (tokens.length >= 3 && singleCharCount / tokens.length >= 0.7) {
2563
+ return tokens.join("");
2564
+ }
2565
+ return text;
2566
+ }
2567
+ function shouldDemoteTable(table) {
2568
+ const allCells = table.cells.flatMap((row) => row.map((c) => c.text.trim())).filter(Boolean);
2569
+ const allText = allCells.join(" ");
2570
+ if (allText.length > 200) return false;
2571
+ if (/[□■◆○●▶]/.test(allText) && table.rows <= 3) return true;
2572
+ const totalCells = table.rows * table.cols;
2573
+ const emptyCells = totalCells - allCells.length;
2574
+ if (table.rows <= 2 && emptyCells > totalCells * 0.5) return true;
2575
+ if (table.rows === 1 && !/\d{2,}/.test(allText)) return true;
2576
+ return false;
2577
+ }
2578
+ function demoteTableToText(table) {
2579
+ const lines = [];
2580
+ for (let r = 0; r < table.rows; r++) {
2581
+ const cells = table.cells[r].map((c) => c.text.trim()).filter(Boolean);
2582
+ if (cells.length === 0) continue;
2583
+ if (table.cols === 2 && cells.length === 2) {
2584
+ lines.push(`${cells[0]} : ${cells[1]}`);
2585
+ } else {
2586
+ lines.push(cells.join(" "));
2587
+ }
2588
+ }
2589
+ return lines.join("\n");
2590
+ }
2591
+ function detectMarkerHeadings(blocks) {
2592
+ for (let i = 0; i < blocks.length; i++) {
2593
+ const block = blocks[i];
2594
+ if (block.type !== "paragraph" || !block.text) continue;
2595
+ const text = block.text.trim();
2596
+ if (text.length < 50 && /^[□■◆◇▶]\s*[가-힣]/.test(text)) {
2597
+ block.type = "heading";
2598
+ block.level = 4;
2599
+ continue;
2600
+ }
2601
+ if (/^[가-힣]{2,6}$/.test(text) && block.style?.fontSize) {
2602
+ const prev = blocks[i - 1];
2603
+ const next = blocks[i + 1];
2604
+ const prevIsStructural = !prev || prev.type === "table" || prev.type === "heading" || prev.type === "separator";
2605
+ const nextIsStructural = !next || next.type === "table" || next.type === "heading" || next.type === "paragraph" && next.text && /^[□■◆○●]/.test(next.text.trim());
2606
+ if (prevIsStructural || nextIsStructural) {
2607
+ block.type = "heading";
2608
+ block.level = 3;
2609
+ }
2363
2610
  }
2364
2611
  }
2365
2612
  }
@@ -2396,7 +2643,7 @@ function computeRegion(items) {
2396
2643
  }
2397
2644
  return { items, minX, minY, maxX, maxY };
2398
2645
  }
2399
- function findYSplit(items, region, gapThreshold) {
2646
+ function findYSplit(items, _region, gapThreshold) {
2400
2647
  const sorted = [...items].sort((a, b) => b.y - a.y);
2401
2648
  let bestGap = gapThreshold;
2402
2649
  let bestSplit = null;
@@ -2411,7 +2658,7 @@ function findYSplit(items, region, gapThreshold) {
2411
2658
  }
2412
2659
  return bestSplit;
2413
2660
  }
2414
- function findXSplit(items, region, gapThreshold) {
2661
+ function findXSplit(items, _region, gapThreshold) {
2415
2662
  const sorted = [...items].sort((a, b) => a.x - b.x);
2416
2663
  let bestGap = gapThreshold;
2417
2664
  let bestSplit = null;
@@ -2470,7 +2717,8 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
2470
2717
  );
2471
2718
  for (const cell of cells) {
2472
2719
  const cellItems = cellTextMap.get(cell) || [];
2473
- const text = cellTextToString(cellItems);
2720
+ let text = cellTextToString(cellItems);
2721
+ text = text.replace(/^[\s]*[-–—]\s*\d+\s*[-–—][\s]*$/gm, "").trim();
2474
2722
  irGrid[cell.row][cell.col] = {
2475
2723
  text,
2476
2724
  colSpan: cell.colSpan,
@@ -2485,18 +2733,21 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
2485
2733
  };
2486
2734
  const hasContent = irGrid.some((row) => row.some((cell) => cell.text.trim() !== ""));
2487
2735
  if (!hasContent) continue;
2488
- blocks.push({
2489
- type: "table",
2490
- table: irTable,
2491
- pageNumber: pageNum,
2492
- bbox: {
2493
- page: pageNum,
2494
- x: grid.bbox.x1,
2495
- y: grid.bbox.y1,
2496
- width: grid.bbox.x2 - grid.bbox.x1,
2497
- height: grid.bbox.y2 - grid.bbox.y1
2736
+ const tableBbox = {
2737
+ page: pageNum,
2738
+ x: grid.bbox.x1,
2739
+ y: grid.bbox.y1,
2740
+ width: grid.bbox.x2 - grid.bbox.x1,
2741
+ height: grid.bbox.y2 - grid.bbox.y1
2742
+ };
2743
+ if (shouldDemoteTable(irTable)) {
2744
+ const demoted = demoteTableToText(irTable);
2745
+ if (demoted) {
2746
+ blocks.push({ type: "paragraph", text: demoted, pageNumber: pageNum, bbox: tableBbox, style: dominantStyle(tableItems) });
2498
2747
  }
2499
- });
2748
+ continue;
2749
+ }
2750
+ blocks.push({ type: "table", table: irTable, pageNumber: pageNum, bbox: tableBbox });
2500
2751
  }
2501
2752
  const remaining = items.filter((i) => !usedItems.has(i));
2502
2753
  if (remaining.length > 0) {
@@ -2508,9 +2759,29 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
2508
2759
  const by = b.bbox ? b.bbox.y + b.bbox.height : 0;
2509
2760
  return by - ay;
2510
2761
  });
2511
- return allBlocks;
2762
+ return mergeAdjacentTableBlocks(allBlocks);
2512
2763
  }
2513
- return blocks;
2764
+ return mergeAdjacentTableBlocks(blocks);
2765
+ }
2766
+ function mergeAdjacentTableBlocks(blocks) {
2767
+ if (blocks.length <= 1) return blocks;
2768
+ const result = [blocks[0]];
2769
+ for (let i = 1; i < blocks.length; i++) {
2770
+ const prev = result[result.length - 1];
2771
+ const curr = blocks[i];
2772
+ if (prev.type === "table" && curr.type === "table" && prev.table && curr.table && prev.table.cols === curr.table.cols) {
2773
+ const merged = {
2774
+ rows: prev.table.rows + curr.table.rows,
2775
+ cols: prev.table.cols,
2776
+ cells: [...prev.table.cells, ...curr.table.cells],
2777
+ hasHeader: prev.table.hasHeader
2778
+ };
2779
+ result[result.length - 1] = { ...prev, table: merged };
2780
+ } else {
2781
+ result.push(curr);
2782
+ }
2783
+ }
2784
+ return result;
2514
2785
  }
2515
2786
  function extractPageBlocksFallback(items, pageNum) {
2516
2787
  if (items.length === 0) return [];
@@ -2533,11 +2804,13 @@ function extractPageBlocksFallback(items, pageNum) {
2533
2804
  }));
2534
2805
  const clusterResults = detectClusterTables(clusterItems, pageNum);
2535
2806
  if (clusterResults.length > 0) {
2807
+ const ciToIdx = /* @__PURE__ */ new Map();
2808
+ for (let ci = 0; ci < clusterItems.length; ci++) ciToIdx.set(clusterItems[ci], ci);
2536
2809
  const usedIndices = /* @__PURE__ */ new Set();
2537
2810
  for (const cr of clusterResults) {
2538
2811
  for (const ci of cr.usedItems) {
2539
- const idx = clusterItems.indexOf(ci);
2540
- if (idx >= 0) usedIndices.add(idx);
2812
+ const idx = ciToIdx.get(ci);
2813
+ if (idx !== void 0) usedIndices.add(idx);
2541
2814
  }
2542
2815
  blocks.push({ type: "table", table: cr.table, pageNumber: pageNum, bbox: cr.bbox });
2543
2816
  }
@@ -2848,7 +3121,8 @@ function mergeLineSimple(items) {
2848
3121
  const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
2849
3122
  const avgFs = (sorted[i].fontSize + sorted[i - 1].fontSize) / 2;
2850
3123
  if (gap > 15) result += " ";
2851
- else if (gap < avgFs * 0.3 && /[가-힣]$/.test(result) && /^[가-힣]/.test(sorted[i].text)) {
3124
+ else if (gap < avgFs * 0.15) {
3125
+ } else if (gap < avgFs * 0.35 && (/[가-힣]$/.test(result) || /^[가-힣]/.test(sorted[i].text))) {
2852
3126
  } else if (gap > 3) result += " ";
2853
3127
  result += sorted[i].text;
2854
3128
  }
@@ -2856,8 +3130,8 @@ function mergeLineSimple(items) {
2856
3130
  }
2857
3131
  function cleanPdfText(text) {
2858
3132
  return mergeKoreanLines(
2859
- text.replace(/^[\s]*[-–—]\s*\d+\s*[-–—][\s]*$/gm, "").replace(/^\s*\d+\s*\/\s*\d+\s*$/gm, "")
2860
- ).replace(/\n{3,}/g, "\n\n").trim();
3133
+ text.replace(/^[\s]*[-–—]\s*[-–—]?\d+[-–—]?[\s]*[-–—]?[\s]*$/gm, "").replace(/^\s*\d+\s*\/\s*\d+\s*$/gm, "").replace(/\n\d{1,4}\n/g, "\n").replace(/\n\d{1,4}$/, "")
3134
+ ).replace(/^(?!\|).{3,30}$/gm, (line) => collapseEvenSpacing(line)).replace(/\n{3,}/g, "\n\n").trim();
2861
3135
  }
2862
3136
  function startsWithMarker(line) {
2863
3137
  const t = line.trimStart();
@@ -2871,15 +3145,13 @@ function detectListBlocks(blocks) {
2871
3145
  for (let i = 0; i < blocks.length; i++) {
2872
3146
  const block = blocks[i];
2873
3147
  if (block.type === "paragraph" && block.text) {
2874
- const match = block.text.match(/^(\d+)\.\s/);
2875
- if (match) {
2876
- result.push({
2877
- ...block,
2878
- type: "list",
2879
- listType: "ordered",
2880
- // 원래 번호를 text에 보존 (blocksToMarkdown에서 그대로 출력)
2881
- text: block.text
2882
- });
3148
+ const text = block.text.trim();
3149
+ if (/^\d+\.\s/.test(text)) {
3150
+ result.push({ ...block, type: "list", listType: "ordered", text: block.text });
3151
+ continue;
3152
+ }
3153
+ if (/^[○●·※▶▷◆◇\-]\s/.test(text)) {
3154
+ result.push({ ...block, type: "list", listType: "unordered", text: block.text });
2883
3155
  continue;
2884
3156
  }
2885
3157
  }
@@ -3038,11 +3310,20 @@ function mergeKoreanLines(text) {
3038
3310
  for (let i = 1; i < lines.length; i++) {
3039
3311
  const prev = result[result.length - 1];
3040
3312
  const curr = lines[i];
3041
- if (/^#{1,6}\s/.test(prev) || /^#{1,6}\s/.test(curr)) {
3313
+ const currTrimmed = curr.trim();
3314
+ if (/^#{1,6}\s/.test(prev) || /^#{1,6}\s/.test(curr) || /^\|/.test(currTrimmed) || /^---/.test(currTrimmed)) {
3042
3315
  result.push(curr);
3043
3316
  continue;
3044
3317
  }
3045
- if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !curr.trimStart().startsWith("|") && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
3318
+ if (/,$/.test(prev.trim()) && currTrimmed.length > 0) {
3319
+ result[result.length - 1] = prev + "\n" + curr;
3320
+ continue;
3321
+ }
3322
+ if (/^\(※/.test(currTrimmed)) {
3323
+ result[result.length - 1] = prev + " " + currTrimmed;
3324
+ continue;
3325
+ }
3326
+ if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
3046
3327
  result[result.length - 1] = prev + " " + curr;
3047
3328
  } else {
3048
3329
  result.push(curr);
@@ -3051,6 +3332,716 @@ function mergeKoreanLines(text) {
3051
3332
  return result.join("\n");
3052
3333
  }
3053
3334
 
3335
+ // src/xlsx/parser.ts
3336
+ var import_jszip3 = __toESM(require("jszip"), 1);
3337
+ var import_xmldom2 = require("@xmldom/xmldom");
3338
+ var MAX_SHEETS = 100;
3339
+ var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
3340
+ var MAX_ROWS2 = 1e4;
3341
+ var MAX_COLS2 = 200;
3342
+ function cleanNumericValue(raw) {
3343
+ if (!/^-?\d+\.\d+$/.test(raw)) return raw;
3344
+ const num = parseFloat(raw);
3345
+ if (!isFinite(num)) return raw;
3346
+ const cleaned = parseFloat(num.toPrecision(15)).toString();
3347
+ return cleaned;
3348
+ }
3349
+ function parseCellRef(ref) {
3350
+ const m = ref.match(/^([A-Z]+)(\d+)$/);
3351
+ if (!m) return null;
3352
+ let col = 0;
3353
+ for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64);
3354
+ return { col: col - 1, row: parseInt(m[2], 10) - 1 };
3355
+ }
3356
+ function parseMergeRef(ref) {
3357
+ const parts = ref.split(":");
3358
+ if (parts.length !== 2) return null;
3359
+ const start = parseCellRef(parts[0]);
3360
+ const end = parseCellRef(parts[1]);
3361
+ if (!start || !end) return null;
3362
+ return { startCol: start.col, startRow: start.row, endCol: end.col, endRow: end.row };
3363
+ }
3364
+ function getElements(parent, tagName) {
3365
+ const nodes = parent.getElementsByTagName(tagName);
3366
+ const result = [];
3367
+ for (let i = 0; i < nodes.length; i++) result.push(nodes[i]);
3368
+ return result;
3369
+ }
3370
+ function getTextContent(el) {
3371
+ return el.textContent?.trim() ?? "";
3372
+ }
3373
+ function parseXml(text) {
3374
+ return new import_xmldom2.DOMParser().parseFromString(text, "text/xml");
3375
+ }
3376
+ function parseSharedStrings(xml) {
3377
+ const doc = parseXml(xml);
3378
+ const strings = [];
3379
+ const siList = getElements(doc.documentElement, "si");
3380
+ for (const si of siList) {
3381
+ const tElements = getElements(si, "t");
3382
+ strings.push(tElements.map((t) => t.textContent ?? "").join(""));
3383
+ }
3384
+ return strings;
3385
+ }
3386
+ function parseWorkbook(xml) {
3387
+ const doc = parseXml(xml);
3388
+ const sheets = [];
3389
+ const sheetElements = getElements(doc.documentElement, "sheet");
3390
+ for (const el of sheetElements) {
3391
+ sheets.push({
3392
+ name: el.getAttribute("name") ?? `Sheet${sheets.length + 1}`,
3393
+ sheetId: el.getAttribute("sheetId") ?? "",
3394
+ rId: el.getAttribute("r:id") ?? ""
3395
+ });
3396
+ }
3397
+ return sheets;
3398
+ }
3399
+ function parseRels(xml) {
3400
+ const doc = parseXml(xml);
3401
+ const map = /* @__PURE__ */ new Map();
3402
+ const rels = getElements(doc.documentElement, "Relationship");
3403
+ for (const rel of rels) {
3404
+ const id = rel.getAttribute("Id");
3405
+ const target = rel.getAttribute("Target");
3406
+ if (id && target) map.set(id, target);
3407
+ }
3408
+ return map;
3409
+ }
3410
+ function parseWorksheet(xml, sharedStrings) {
3411
+ const doc = parseXml(xml);
3412
+ const grid = [];
3413
+ let maxRow = 0;
3414
+ let maxCol = 0;
3415
+ const rows = getElements(doc.documentElement, "row");
3416
+ for (const rowEl of rows) {
3417
+ const rowNum = parseInt(rowEl.getAttribute("r") ?? "0", 10) - 1;
3418
+ if (rowNum < 0 || rowNum >= MAX_ROWS2) continue;
3419
+ const cells = getElements(rowEl, "c");
3420
+ for (const cellEl of cells) {
3421
+ const ref = cellEl.getAttribute("r");
3422
+ if (!ref) continue;
3423
+ const pos = parseCellRef(ref);
3424
+ if (!pos || pos.col >= MAX_COLS2) continue;
3425
+ const type = cellEl.getAttribute("t");
3426
+ const vElements = getElements(cellEl, "v");
3427
+ const fElements = getElements(cellEl, "f");
3428
+ let value = "";
3429
+ if (vElements.length > 0) {
3430
+ const raw = getTextContent(vElements[0]);
3431
+ if (type === "s") {
3432
+ const idx = parseInt(raw, 10);
3433
+ value = sharedStrings[idx] ?? "";
3434
+ } else if (type === "b") {
3435
+ value = raw === "1" ? "TRUE" : "FALSE";
3436
+ } else {
3437
+ value = cleanNumericValue(raw);
3438
+ }
3439
+ } else if (type === "inlineStr") {
3440
+ const isEl = getElements(cellEl, "is");
3441
+ if (isEl.length > 0) {
3442
+ const tElements = getElements(isEl[0], "t");
3443
+ value = tElements.map((t) => t.textContent ?? "").join("");
3444
+ }
3445
+ }
3446
+ if (!value && fElements.length > 0) {
3447
+ value = `=${getTextContent(fElements[0])}`;
3448
+ }
3449
+ while (grid.length <= pos.row) grid.push([]);
3450
+ while (grid[pos.row].length <= pos.col) grid[pos.row].push("");
3451
+ grid[pos.row][pos.col] = value;
3452
+ if (pos.row > maxRow) maxRow = pos.row;
3453
+ if (pos.col > maxCol) maxCol = pos.col;
3454
+ }
3455
+ }
3456
+ const merges = [];
3457
+ const mergeCellElements = getElements(doc.documentElement, "mergeCell");
3458
+ for (const el of mergeCellElements) {
3459
+ const ref = el.getAttribute("ref");
3460
+ if (!ref) continue;
3461
+ const m = parseMergeRef(ref);
3462
+ if (m) merges.push(m);
3463
+ }
3464
+ return { grid, merges, maxRow, maxCol };
3465
+ }
3466
+ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex) {
3467
+ const blocks = [];
3468
+ if (sheetName) {
3469
+ blocks.push({
3470
+ type: "heading",
3471
+ text: sheetName,
3472
+ level: 2,
3473
+ pageNumber: sheetIndex + 1
3474
+ });
3475
+ }
3476
+ if (maxRow < 0 || maxCol < 0 || grid.length === 0) return blocks;
3477
+ const mergeMap = /* @__PURE__ */ new Map();
3478
+ const mergeSkip = /* @__PURE__ */ new Set();
3479
+ for (const m of merges) {
3480
+ const colSpan = m.endCol - m.startCol + 1;
3481
+ const rowSpan = m.endRow - m.startRow + 1;
3482
+ mergeMap.set(`${m.startRow},${m.startCol}`, { colSpan, rowSpan });
3483
+ for (let r = m.startRow; r <= m.endRow; r++) {
3484
+ for (let c = m.startCol; c <= m.endCol; c++) {
3485
+ if (r !== m.startRow || c !== m.startCol) {
3486
+ mergeSkip.add(`${r},${c}`);
3487
+ }
3488
+ }
3489
+ }
3490
+ }
3491
+ let firstRow = -1;
3492
+ let lastRow = -1;
3493
+ for (let r = 0; r <= maxRow; r++) {
3494
+ const row = grid[r];
3495
+ if (row && row.some((cell) => cell !== "")) {
3496
+ if (firstRow === -1) firstRow = r;
3497
+ lastRow = r;
3498
+ }
3499
+ }
3500
+ if (firstRow === -1) return blocks;
3501
+ const cellRows = [];
3502
+ for (let r = firstRow; r <= lastRow; r++) {
3503
+ const row = [];
3504
+ for (let c = 0; c <= maxCol; c++) {
3505
+ const key = `${r},${c}`;
3506
+ if (mergeSkip.has(key)) continue;
3507
+ const text = (grid[r] && grid[r][c]) ?? "";
3508
+ const merge = mergeMap.get(key);
3509
+ row.push({
3510
+ text,
3511
+ colSpan: merge?.colSpan ?? 1,
3512
+ rowSpan: merge?.rowSpan ?? 1
3513
+ });
3514
+ }
3515
+ cellRows.push(row);
3516
+ }
3517
+ if (cellRows.length > 0) {
3518
+ const table = buildTable(cellRows);
3519
+ if (table.rows > 0) {
3520
+ blocks.push({ type: "table", table, pageNumber: sheetIndex + 1 });
3521
+ }
3522
+ }
3523
+ return blocks;
3524
+ }
3525
+ async function parseXlsxDocument(buffer, options) {
3526
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE3);
3527
+ const zip = await import_jszip3.default.loadAsync(buffer);
3528
+ const warnings = [];
3529
+ const workbookFile = zip.file("xl/workbook.xml");
3530
+ if (!workbookFile) {
3531
+ throw new KordocError("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 XLSX \uD30C\uC77C: xl/workbook.xml\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
3532
+ }
3533
+ let sharedStrings = [];
3534
+ const ssFile = zip.file("xl/sharedStrings.xml");
3535
+ if (ssFile) {
3536
+ sharedStrings = parseSharedStrings(await ssFile.async("text"));
3537
+ }
3538
+ const sheets = parseWorkbook(await workbookFile.async("text"));
3539
+ if (sheets.length === 0) {
3540
+ throw new KordocError("XLSX \uD30C\uC77C\uC5D0 \uC2DC\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4");
3541
+ }
3542
+ let relsMap = /* @__PURE__ */ new Map();
3543
+ const relsFile = zip.file("xl/_rels/workbook.xml.rels");
3544
+ if (relsFile) {
3545
+ relsMap = parseRels(await relsFile.async("text"));
3546
+ }
3547
+ let pageFilter = null;
3548
+ if (options?.pages) {
3549
+ const { parsePageRange: parsePageRange2 } = await Promise.resolve().then(() => (init_page_range(), page_range_exports));
3550
+ pageFilter = parsePageRange2(options.pages, sheets.length);
3551
+ }
3552
+ const blocks = [];
3553
+ const processedSheets = Math.min(sheets.length, MAX_SHEETS);
3554
+ for (let i = 0; i < processedSheets; i++) {
3555
+ if (pageFilter && !pageFilter.has(i + 1)) continue;
3556
+ const sheet = sheets[i];
3557
+ options?.onProgress?.(i + 1, processedSheets);
3558
+ let sheetPath = relsMap.get(sheet.rId);
3559
+ if (sheetPath) {
3560
+ if (!sheetPath.startsWith("xl/") && !sheetPath.startsWith("/")) {
3561
+ sheetPath = `xl/${sheetPath}`;
3562
+ } else if (sheetPath.startsWith("/")) {
3563
+ sheetPath = sheetPath.slice(1);
3564
+ }
3565
+ } else {
3566
+ sheetPath = `xl/worksheets/sheet${i + 1}.xml`;
3567
+ }
3568
+ const sheetFile = zip.file(sheetPath);
3569
+ if (!sheetFile) {
3570
+ warnings.push({
3571
+ page: i + 1,
3572
+ message: `\uC2DC\uD2B8 "${sheet.name}" \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${sheetPath}`,
3573
+ code: "PARTIAL_PARSE"
3574
+ });
3575
+ continue;
3576
+ }
3577
+ try {
3578
+ const sheetXml = await sheetFile.async("text");
3579
+ const { grid, merges, maxRow, maxCol } = parseWorksheet(sheetXml, sharedStrings);
3580
+ const sheetBlocks = sheetToBlocks(sheet.name, grid, merges, maxRow, maxCol, i);
3581
+ blocks.push(...sheetBlocks);
3582
+ } catch (err) {
3583
+ warnings.push({
3584
+ page: i + 1,
3585
+ message: `\uC2DC\uD2B8 "${sheet.name}" \uD30C\uC2F1 \uC2E4\uD328: ${err instanceof Error ? err.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`,
3586
+ code: "PARTIAL_PARSE"
3587
+ });
3588
+ }
3589
+ }
3590
+ const metadata = {
3591
+ pageCount: processedSheets
3592
+ };
3593
+ const coreFile = zip.file("docProps/core.xml");
3594
+ if (coreFile) {
3595
+ try {
3596
+ const coreXml = await coreFile.async("text");
3597
+ const doc = parseXml(coreXml);
3598
+ const getFirst = (tag) => {
3599
+ const els = doc.getElementsByTagName(tag);
3600
+ return els.length > 0 ? (els[0].textContent ?? "").trim() : void 0;
3601
+ };
3602
+ metadata.title = getFirst("dc:title") || getFirst("dcterms:title");
3603
+ metadata.author = getFirst("dc:creator");
3604
+ metadata.description = getFirst("dc:description");
3605
+ const created = getFirst("dcterms:created");
3606
+ if (created) metadata.createdAt = created;
3607
+ const modified = getFirst("dcterms:modified");
3608
+ if (modified) metadata.modifiedAt = modified;
3609
+ } catch {
3610
+ }
3611
+ }
3612
+ const markdown = blocksToMarkdown(blocks);
3613
+ return { markdown, blocks, metadata, warnings: warnings.length > 0 ? warnings : void 0 };
3614
+ }
3615
+
3616
+ // src/docx/parser.ts
3617
+ var import_jszip4 = __toESM(require("jszip"), 1);
3618
+ var import_xmldom3 = require("@xmldom/xmldom");
3619
+ var MAX_DECOMPRESS_SIZE4 = 100 * 1024 * 1024;
3620
+ function getChildElements(parent, localName) {
3621
+ const result = [];
3622
+ const children = parent.childNodes;
3623
+ for (let i = 0; i < children.length; i++) {
3624
+ const node = children[i];
3625
+ if (node.nodeType === 1) {
3626
+ const el = node;
3627
+ if (el.localName === localName || el.tagName?.endsWith(`:${localName}`)) {
3628
+ result.push(el);
3629
+ }
3630
+ }
3631
+ }
3632
+ return result;
3633
+ }
3634
+ function findElements(parent, localName) {
3635
+ const result = [];
3636
+ const walk = (node) => {
3637
+ const children = node.childNodes;
3638
+ for (let i = 0; i < children.length; i++) {
3639
+ const child = children[i];
3640
+ if (child.nodeType === 1) {
3641
+ const el = child;
3642
+ if (el.localName === localName || el.tagName?.endsWith(`:${localName}`)) {
3643
+ result.push(el);
3644
+ }
3645
+ walk(el);
3646
+ }
3647
+ }
3648
+ };
3649
+ walk(parent);
3650
+ return result;
3651
+ }
3652
+ function getAttr(el, localName) {
3653
+ const attrs = el.attributes;
3654
+ for (let i = 0; i < attrs.length; i++) {
3655
+ const attr = attrs[i];
3656
+ if (attr.localName === localName || attr.name === localName) return attr.value;
3657
+ }
3658
+ return null;
3659
+ }
3660
+ function parseXml2(text) {
3661
+ return new import_xmldom3.DOMParser().parseFromString(text, "text/xml");
3662
+ }
3663
+ function parseStyles(xml) {
3664
+ const doc = parseXml2(xml);
3665
+ const styles = /* @__PURE__ */ new Map();
3666
+ const styleElements = findElements(doc, "style");
3667
+ for (const el of styleElements) {
3668
+ const styleId = getAttr(el, "styleId");
3669
+ if (!styleId) continue;
3670
+ const nameEls = getChildElements(el, "name");
3671
+ const name = nameEls.length > 0 ? getAttr(nameEls[0], "val") ?? "" : "";
3672
+ const basedOnEls = getChildElements(el, "basedOn");
3673
+ const basedOn = basedOnEls.length > 0 ? getAttr(basedOnEls[0], "val") ?? void 0 : void 0;
3674
+ const pPrEls = getChildElements(el, "pPr");
3675
+ let outlineLevel;
3676
+ if (pPrEls.length > 0) {
3677
+ const outlineEls = getChildElements(pPrEls[0], "outlineLvl");
3678
+ if (outlineEls.length > 0) {
3679
+ const val = getAttr(outlineEls[0], "val");
3680
+ if (val !== null) outlineLevel = parseInt(val, 10);
3681
+ }
3682
+ }
3683
+ if (outlineLevel === void 0) {
3684
+ const headingMatch = name.match(/^(?:heading|Heading)\s*(\d+)$/i);
3685
+ if (headingMatch) outlineLevel = parseInt(headingMatch[1], 10) - 1;
3686
+ }
3687
+ styles.set(styleId, { name, basedOn, outlineLevel });
3688
+ }
3689
+ return styles;
3690
+ }
3691
+ function parseNumbering(xml) {
3692
+ const doc = parseXml2(xml);
3693
+ const abstractNums = /* @__PURE__ */ new Map();
3694
+ const abstractElements = findElements(doc, "abstractNum");
3695
+ for (const el of abstractElements) {
3696
+ const abstractNumId = getAttr(el, "abstractNumId");
3697
+ if (!abstractNumId) continue;
3698
+ const levels = /* @__PURE__ */ new Map();
3699
+ const lvlElements = getChildElements(el, "lvl");
3700
+ for (const lvl of lvlElements) {
3701
+ const ilvl = parseInt(getAttr(lvl, "ilvl") ?? "0", 10);
3702
+ const numFmtEls = getChildElements(lvl, "numFmt");
3703
+ const numFmt = numFmtEls.length > 0 ? getAttr(numFmtEls[0], "val") ?? "bullet" : "bullet";
3704
+ levels.set(ilvl, { numFmt, level: ilvl });
3705
+ }
3706
+ abstractNums.set(abstractNumId, levels);
3707
+ }
3708
+ const nums = /* @__PURE__ */ new Map();
3709
+ const numElements = findElements(doc, "num");
3710
+ for (const el of numElements) {
3711
+ const numId = getAttr(el, "numId");
3712
+ if (!numId) continue;
3713
+ const abstractRefs = getChildElements(el, "abstractNumId");
3714
+ if (abstractRefs.length > 0) {
3715
+ const ref = getAttr(abstractRefs[0], "val");
3716
+ if (ref && abstractNums.has(ref)) {
3717
+ nums.set(numId, abstractNums.get(ref));
3718
+ }
3719
+ }
3720
+ }
3721
+ return nums;
3722
+ }
3723
+ function parseRels2(xml) {
3724
+ const doc = parseXml2(xml);
3725
+ const map = /* @__PURE__ */ new Map();
3726
+ const rels = findElements(doc, "Relationship");
3727
+ for (const rel of rels) {
3728
+ const id = getAttr(rel, "Id");
3729
+ const target = getAttr(rel, "Target");
3730
+ if (id && target) map.set(id, target);
3731
+ }
3732
+ return map;
3733
+ }
3734
+ function parseFootnotes(xml) {
3735
+ const doc = parseXml2(xml);
3736
+ const notes = /* @__PURE__ */ new Map();
3737
+ const fnElements = findElements(doc, "footnote");
3738
+ for (const fn of fnElements) {
3739
+ const id = getAttr(fn, "id");
3740
+ if (!id || id === "0" || id === "-1") continue;
3741
+ const texts = [];
3742
+ const pElements = findElements(fn, "p");
3743
+ for (const p of pElements) {
3744
+ const runs = findElements(p, "r");
3745
+ for (const r of runs) {
3746
+ const tElements = getChildElements(r, "t");
3747
+ for (const t of tElements) texts.push(t.textContent ?? "");
3748
+ }
3749
+ }
3750
+ notes.set(id, texts.join("").trim());
3751
+ }
3752
+ return notes;
3753
+ }
3754
+ function extractRun(r) {
3755
+ const tElements = getChildElements(r, "t");
3756
+ const text = tElements.map((t) => t.textContent ?? "").join("");
3757
+ let bold = false;
3758
+ let italic = false;
3759
+ const rPrEls = getChildElements(r, "rPr");
3760
+ if (rPrEls.length > 0) {
3761
+ bold = getChildElements(rPrEls[0], "b").length > 0;
3762
+ italic = getChildElements(rPrEls[0], "i").length > 0;
3763
+ }
3764
+ return { text, bold, italic };
3765
+ }
3766
+ function parseParagraph(p, styles, numbering, footnotes, rels) {
3767
+ const pPrEls = getChildElements(p, "pPr");
3768
+ let styleId = "";
3769
+ let numId = "";
3770
+ let ilvl = 0;
3771
+ if (pPrEls.length > 0) {
3772
+ const pStyleEls = getChildElements(pPrEls[0], "pStyle");
3773
+ if (pStyleEls.length > 0) styleId = getAttr(pStyleEls[0], "val") ?? "";
3774
+ const numPrEls = getChildElements(pPrEls[0], "numPr");
3775
+ if (numPrEls.length > 0) {
3776
+ const numIdEls = getChildElements(numPrEls[0], "numId");
3777
+ const ilvlEls = getChildElements(numPrEls[0], "ilvl");
3778
+ numId = numIdEls.length > 0 ? getAttr(numIdEls[0], "val") ?? "" : "";
3779
+ ilvl = ilvlEls.length > 0 ? parseInt(getAttr(ilvlEls[0], "val") ?? "0", 10) : 0;
3780
+ }
3781
+ }
3782
+ const parts = [];
3783
+ let hasBold = false;
3784
+ let hasItalic = false;
3785
+ let href;
3786
+ let footnoteText;
3787
+ const hyperlinks = getChildElements(p, "hyperlink");
3788
+ const hyperlinkTexts = /* @__PURE__ */ new Set();
3789
+ for (const hl of hyperlinks) {
3790
+ const rId = getAttr(hl, "id");
3791
+ const hlText = [];
3792
+ const runs2 = findElements(hl, "r");
3793
+ for (const r of runs2) {
3794
+ const result = extractRun(r);
3795
+ hlText.push(result.text);
3796
+ }
3797
+ const text2 = hlText.join("");
3798
+ if (text2) {
3799
+ hyperlinkTexts.add(text2);
3800
+ if (rId && rels.has(rId)) {
3801
+ href = rels.get(rId);
3802
+ parts.push(text2);
3803
+ } else {
3804
+ parts.push(text2);
3805
+ }
3806
+ }
3807
+ }
3808
+ const runs = getChildElements(p, "r");
3809
+ for (const r of runs) {
3810
+ if (r.parentNode && r.parentNode.localName === "hyperlink") continue;
3811
+ const result = extractRun(r);
3812
+ if (result.bold) hasBold = true;
3813
+ if (result.italic) hasItalic = true;
3814
+ const fnRefEls = getChildElements(r, "footnoteReference");
3815
+ if (fnRefEls.length > 0) {
3816
+ const fnId = getAttr(fnRefEls[0], "id");
3817
+ if (fnId && footnotes.has(fnId)) {
3818
+ footnoteText = footnotes.get(fnId);
3819
+ }
3820
+ }
3821
+ if (result.text) parts.push(result.text);
3822
+ }
3823
+ const text = parts.join("").trim();
3824
+ if (!text) return null;
3825
+ const style = styles.get(styleId);
3826
+ if (style?.outlineLevel !== void 0 && style.outlineLevel >= 0 && style.outlineLevel <= 5) {
3827
+ return {
3828
+ type: "heading",
3829
+ text,
3830
+ level: style.outlineLevel + 1
3831
+ };
3832
+ }
3833
+ if (numId && numId !== "0") {
3834
+ const numDef = numbering.get(numId);
3835
+ const levelInfo = numDef?.get(ilvl);
3836
+ const listType = levelInfo?.numFmt === "bullet" ? "unordered" : "ordered";
3837
+ return { type: "list", text, listType };
3838
+ }
3839
+ const block = { type: "paragraph", text };
3840
+ if (hasBold || hasItalic) {
3841
+ block.style = { bold: hasBold || void 0, italic: hasItalic || void 0 };
3842
+ }
3843
+ if (href) block.href = href;
3844
+ if (footnoteText) block.footnoteText = footnoteText;
3845
+ return block;
3846
+ }
3847
+ function parseTable(tbl, styles, numbering, footnotes, rels) {
3848
+ const trElements = getChildElements(tbl, "tr");
3849
+ if (trElements.length === 0) return null;
3850
+ const rows = [];
3851
+ let maxCols = 0;
3852
+ for (const tr of trElements) {
3853
+ const tcElements = getChildElements(tr, "tc");
3854
+ const row = [];
3855
+ for (const tc of tcElements) {
3856
+ let colSpan = 1;
3857
+ let rowSpan = 1;
3858
+ const tcPrEls = getChildElements(tc, "tcPr");
3859
+ if (tcPrEls.length > 0) {
3860
+ const gridSpanEls = getChildElements(tcPrEls[0], "gridSpan");
3861
+ if (gridSpanEls.length > 0) {
3862
+ colSpan = parseInt(getAttr(gridSpanEls[0], "val") ?? "1", 10);
3863
+ }
3864
+ const vMergeEls = getChildElements(tcPrEls[0], "vMerge");
3865
+ if (vMergeEls.length > 0) {
3866
+ const val = getAttr(vMergeEls[0], "val");
3867
+ if (val !== "restart" && val !== null) {
3868
+ row.push({ text: "", colSpan, rowSpan: 0 });
3869
+ continue;
3870
+ }
3871
+ }
3872
+ }
3873
+ const cellTexts = [];
3874
+ const pElements = getChildElements(tc, "p");
3875
+ for (const p of pElements) {
3876
+ const block = parseParagraph(p, styles, numbering, footnotes, rels);
3877
+ if (block?.text) cellTexts.push(block.text);
3878
+ }
3879
+ row.push({ text: cellTexts.join("\n"), colSpan, rowSpan });
3880
+ }
3881
+ rows.push(row);
3882
+ if (row.length > maxCols) maxCols = row.length;
3883
+ }
3884
+ for (let c = 0; c < maxCols; c++) {
3885
+ for (let r = 0; r < rows.length; r++) {
3886
+ const cell = rows[r][c];
3887
+ if (!cell || cell.rowSpan === 0) continue;
3888
+ let span = 1;
3889
+ for (let nr = r + 1; nr < rows.length; nr++) {
3890
+ if (rows[nr][c]?.rowSpan === 0) span++;
3891
+ else break;
3892
+ }
3893
+ cell.rowSpan = span;
3894
+ }
3895
+ }
3896
+ const cleanRows = [];
3897
+ for (const row of rows) {
3898
+ const clean = row.filter((cell) => cell.rowSpan !== 0);
3899
+ cleanRows.push(clean);
3900
+ }
3901
+ if (cleanRows.length === 0) return null;
3902
+ let cols = 0;
3903
+ for (const row of cleanRows) {
3904
+ let c = 0;
3905
+ for (const cell of row) c += cell.colSpan;
3906
+ if (c > cols) cols = c;
3907
+ }
3908
+ const table = {
3909
+ rows: cleanRows.length,
3910
+ cols,
3911
+ cells: cleanRows,
3912
+ hasHeader: cleanRows.length > 1
3913
+ };
3914
+ return { type: "table", table };
3915
+ }
3916
+ async function extractImages(zip, rels, doc) {
3917
+ const blocks = [];
3918
+ const images = [];
3919
+ const drawingElements = findElements(doc.documentElement, "drawing");
3920
+ let imgIdx = 0;
3921
+ for (const drawing of drawingElements) {
3922
+ const blips = findElements(drawing, "blip");
3923
+ for (const blip of blips) {
3924
+ const embedId = getAttr(blip, "embed");
3925
+ if (!embedId) continue;
3926
+ const target = rels.get(embedId);
3927
+ if (!target) continue;
3928
+ const imgPath = target.startsWith("/") ? target.slice(1) : target.startsWith("word/") ? target : `word/${target}`;
3929
+ const imgFile = zip.file(imgPath);
3930
+ if (!imgFile) continue;
3931
+ try {
3932
+ const data = await imgFile.async("uint8array");
3933
+ imgIdx++;
3934
+ const ext = imgPath.split(".").pop()?.toLowerCase() ?? "png";
3935
+ const mimeMap = {
3936
+ png: "image/png",
3937
+ jpg: "image/jpeg",
3938
+ jpeg: "image/jpeg",
3939
+ gif: "image/gif",
3940
+ bmp: "image/bmp",
3941
+ wmf: "image/wmf",
3942
+ emf: "image/emf"
3943
+ };
3944
+ const filename = `image_${String(imgIdx).padStart(3, "0")}.${ext}`;
3945
+ images.push({ filename, data, mimeType: mimeMap[ext] ?? "image/png" });
3946
+ blocks.push({ type: "image", text: filename });
3947
+ } catch {
3948
+ }
3949
+ }
3950
+ }
3951
+ return { blocks, images };
3952
+ }
3953
+ async function parseDocxDocument(buffer, options) {
3954
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
3955
+ const zip = await import_jszip4.default.loadAsync(buffer);
3956
+ const warnings = [];
3957
+ const docFile = zip.file("word/document.xml");
3958
+ if (!docFile) {
3959
+ throw new KordocError("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 DOCX \uD30C\uC77C: word/document.xml\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
3960
+ }
3961
+ let rels = /* @__PURE__ */ new Map();
3962
+ const relsFile = zip.file("word/_rels/document.xml.rels");
3963
+ if (relsFile) {
3964
+ rels = parseRels2(await relsFile.async("text"));
3965
+ }
3966
+ let styles = /* @__PURE__ */ new Map();
3967
+ const stylesFile = zip.file("word/styles.xml");
3968
+ if (stylesFile) {
3969
+ try {
3970
+ styles = parseStyles(await stylesFile.async("text"));
3971
+ } catch {
3972
+ }
3973
+ }
3974
+ let numbering = /* @__PURE__ */ new Map();
3975
+ const numFile = zip.file("word/numbering.xml");
3976
+ if (numFile) {
3977
+ try {
3978
+ numbering = parseNumbering(await numFile.async("text"));
3979
+ } catch {
3980
+ }
3981
+ }
3982
+ let footnotes = /* @__PURE__ */ new Map();
3983
+ const fnFile = zip.file("word/footnotes.xml");
3984
+ if (fnFile) {
3985
+ try {
3986
+ footnotes = parseFootnotes(await fnFile.async("text"));
3987
+ } catch {
3988
+ }
3989
+ }
3990
+ const docXml = await docFile.async("text");
3991
+ const doc = parseXml2(docXml);
3992
+ const body = findElements(doc, "body");
3993
+ if (body.length === 0) {
3994
+ throw new KordocError("DOCX \uBCF8\uBB38(w:body)\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
3995
+ }
3996
+ const blocks = [];
3997
+ const bodyEl = body[0];
3998
+ const children = bodyEl.childNodes;
3999
+ for (let i = 0; i < children.length; i++) {
4000
+ const node = children[i];
4001
+ if (node.nodeType !== 1) continue;
4002
+ const el = node;
4003
+ const localName = el.localName ?? el.tagName?.split(":").pop();
4004
+ if (localName === "p") {
4005
+ const block = parseParagraph(el, styles, numbering, footnotes, rels);
4006
+ if (block) blocks.push(block);
4007
+ } else if (localName === "tbl") {
4008
+ const block = parseTable(el, styles, numbering, footnotes, rels);
4009
+ if (block) blocks.push(block);
4010
+ }
4011
+ }
4012
+ const { blocks: imgBlocks, images } = await extractImages(zip, rels, doc);
4013
+ const metadata = {};
4014
+ const coreFile = zip.file("docProps/core.xml");
4015
+ if (coreFile) {
4016
+ try {
4017
+ const coreXml = await coreFile.async("text");
4018
+ const coreDoc = parseXml2(coreXml);
4019
+ const getFirst = (tag) => {
4020
+ const els = coreDoc.getElementsByTagName(tag);
4021
+ return els.length > 0 ? (els[0].textContent ?? "").trim() : void 0;
4022
+ };
4023
+ metadata.title = getFirst("dc:title") || getFirst("dcterms:title");
4024
+ metadata.author = getFirst("dc:creator");
4025
+ metadata.description = getFirst("dc:description");
4026
+ const created = getFirst("dcterms:created");
4027
+ if (created) metadata.createdAt = created;
4028
+ const modified = getFirst("dcterms:modified");
4029
+ if (modified) metadata.modifiedAt = modified;
4030
+ } catch {
4031
+ }
4032
+ }
4033
+ const outline = blocks.filter((b) => b.type === "heading").map((b) => ({ level: b.level ?? 2, text: b.text ?? "" }));
4034
+ const markdown = blocksToMarkdown(blocks);
4035
+ return {
4036
+ markdown,
4037
+ blocks,
4038
+ metadata,
4039
+ outline: outline.length > 0 ? outline : void 0,
4040
+ warnings: warnings.length > 0 ? warnings : void 0,
4041
+ images: images.length > 0 ? images : void 0
4042
+ };
4043
+ }
4044
+
3054
4045
  // src/diff/text-diff.ts
3055
4046
  function similarity(a, b) {
3056
4047
  if (a === b) return 1;
@@ -3349,12 +4340,12 @@ function extractInlineFields(text) {
3349
4340
  }
3350
4341
 
3351
4342
  // src/hwpx/generator.ts
3352
- var import_jszip2 = __toESM(require("jszip"), 1);
4343
+ var import_jszip5 = __toESM(require("jszip"), 1);
3353
4344
  var HWPML_NS = "http://www.hancom.co.kr/hwpml/2016/HwpMl";
3354
4345
  async function markdownToHwpx(markdown) {
3355
4346
  const blocks = parseMarkdownToBlocks(markdown);
3356
4347
  const sectionXml = blocksToSectionXml(blocks);
3357
- const zip = new import_jszip2.default();
4348
+ const zip = new import_jszip5.default();
3358
4349
  zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
3359
4350
  zip.file("Contents/content.hpf", generateManifest());
3360
4351
  zip.file("Contents/section0.xml", sectionXml);
@@ -3464,8 +4455,12 @@ async function parse(input, options) {
3464
4455
  }
3465
4456
  const format = detectFormat(buffer);
3466
4457
  switch (format) {
3467
- case "hwpx":
4458
+ case "hwpx": {
4459
+ const zipFormat = await detectZipFormat(buffer);
4460
+ if (zipFormat === "xlsx") return parseXlsx(buffer, options);
4461
+ if (zipFormat === "docx") return parseDocx(buffer, options);
3468
4462
  return parseHwpx(buffer, options);
4463
+ }
3469
4464
  case "hwp":
3470
4465
  return parseHwp(buffer, options);
3471
4466
  case "pdf":
@@ -3492,9 +4487,27 @@ async function parseHwp(buffer, options) {
3492
4487
  }
3493
4488
  async function parsePdf(buffer, options) {
3494
4489
  try {
3495
- return await parsePdfDocument(buffer, options);
4490
+ const { markdown, blocks, metadata, outline, warnings, isImageBased } = await parsePdfDocument(buffer, options);
4491
+ return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased };
4492
+ } catch (err) {
4493
+ const isImageBased = err instanceof Error && "isImageBased" in err ? true : void 0;
4494
+ return { success: false, fileType: "pdf", error: err instanceof Error ? err.message : "PDF \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err), isImageBased };
4495
+ }
4496
+ }
4497
+ async function parseXlsx(buffer, options) {
4498
+ try {
4499
+ const { markdown, blocks, metadata, warnings } = await parseXlsxDocument(buffer, options);
4500
+ return { success: true, fileType: "xlsx", markdown, blocks, metadata, warnings };
4501
+ } catch (err) {
4502
+ return { success: false, fileType: "xlsx", error: err instanceof Error ? err.message : "XLSX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
4503
+ }
4504
+ }
4505
+ async function parseDocx(buffer, options) {
4506
+ try {
4507
+ const { markdown, blocks, metadata, outline, warnings, images } = await parseDocxDocument(buffer, options);
4508
+ return { success: true, fileType: "docx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
3496
4509
  } catch (err) {
3497
- return { success: false, fileType: "pdf", error: err instanceof Error ? err.message : "PDF \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
4510
+ return { success: false, fileType: "docx", error: err instanceof Error ? err.message : "DOCX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
3498
4511
  }
3499
4512
  }
3500
4513
  // Annotate the CommonJS export names for ESM import in node:
@@ -3503,15 +4516,19 @@ async function parsePdf(buffer, options) {
3503
4516
  blocksToMarkdown,
3504
4517
  compare,
3505
4518
  detectFormat,
4519
+ detectZipFormat,
3506
4520
  diffBlocks,
3507
4521
  extractFormFields,
3508
4522
  isHwpxFile,
3509
4523
  isOldHwpFile,
3510
4524
  isPdfFile,
4525
+ isZipFile,
3511
4526
  markdownToHwpx,
3512
4527
  parse,
4528
+ parseDocx,
3513
4529
  parseHwp,
3514
4530
  parseHwpx,
3515
- parsePdf
4531
+ parsePdf,
4532
+ parseXlsx
3516
4533
  });
3517
4534
  //# sourceMappingURL=index.cjs.map