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.js CHANGED
@@ -8,6 +8,44 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // src/page-range.ts
12
+ var page_range_exports = {};
13
+ __export(page_range_exports, {
14
+ parsePageRange: () => parsePageRange
15
+ });
16
+ function parsePageRange(spec, maxPages) {
17
+ const result = /* @__PURE__ */ new Set();
18
+ if (maxPages <= 0) return result;
19
+ if (Array.isArray(spec)) {
20
+ for (const n of spec) {
21
+ const page = Math.round(n);
22
+ if (page >= 1 && page <= maxPages) result.add(page);
23
+ }
24
+ return result;
25
+ }
26
+ if (typeof spec !== "string" || spec.trim() === "") return result;
27
+ const parts = spec.split(",");
28
+ for (const part of parts) {
29
+ const trimmed = part.trim();
30
+ if (!trimmed) continue;
31
+ const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
32
+ if (rangeMatch) {
33
+ const start = Math.max(1, parseInt(rangeMatch[1], 10));
34
+ const end = Math.min(maxPages, parseInt(rangeMatch[2], 10));
35
+ for (let i = start; i <= end; i++) result.add(i);
36
+ } else {
37
+ const page = parseInt(trimmed, 10);
38
+ if (!isNaN(page) && page >= 1 && page <= maxPages) result.add(page);
39
+ }
40
+ }
41
+ return result;
42
+ }
43
+ var init_page_range = __esm({
44
+ "src/page-range.ts"() {
45
+ "use strict";
46
+ }
47
+ });
48
+
11
49
  // src/ocr/provider.ts
12
50
  var provider_exports = {};
13
51
  __export(provider_exports, {
@@ -54,13 +92,17 @@ var init_provider = __esm({
54
92
  import { readFile } from "fs/promises";
55
93
 
56
94
  // src/detect.ts
95
+ import JSZip from "jszip";
57
96
  function magicBytes(buffer) {
58
97
  return new Uint8Array(buffer, 0, Math.min(4, buffer.byteLength));
59
98
  }
60
- function isHwpxFile(buffer) {
99
+ function isZipFile(buffer) {
61
100
  const b = magicBytes(buffer);
62
101
  return b[0] === 80 && b[1] === 75 && b[2] === 3 && b[3] === 4;
63
102
  }
103
+ function isHwpxFile(buffer) {
104
+ return isZipFile(buffer);
105
+ }
64
106
  function isOldHwpFile(buffer) {
65
107
  const b = magicBytes(buffer);
66
108
  return b[0] === 208 && b[1] === 207 && b[2] === 17 && b[3] === 224;
@@ -71,14 +113,27 @@ function isPdfFile(buffer) {
71
113
  }
72
114
  function detectFormat(buffer) {
73
115
  if (buffer.byteLength < 4) return "unknown";
74
- if (isHwpxFile(buffer)) return "hwpx";
116
+ if (isZipFile(buffer)) return "hwpx";
75
117
  if (isOldHwpFile(buffer)) return "hwp";
76
118
  if (isPdfFile(buffer)) return "pdf";
77
119
  return "unknown";
78
120
  }
121
+ async function detectZipFormat(buffer) {
122
+ try {
123
+ const zip = await JSZip.loadAsync(buffer);
124
+ if (zip.file("xl/workbook.xml")) return "xlsx";
125
+ if (zip.file("word/document.xml")) return "docx";
126
+ if (zip.file("Contents/content.hpf") || zip.file("mimetype")) return "hwpx";
127
+ const hasSection = Object.keys(zip.files).some((f) => f.startsWith("Contents/"));
128
+ if (hasSection) return "hwpx";
129
+ return "unknown";
130
+ } catch {
131
+ return "unknown";
132
+ }
133
+ }
79
134
 
80
135
  // src/hwpx/parser.ts
81
- import JSZip from "jszip";
136
+ import JSZip2 from "jszip";
82
137
  import { inflateRawSync } from "zlib";
83
138
  import { DOMParser } from "@xmldom/xmldom";
84
139
 
@@ -138,6 +193,16 @@ function buildTable(rows) {
138
193
  cellIdx++;
139
194
  }
140
195
  }
196
+ let effectiveCols = maxCols;
197
+ while (effectiveCols > 0) {
198
+ const colEmpty = grid.every((row) => !row[effectiveCols - 1]?.text?.trim());
199
+ if (!colEmpty) break;
200
+ effectiveCols--;
201
+ }
202
+ if (effectiveCols < maxCols && effectiveCols > 0) {
203
+ const trimmed = grid.map((row) => row.slice(0, effectiveCols));
204
+ return { rows: numRows, cols: effectiveCols, cells: trimmed, hasHeader: numRows > 1 };
205
+ }
141
206
  return { rows: numRows, cols: maxCols, cells: grid, hasHeader: numRows > 1 };
142
207
  }
143
208
  function convertTableToText(rows) {
@@ -145,13 +210,26 @@ function convertTableToText(rows) {
145
210
  (row) => row.map((c) => c.text.trim().replace(/\n/g, " ")).filter(Boolean).join(" | ")
146
211
  ).filter(Boolean).join("\n");
147
212
  }
213
+ var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
214
+ function sanitizeText(text) {
215
+ let result = text.replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
216
+ if (result.length <= 30 && result.includes(" ")) {
217
+ const tokens = result.split(" ");
218
+ const koreanSingleCharCount = tokens.filter((t) => t.length === 1 && /[\uAC00-\uD7AF\u3131-\u318E]/.test(t)).length;
219
+ if (tokens.length >= 3 && koreanSingleCharCount / tokens.length >= 0.7) {
220
+ result = tokens.join("");
221
+ }
222
+ }
223
+ return result;
224
+ }
148
225
  function blocksToMarkdown(blocks) {
149
226
  const lines = [];
150
227
  for (let i = 0; i < blocks.length; i++) {
151
228
  const block = blocks[i];
152
229
  if (block.type === "heading" && block.text) {
153
230
  const prefix = "#".repeat(Math.min(block.level || 2, 6));
154
- lines.push("", `${prefix} ${block.text}`, "");
231
+ const headingText = sanitizeText(block.text);
232
+ if (headingText) lines.push("", `${prefix} ${headingText}`, "");
155
233
  continue;
156
234
  }
157
235
  if (block.type === "image" && block.text) {
@@ -163,9 +241,11 @@ function blocksToMarkdown(blocks) {
163
241
  continue;
164
242
  }
165
243
  if (block.type === "list" && block.text) {
166
- const alreadyNumbered = block.listType === "ordered" && /^\d+\.\s/.test(block.text);
244
+ const listText = sanitizeText(block.text);
245
+ if (!listText) continue;
246
+ const alreadyNumbered = block.listType === "ordered" && /^\d+\.\s/.test(listText);
167
247
  const prefix = alreadyNumbered ? "" : block.listType === "ordered" ? "1. " : "- ";
168
- lines.push(`${prefix}${block.text}`);
248
+ lines.push(`${prefix}${listText}`);
169
249
  if (block.children) {
170
250
  for (const child of block.children) {
171
251
  const childPrefix = child.listType === "ordered" ? "1." : "-";
@@ -175,7 +255,8 @@ function blocksToMarkdown(blocks) {
175
255
  continue;
176
256
  }
177
257
  if (block.type === "paragraph" && block.text) {
178
- let text = block.text;
258
+ let text = sanitizeText(block.text);
259
+ if (!text) continue;
179
260
  if (/^\[별표\s*\d+/.test(text)) {
180
261
  const nextBlock = blocks[i + 1];
181
262
  if (nextBlock?.type === "paragraph" && nextBlock.text && /관련\)?$/.test(nextBlock.text)) {
@@ -212,7 +293,7 @@ function tableToMarkdown(table) {
212
293
  if (table.rows === 0 || table.cols === 0) return "";
213
294
  const { cells, rows: numRows, cols: numCols } = table;
214
295
  if (numRows === 1 && numCols === 1) {
215
- const content = cells[0][0].text;
296
+ const content = sanitizeText(cells[0][0].text);
216
297
  return content.split(/\n/).map((line) => {
217
298
  const trimmed = line.trim();
218
299
  if (!trimmed) return "";
@@ -221,13 +302,19 @@ function tableToMarkdown(table) {
221
302
  return trimmed;
222
303
  }).filter(Boolean).join("\n");
223
304
  }
305
+ if (numCols === 1 && numRows >= 2) {
306
+ return cells.map((row) => sanitizeText(row[0].text).replace(/\n/g, " ")).filter(Boolean).join("\n");
307
+ }
224
308
  const display = Array.from({ length: numRows }, () => Array(numCols).fill(""));
225
309
  const skip = /* @__PURE__ */ new Set();
226
310
  for (let r = 0; r < numRows; r++) {
311
+ let cellIdx = 0;
227
312
  for (let c = 0; c < numCols; c++) {
228
313
  if (skip.has(`${r},${c}`)) continue;
229
- const cell = cells[r][c];
230
- display[r][c] = cell.text.replace(/\n/g, "<br>");
314
+ const cell = cells[r]?.[cellIdx];
315
+ if (!cell) break;
316
+ cellIdx++;
317
+ display[r][c] = sanitizeText(cell.text).replace(/\n/g, "<br>");
231
318
  for (let dr = 0; dr < cell.rowSpan; dr++) {
232
319
  for (let dc = 0; dc < cell.colSpan; dc++) {
233
320
  if (dr === 0 && dc === 0) continue;
@@ -236,12 +323,28 @@ function tableToMarkdown(table) {
236
323
  }
237
324
  }
238
325
  }
326
+ c += cell.colSpan - 1;
239
327
  }
240
328
  }
241
329
  const uniqueRows = [];
242
- for (const row of display) {
330
+ let pendingFirstCol = "";
331
+ for (let r = 0; r < display.length; r++) {
332
+ const row = display[r];
243
333
  const isEmptyPlaceholder = row.every((cell) => cell === "");
244
- if (!isEmptyPlaceholder) uniqueRows.push(row);
334
+ if (isEmptyPlaceholder) continue;
335
+ const hasSkippedCols = row.some((cell, c) => cell === "" && skip.has(`${r},${c}`));
336
+ const nonEmptyCols = row.filter((cell) => cell !== "");
337
+ if (!hasSkippedCols && nonEmptyCols.length === 1 && row[0] !== "" && row.slice(1).every((c) => c === "")) {
338
+ pendingFirstCol = row[0];
339
+ continue;
340
+ }
341
+ if (pendingFirstCol && row[0] === "") {
342
+ row[0] = pendingFirstCol;
343
+ pendingFirstCol = "";
344
+ } else {
345
+ pendingFirstCol = "";
346
+ }
347
+ uniqueRows.push(row);
245
348
  }
246
349
  if (uniqueRows.length === 0) return "";
247
350
  const md = [];
@@ -253,8 +356,13 @@ function tableToMarkdown(table) {
253
356
  return md.join("\n");
254
357
  }
255
358
 
359
+ // src/types.ts
360
+ var HEADING_RATIO_H1 = 1.5;
361
+ var HEADING_RATIO_H2 = 1.3;
362
+ var HEADING_RATIO_H3 = 1.15;
363
+
256
364
  // src/utils.ts
257
- var VERSION = true ? "1.7.2" : "0.0.0-dev";
365
+ var VERSION = true ? "1.8.0" : "0.0.0-dev";
258
366
  function toArrayBuffer(buf) {
259
367
  if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
260
368
  return buf.buffer;
@@ -272,6 +380,50 @@ function isPathTraversal(name) {
272
380
  const normalized = name.replace(/\\/g, "/");
273
381
  return normalized.includes("..") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized);
274
382
  }
383
+ function precheckZipSize(buffer, maxUncompressedSize = 100 * 1024 * 1024, maxEntries = 500) {
384
+ try {
385
+ const data = new DataView(buffer);
386
+ const len = buffer.byteLength;
387
+ let eocdOffset = -1;
388
+ for (let i = len - 22; i >= Math.max(0, len - 65557); i--) {
389
+ if (data.getUint32(i, true) === 101010256) {
390
+ eocdOffset = i;
391
+ break;
392
+ }
393
+ }
394
+ if (eocdOffset < 0) return { totalUncompressed: 0, entryCount: 0 };
395
+ const entryCount = data.getUint16(eocdOffset + 10, true);
396
+ if (entryCount > maxEntries) {
397
+ throw new KordocError(`ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC: ${entryCount} (\uCD5C\uB300 ${maxEntries})`);
398
+ }
399
+ const cdSize = data.getUint32(eocdOffset + 12, true);
400
+ const cdOffset = data.getUint32(eocdOffset + 16, true);
401
+ if (cdOffset + cdSize > len) return { totalUncompressed: 0, entryCount };
402
+ let totalUncompressed = 0;
403
+ let pos = cdOffset;
404
+ for (let i = 0; i < entryCount && pos + 46 <= cdOffset + cdSize; i++) {
405
+ if (data.getUint32(pos, true) !== 33639248) break;
406
+ totalUncompressed += data.getUint32(pos + 24, true);
407
+ const nameLen = data.getUint16(pos + 28, true);
408
+ const extraLen = data.getUint16(pos + 30, true);
409
+ const commentLen = data.getUint16(pos + 32, true);
410
+ pos += 46 + nameLen + extraLen + commentLen;
411
+ }
412
+ if (totalUncompressed > maxUncompressedSize) {
413
+ throw new KordocError(`ZIP \uBE44\uC555\uCD95 \uD06C\uAE30 \uCD08\uACFC: ${(totalUncompressed / 1024 / 1024).toFixed(1)}MB (\uCD5C\uB300 ${maxUncompressedSize / 1024 / 1024}MB)`);
414
+ }
415
+ return { totalUncompressed, entryCount };
416
+ } catch (err) {
417
+ if (err instanceof KordocError) throw err;
418
+ return { totalUncompressed: 0, entryCount: 0 };
419
+ }
420
+ }
421
+ var SAFE_HREF_RE2 = /^(?:https?:|mailto:|tel:|#)/i;
422
+ function sanitizeHref2(href) {
423
+ const trimmed = href.trim();
424
+ if (!trimmed || !SAFE_HREF_RE2.test(trimmed)) return null;
425
+ return trimmed;
426
+ }
275
427
  function classifyError(err) {
276
428
  if (!(err instanceof Error)) return "PARSE_ERROR";
277
429
  const msg = err.message;
@@ -285,36 +437,8 @@ function classifyError(err) {
285
437
  return "PARSE_ERROR";
286
438
  }
287
439
 
288
- // src/page-range.ts
289
- function parsePageRange(spec, maxPages) {
290
- const result = /* @__PURE__ */ new Set();
291
- if (maxPages <= 0) return result;
292
- if (Array.isArray(spec)) {
293
- for (const n of spec) {
294
- const page = Math.round(n);
295
- if (page >= 1 && page <= maxPages) result.add(page);
296
- }
297
- return result;
298
- }
299
- if (typeof spec !== "string" || spec.trim() === "") return result;
300
- const parts = spec.split(",");
301
- for (const part of parts) {
302
- const trimmed = part.trim();
303
- if (!trimmed) continue;
304
- const rangeMatch = trimmed.match(/^(\d+)\s*-\s*(\d+)$/);
305
- if (rangeMatch) {
306
- const start = Math.max(1, parseInt(rangeMatch[1], 10));
307
- const end = Math.min(maxPages, parseInt(rangeMatch[2], 10));
308
- for (let i = start; i <= end; i++) result.add(i);
309
- } else {
310
- const page = parseInt(trimmed, 10);
311
- if (!isNaN(page) && page >= 1 && page <= maxPages) result.add(page);
312
- }
313
- }
314
- return result;
315
- }
316
-
317
440
  // src/hwpx/parser.ts
441
+ init_page_range();
318
442
  var MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024;
319
443
  var MAX_ZIP_ENTRIES = 500;
320
444
  function clampSpan(val, max) {
@@ -406,16 +530,10 @@ function stripDtd(xml) {
406
530
  return xml.replace(/<!DOCTYPE\s[^[>]*(\[[\s\S]*?\])?\s*>/gi, "");
407
531
  }
408
532
  async function parseHwpxDocument(buffer, options) {
409
- const precheck = precheckZipSize(buffer);
410
- if (precheck.totalUncompressed > MAX_DECOMPRESS_SIZE) {
411
- throw new KordocError("ZIP \uBE44\uC555\uCD95 \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
412
- }
413
- if (precheck.entryCount > MAX_ZIP_ENTRIES) {
414
- throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
415
- }
533
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
416
534
  let zip;
417
535
  try {
418
- zip = await JSZip.loadAsync(buffer);
536
+ zip = await JSZip2.loadAsync(buffer);
419
537
  } catch {
420
538
  return extractFromBrokenZip(buffer);
421
539
  }
@@ -575,46 +693,17 @@ function parseDublinCoreMetadata(xml, metadata) {
575
693
  metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
576
694
  }
577
695
  }
578
- function precheckZipSize(buffer) {
579
- try {
580
- const data = new DataView(buffer);
581
- const len = buffer.byteLength;
582
- if (len < 22) return { totalUncompressed: 0, entryCount: 0 };
583
- const searchStart = Math.max(0, len - 22 - 65535);
584
- let eocdOffset = -1;
585
- for (let i = len - 22; i >= searchStart; i--) {
586
- if (data.getUint32(i, true) === 101010256) {
587
- eocdOffset = i;
588
- break;
589
- }
590
- }
591
- if (eocdOffset < 0) return { totalUncompressed: 0, entryCount: 0 };
592
- const entryCount = data.getUint16(eocdOffset + 10, true);
593
- const cdSize = data.getUint32(eocdOffset + 12, true);
594
- const cdOffset = data.getUint32(eocdOffset + 16, true);
595
- if (cdOffset + cdSize > len) return { totalUncompressed: 0, entryCount };
596
- let totalUncompressed = 0;
597
- let pos = cdOffset;
598
- for (let i = 0; i < entryCount && pos + 46 <= cdOffset + cdSize; i++) {
599
- if (data.getUint32(pos, true) !== 33639248) break;
600
- totalUncompressed += data.getUint32(pos + 24, true);
601
- const nameLen = data.getUint16(pos + 28, true);
602
- const extraLen = data.getUint16(pos + 30, true);
603
- const commentLen = data.getUint16(pos + 32, true);
604
- pos += 46 + nameLen + extraLen + commentLen;
605
- }
606
- return { totalUncompressed, entryCount };
607
- } catch {
608
- return { totalUncompressed: 0, entryCount: 0 };
609
- }
610
- }
611
696
  function extractFromBrokenZip(buffer) {
612
697
  const data = new Uint8Array(buffer);
613
698
  const view = new DataView(buffer);
614
699
  let pos = 0;
615
700
  const blocks = [];
701
+ const warnings = [
702
+ { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
703
+ ];
616
704
  let totalDecompressed = 0;
617
705
  let entryCount = 0;
706
+ let sectionNum = 0;
618
707
  while (pos < data.length - 30) {
619
708
  if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
620
709
  pos++;
@@ -660,14 +749,15 @@ function extractFromBrokenZip(buffer) {
660
749
  }
661
750
  totalDecompressed += content.length * 2;
662
751
  if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
663
- blocks.push(...parseSectionXml(content));
752
+ sectionNum++;
753
+ blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum));
664
754
  } catch {
665
755
  continue;
666
756
  }
667
757
  }
668
758
  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");
669
759
  const markdown = blocksToMarkdown(blocks);
670
- return { markdown, blocks };
760
+ return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
671
761
  }
672
762
  async function resolveSectionPaths(zip) {
673
763
  const manifestPaths = ["Contents/content.hpf", "content.hpf"];
@@ -731,9 +821,9 @@ function detectHwpxHeadings(blocks, styleMap) {
731
821
  let level = 0;
732
822
  if (baseFontSize > 0 && block.style?.fontSize) {
733
823
  const ratio = block.style.fontSize / baseFontSize;
734
- if (ratio >= 1.5) level = 1;
735
- else if (ratio >= 1.3) level = 2;
736
- else if (ratio >= 1.15) level = 3;
824
+ if (ratio >= HEADING_RATIO_H1) level = 1;
825
+ else if (ratio >= HEADING_RATIO_H2) level = 2;
826
+ else if (ratio >= HEADING_RATIO_H3) level = 3;
737
827
  }
738
828
  if (/^제\d+[조장절편]/.test(text) && text.length <= 50) {
739
829
  if (level === 0) level = 3;
@@ -865,39 +955,47 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
865
955
  if (depth > MAX_XML_DEPTH) return tableCtx;
866
956
  const children = node.childNodes;
867
957
  if (!children) return tableCtx;
868
- for (let i = 0; i < children.length; i++) {
869
- const el = children[i];
870
- if (el.nodeType !== 1) continue;
871
- const tag = el.tagName || el.localName || "";
872
- const localTag = tag.replace(/^[^:]+:/, "");
873
- if (localTag === "tbl") {
874
- if (tableCtx) tableStack.push(tableCtx);
875
- const newTable = { rows: [], currentRow: [], cell: null };
876
- walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
877
- if (newTable.rows.length > 0) {
878
- if (tableStack.length > 0) {
879
- const parentTable = tableStack.pop();
880
- const nestedText = convertTableToText(newTable.rows);
881
- if (parentTable.cell) {
882
- parentTable.cell.text += (parentTable.cell.text ? "\n" : "") + nestedText;
958
+ const walkChildren = (parent, d) => {
959
+ if (d > MAX_XML_DEPTH) return;
960
+ const kids = parent.childNodes;
961
+ if (!kids) return;
962
+ for (let i = 0; i < kids.length; i++) {
963
+ const el = kids[i];
964
+ if (el.nodeType !== 1) continue;
965
+ const tag = el.tagName || el.localName || "";
966
+ const localTag = tag.replace(/^[^:]+:/, "");
967
+ if (localTag === "tbl") {
968
+ if (tableCtx) tableStack.push(tableCtx);
969
+ const newTable = { rows: [], currentRow: [], cell: null };
970
+ walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, d + 1);
971
+ if (newTable.rows.length > 0) {
972
+ if (tableStack.length > 0) {
973
+ const parentTable = tableStack.pop();
974
+ const nestedText = convertTableToText(newTable.rows);
975
+ if (parentTable.cell) {
976
+ parentTable.cell.text += (parentTable.cell.text ? "\n" : "") + nestedText;
977
+ }
978
+ tableCtx = parentTable;
979
+ } else {
980
+ blocks.push({ type: "table", table: buildTable(newTable.rows), pageNumber: sectionNum });
981
+ tableCtx = null;
883
982
  }
884
- tableCtx = parentTable;
885
983
  } else {
886
- blocks.push({ type: "table", table: buildTable(newTable.rows), pageNumber: sectionNum });
887
- tableCtx = null;
984
+ tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
888
985
  }
889
- } else {
890
- tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
891
- }
892
- } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
893
- const imgRef = extractImageRef(el);
894
- if (imgRef) {
895
- blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
896
- } else if (warnings && sectionNum) {
897
- warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
986
+ } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
987
+ const imgRef = extractImageRef(el);
988
+ if (imgRef) {
989
+ blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
990
+ } else if (warnings && sectionNum) {
991
+ warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
992
+ }
993
+ } else if (localTag === "r" || localTag === "run" || localTag === "ctrl") {
994
+ walkChildren(el, d + 1);
898
995
  }
899
996
  }
900
- }
997
+ };
998
+ walkChildren(node, depth);
901
999
  return tableCtx;
902
1000
  }
903
1001
  function extractParagraphInfo(para, styleMap) {
@@ -936,7 +1034,10 @@ function extractParagraphInfo(para, styleMap) {
936
1034
  // 하이퍼링크
937
1035
  case "hyperlink": {
938
1036
  const url = child.getAttribute("url") || child.getAttribute("href") || "";
939
- if (url) href = url;
1037
+ if (url) {
1038
+ const safe = sanitizeHref2(url);
1039
+ if (safe) href = safe;
1040
+ }
940
1041
  walk(child);
941
1042
  break;
942
1043
  }
@@ -949,6 +1050,29 @@ function extractParagraphInfo(para, styleMap) {
949
1050
  if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
950
1051
  break;
951
1052
  }
1053
+ // 제어 요소 — 필드, 컨트롤, 매개변수 등 스킵
1054
+ case "ctrl":
1055
+ case "fieldBegin":
1056
+ case "fieldEnd":
1057
+ case "parameters":
1058
+ case "stringParam":
1059
+ case "integerParam":
1060
+ case "boolParam":
1061
+ case "floatParam":
1062
+ case "secPr":
1063
+ // 섹션 속성 (페이지 설정 등)
1064
+ case "colPr":
1065
+ // 다단 속성
1066
+ case "linesegarray":
1067
+ case "lineseg":
1068
+ // 레이아웃 정보
1069
+ // 도형/이미지 요소 — 대체텍스트("사각형입니다." 등) 누출 방지
1070
+ case "pic":
1071
+ case "shape":
1072
+ case "drawingObject":
1073
+ case "shapeComment":
1074
+ case "drawText":
1075
+ break;
952
1076
  // run 요소에서 charPrIDRef 추출
953
1077
  case "r": {
954
1078
  const runCharPr = child.getAttribute("charPrIDRef");
@@ -963,7 +1087,10 @@ function extractParagraphInfo(para, styleMap) {
963
1087
  }
964
1088
  };
965
1089
  walk(para);
966
- const cleanText = text.replace(/[ \t]+/g, " ").trim();
1090
+ let cleanText = text.replace(/[ \t]+/g, " ").trim();
1091
+ if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
1092
+ cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
1093
+ cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|수식|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
967
1094
  let style;
968
1095
  if (styleMap && charPrId) {
969
1096
  const charProp = styleMap.charProperties.get(charPrId);
@@ -1142,6 +1269,7 @@ function extractText(data) {
1142
1269
  }
1143
1270
 
1144
1271
  // src/hwp5/parser.ts
1272
+ init_page_range();
1145
1273
  import { createRequire } from "module";
1146
1274
  var require2 = createRequire(import.meta.url);
1147
1275
  var CFB = require2("cfb");
@@ -1241,9 +1369,9 @@ function detectHwp5Headings(blocks, docInfo) {
1241
1369
  if (/^\d+$/.test(text)) continue;
1242
1370
  const ratio = block.style.fontSize / baseFontSize;
1243
1371
  let level = 0;
1244
- if (ratio >= 1.5) level = 1;
1245
- else if (ratio >= 1.3) level = 2;
1246
- else if (ratio >= 1.15) level = 3;
1372
+ if (ratio >= HEADING_RATIO_H1) level = 1;
1373
+ else if (ratio >= HEADING_RATIO_H2) level = 2;
1374
+ else if (ratio >= HEADING_RATIO_H3) level = 3;
1247
1375
  if (/^제\d+[조장절편]/.test(text) && text.length <= 50) {
1248
1376
  if (level === 0) level = 3;
1249
1377
  }
@@ -1330,20 +1458,22 @@ function detectImageMime(data) {
1330
1458
  }
1331
1459
  function extractHwp5Images(cfb, blocks, compressed, warnings) {
1332
1460
  const binDataMap = /* @__PURE__ */ new Map();
1333
- for (let idx = 0; idx < 1e4; idx++) {
1334
- const entry = CFB.find(cfb, `/BinData/BIN${String(idx).padStart(4, "0")}`) || CFB.find(cfb, `/BinData/Bin${String(idx).padStart(4, "0")}`);
1335
- if (!entry?.content) {
1336
- if (idx > 0) break;
1337
- continue;
1338
- }
1339
- let data = Buffer.from(entry.content);
1340
- if (compressed) {
1341
- try {
1342
- data = decompressStream(data);
1343
- } catch {
1461
+ const binDataRe = /\/BinData\/[Bb][Ii][Nn](\d{4})$/;
1462
+ if (cfb.FileIndex) {
1463
+ for (const entry of cfb.FileIndex) {
1464
+ if (!entry?.name || !entry.content) continue;
1465
+ const match = entry.name.match(binDataRe);
1466
+ if (!match) continue;
1467
+ const idx = parseInt(match[1], 10);
1468
+ let data = Buffer.from(entry.content);
1469
+ if (compressed) {
1470
+ try {
1471
+ data = decompressStream(data);
1472
+ } catch {
1473
+ }
1344
1474
  }
1475
+ binDataMap.set(idx, { data, name: entry.name });
1345
1476
  }
1346
- binDataMap.set(idx, { data, name: entry.name || `BIN${idx}` });
1347
1477
  }
1348
1478
  if (binDataMap.size === 0) return [];
1349
1479
  const images = [];
@@ -1490,6 +1620,16 @@ function parseTableBlock(records, startIdx) {
1490
1620
  i++;
1491
1621
  }
1492
1622
  if (rows === 0 || cols === 0 || cells.length === 0) return { table: null, nextIdx: i };
1623
+ const hasAddr = cells.some((c) => c.colAddr !== void 0 && c.rowAddr !== void 0);
1624
+ if (hasAddr) {
1625
+ const cellRows2 = arrangeCells(rows, cols, cells);
1626
+ const irCells = cellRows2.map((row) => row.map((c) => ({
1627
+ text: c.text.trim(),
1628
+ colSpan: c.colSpan,
1629
+ rowSpan: c.rowSpan
1630
+ })));
1631
+ return { table: { rows, cols, cells: irCells, hasHeader: rows > 1 }, nextIdx: i };
1632
+ }
1493
1633
  const cellRows = arrangeCells(rows, cols, cells);
1494
1634
  return { table: buildTable(cellRows), nextIdx: i };
1495
1635
  }
@@ -1559,6 +1699,9 @@ function arrangeCells(rows, cols, cells) {
1559
1699
  return grid.map((row) => row.map((c) => c || { text: "", colSpan: 1, rowSpan: 1 }));
1560
1700
  }
1561
1701
 
1702
+ // src/pdf/parser.ts
1703
+ init_page_range();
1704
+
1562
1705
  // src/pdf/line-detector.ts
1563
1706
  import { OPS } from "pdfjs-dist/legacy/build/pdf.mjs";
1564
1707
  var ORIENTATION_TOL = 2;
@@ -1753,7 +1896,36 @@ function buildTableGrids(horizontals, verticals) {
1753
1896
  };
1754
1897
  grids.push({ rowYs, colXs, bbox });
1755
1898
  }
1756
- return grids;
1899
+ return mergeAdjacentGrids(grids);
1900
+ }
1901
+ function mergeAdjacentGrids(grids) {
1902
+ if (grids.length <= 1) return grids;
1903
+ const sorted = [...grids].sort((a, b) => b.bbox.y2 - a.bbox.y2);
1904
+ const merged = [sorted[0]];
1905
+ for (let i = 1; i < sorted.length; i++) {
1906
+ const prev = merged[merged.length - 1];
1907
+ const curr = sorted[i];
1908
+ if (prev.colXs.length === curr.colXs.length) {
1909
+ const colMatch = prev.colXs.every((x, ci) => Math.abs(x - curr.colXs[ci]) <= COORD_MERGE_TOL * 3);
1910
+ const verticalGap = prev.bbox.y1 - curr.bbox.y2;
1911
+ if (colMatch && verticalGap >= -COORD_MERGE_TOL && verticalGap <= 20) {
1912
+ const allRowYs = [.../* @__PURE__ */ new Set([...prev.rowYs, ...curr.rowYs])].sort((a, b) => b - a);
1913
+ merged[merged.length - 1] = {
1914
+ rowYs: allRowYs,
1915
+ colXs: prev.colXs,
1916
+ bbox: {
1917
+ x1: Math.min(prev.bbox.x1, curr.bbox.x1),
1918
+ y1: Math.min(prev.bbox.y1, curr.bbox.y1),
1919
+ x2: Math.max(prev.bbox.x2, curr.bbox.x2),
1920
+ y2: Math.max(prev.bbox.y2, curr.bbox.y2)
1921
+ }
1922
+ };
1923
+ continue;
1924
+ }
1925
+ }
1926
+ merged.push(curr);
1927
+ }
1928
+ return merged;
1757
1929
  }
1758
1930
  function clusterCoordinates(values) {
1759
1931
  if (values.length === 0) return [];
@@ -1940,7 +2112,11 @@ function cellTextToString(items) {
1940
2112
  for (let j = 1; j < s.length; j++) {
1941
2113
  const gap = s[j].x - (s[j - 1].x + s[j - 1].w);
1942
2114
  const avgFs = (s[j].fontSize + s[j - 1].fontSize) / 2;
1943
- if (gap < avgFs * 0.3 && /[가-힣]$/.test(result) && /^[가-힣]/.test(s[j].text)) {
2115
+ const prevIsKorean = /[가-힣]$/.test(result);
2116
+ const currIsKorean = /^[가-힣]/.test(s[j].text);
2117
+ if (gap < avgFs * 0.15) {
2118
+ result += s[j].text;
2119
+ } else if (gap < avgFs * 0.35 && (prevIsKorean || currIsKorean)) {
1944
2120
  result += s[j].text;
1945
2121
  } else {
1946
2122
  result += " " + s[j].text;
@@ -1955,6 +2131,12 @@ function cellTextToString(items) {
1955
2131
  const curr = textLines[i];
1956
2132
  if (/[가-힣]$/.test(prev) && /^[가-힣]+$/.test(curr) && curr.length <= 8 && !curr.includes(" ")) {
1957
2133
  merged[merged.length - 1] = prev + curr;
2134
+ } else if (curr.trim().length <= 3 && /^[)\]%}]/.test(curr.trim())) {
2135
+ merged[merged.length - 1] = prev + curr.trim();
2136
+ } else if (/[,(]$/.test(prev.trim()) && curr.trim().length <= 15) {
2137
+ merged[merged.length - 1] = prev + curr.trim();
2138
+ } else if (/[\d,]$/.test(prev) && /^[\d,]+[)\]]?$/.test(curr.trim()) && curr.trim().length <= 10) {
2139
+ merged[merged.length - 1] = prev + curr.trim();
1958
2140
  } else {
1959
2141
  merged.push(curr);
1960
2142
  }
@@ -2167,21 +2349,26 @@ async function loadPdfWithTimeout(buffer) {
2167
2349
  disableFontFace: true,
2168
2350
  isEvalSupported: false
2169
2351
  });
2170
- return Promise.race([
2171
- loadingTask.promise,
2172
- new Promise(
2173
- (_, reject) => setTimeout(() => {
2174
- loadingTask.destroy();
2175
- reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
2176
- }, PDF_LOAD_TIMEOUT_MS)
2177
- )
2178
- ]);
2352
+ let timer;
2353
+ try {
2354
+ return await Promise.race([
2355
+ loadingTask.promise,
2356
+ new Promise((_, reject) => {
2357
+ timer = setTimeout(() => {
2358
+ loadingTask.destroy();
2359
+ reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
2360
+ }, PDF_LOAD_TIMEOUT_MS);
2361
+ })
2362
+ ]);
2363
+ } finally {
2364
+ if (timer !== void 0) clearTimeout(timer);
2365
+ }
2179
2366
  }
2180
2367
  async function parsePdfDocument(buffer, options) {
2181
2368
  const doc = await loadPdfWithTimeout(buffer);
2182
2369
  try {
2183
2370
  const pageCount = doc.numPages;
2184
- if (pageCount === 0) return { success: false, fileType: "pdf", pageCount: 0, error: "PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", code: "PARSE_ERROR" };
2371
+ if (pageCount === 0) throw new KordocError("PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
2185
2372
  const metadata = { pageCount };
2186
2373
  await extractPdfMetadata(doc, metadata);
2187
2374
  const blocks = [];
@@ -2234,14 +2421,14 @@ async function parsePdfDocument(buffer, options) {
2234
2421
  const ocrBlocks = await ocrPages2(doc, options.ocr, pageFilter, effectivePageCount);
2235
2422
  if (ocrBlocks.length > 0) {
2236
2423
  const ocrMarkdown = ocrBlocks.map((b) => b.text || "").filter(Boolean).join("\n\n");
2237
- return { success: true, fileType: "pdf", markdown: ocrMarkdown, pageCount: parsedPageCount, blocks: ocrBlocks, metadata, isImageBased: true, warnings };
2424
+ return { markdown: ocrMarkdown, blocks: ocrBlocks, metadata, warnings, isImageBased: true };
2238
2425
  }
2239
2426
  } catch {
2240
2427
  }
2241
2428
  }
2242
- 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" };
2429
+ throw Object.assign(new KordocError(`\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (${pageCount}\uD398\uC774\uC9C0, ${totalChars}\uC790)`), { isImageBased: true });
2243
2430
  }
2244
- if (options?.removeHeaderFooter && parsedPageCount >= 3) {
2431
+ if (options?.removeHeaderFooter !== false && parsedPageCount >= 3) {
2245
2432
  const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
2246
2433
  for (let ri = removed.length - 1; ri >= 0; ri--) {
2247
2434
  blocks.splice(removed[ri], 1);
@@ -2251,9 +2438,10 @@ async function parsePdfDocument(buffer, options) {
2251
2438
  if (medianFontSize > 0) {
2252
2439
  detectHeadings(blocks, medianFontSize);
2253
2440
  }
2441
+ detectMarkerHeadings(blocks);
2254
2442
  const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
2255
2443
  let markdown = cleanPdfText(blocksToMarkdown(blocks));
2256
- return { success: true, fileType: "pdf", markdown, pageCount: parsedPageCount, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
2444
+ return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
2257
2445
  } finally {
2258
2446
  await doc.destroy().catch(() => {
2259
2447
  });
@@ -2313,12 +2501,67 @@ function detectHeadings(blocks, medianFontSize) {
2313
2501
  if (/^\d+$/.test(text)) continue;
2314
2502
  const ratio = block.style.fontSize / medianFontSize;
2315
2503
  let level = 0;
2316
- if (ratio >= 1.5) level = 1;
2317
- else if (ratio >= 1.3) level = 2;
2318
- else if (ratio >= 1.15) level = 3;
2504
+ if (ratio >= HEADING_RATIO_H1) level = 1;
2505
+ else if (ratio >= HEADING_RATIO_H2) level = 2;
2506
+ else if (ratio >= HEADING_RATIO_H3) level = 3;
2319
2507
  if (level > 0) {
2320
2508
  block.type = "heading";
2321
2509
  block.level = level;
2510
+ block.text = collapseEvenSpacing(text);
2511
+ }
2512
+ }
2513
+ }
2514
+ function collapseEvenSpacing(text) {
2515
+ const tokens = text.split(" ");
2516
+ const singleCharCount = tokens.filter((t) => t.length === 1).length;
2517
+ if (tokens.length >= 3 && singleCharCount / tokens.length >= 0.7) {
2518
+ return tokens.join("");
2519
+ }
2520
+ return text;
2521
+ }
2522
+ function shouldDemoteTable(table) {
2523
+ const allCells = table.cells.flatMap((row) => row.map((c) => c.text.trim())).filter(Boolean);
2524
+ const allText = allCells.join(" ");
2525
+ if (allText.length > 200) return false;
2526
+ if (/[□■◆○●▶]/.test(allText) && table.rows <= 3) return true;
2527
+ const totalCells = table.rows * table.cols;
2528
+ const emptyCells = totalCells - allCells.length;
2529
+ if (table.rows <= 2 && emptyCells > totalCells * 0.5) return true;
2530
+ if (table.rows === 1 && !/\d{2,}/.test(allText)) return true;
2531
+ return false;
2532
+ }
2533
+ function demoteTableToText(table) {
2534
+ const lines = [];
2535
+ for (let r = 0; r < table.rows; r++) {
2536
+ const cells = table.cells[r].map((c) => c.text.trim()).filter(Boolean);
2537
+ if (cells.length === 0) continue;
2538
+ if (table.cols === 2 && cells.length === 2) {
2539
+ lines.push(`${cells[0]} : ${cells[1]}`);
2540
+ } else {
2541
+ lines.push(cells.join(" "));
2542
+ }
2543
+ }
2544
+ return lines.join("\n");
2545
+ }
2546
+ function detectMarkerHeadings(blocks) {
2547
+ for (let i = 0; i < blocks.length; i++) {
2548
+ const block = blocks[i];
2549
+ if (block.type !== "paragraph" || !block.text) continue;
2550
+ const text = block.text.trim();
2551
+ if (text.length < 50 && /^[□■◆◇▶]\s*[가-힣]/.test(text)) {
2552
+ block.type = "heading";
2553
+ block.level = 4;
2554
+ continue;
2555
+ }
2556
+ if (/^[가-힣]{2,6}$/.test(text) && block.style?.fontSize) {
2557
+ const prev = blocks[i - 1];
2558
+ const next = blocks[i + 1];
2559
+ const prevIsStructural = !prev || prev.type === "table" || prev.type === "heading" || prev.type === "separator";
2560
+ const nextIsStructural = !next || next.type === "table" || next.type === "heading" || next.type === "paragraph" && next.text && /^[□■◆○●]/.test(next.text.trim());
2561
+ if (prevIsStructural || nextIsStructural) {
2562
+ block.type = "heading";
2563
+ block.level = 3;
2564
+ }
2322
2565
  }
2323
2566
  }
2324
2567
  }
@@ -2355,7 +2598,7 @@ function computeRegion(items) {
2355
2598
  }
2356
2599
  return { items, minX, minY, maxX, maxY };
2357
2600
  }
2358
- function findYSplit(items, region, gapThreshold) {
2601
+ function findYSplit(items, _region, gapThreshold) {
2359
2602
  const sorted = [...items].sort((a, b) => b.y - a.y);
2360
2603
  let bestGap = gapThreshold;
2361
2604
  let bestSplit = null;
@@ -2370,7 +2613,7 @@ function findYSplit(items, region, gapThreshold) {
2370
2613
  }
2371
2614
  return bestSplit;
2372
2615
  }
2373
- function findXSplit(items, region, gapThreshold) {
2616
+ function findXSplit(items, _region, gapThreshold) {
2374
2617
  const sorted = [...items].sort((a, b) => a.x - b.x);
2375
2618
  let bestGap = gapThreshold;
2376
2619
  let bestSplit = null;
@@ -2429,7 +2672,8 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
2429
2672
  );
2430
2673
  for (const cell of cells) {
2431
2674
  const cellItems = cellTextMap.get(cell) || [];
2432
- const text = cellTextToString(cellItems);
2675
+ let text = cellTextToString(cellItems);
2676
+ text = text.replace(/^[\s]*[-–—]\s*\d+\s*[-–—][\s]*$/gm, "").trim();
2433
2677
  irGrid[cell.row][cell.col] = {
2434
2678
  text,
2435
2679
  colSpan: cell.colSpan,
@@ -2444,18 +2688,21 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
2444
2688
  };
2445
2689
  const hasContent = irGrid.some((row) => row.some((cell) => cell.text.trim() !== ""));
2446
2690
  if (!hasContent) continue;
2447
- blocks.push({
2448
- type: "table",
2449
- table: irTable,
2450
- pageNumber: pageNum,
2451
- bbox: {
2452
- page: pageNum,
2453
- x: grid.bbox.x1,
2454
- y: grid.bbox.y1,
2455
- width: grid.bbox.x2 - grid.bbox.x1,
2456
- height: grid.bbox.y2 - grid.bbox.y1
2691
+ const tableBbox = {
2692
+ page: pageNum,
2693
+ x: grid.bbox.x1,
2694
+ y: grid.bbox.y1,
2695
+ width: grid.bbox.x2 - grid.bbox.x1,
2696
+ height: grid.bbox.y2 - grid.bbox.y1
2697
+ };
2698
+ if (shouldDemoteTable(irTable)) {
2699
+ const demoted = demoteTableToText(irTable);
2700
+ if (demoted) {
2701
+ blocks.push({ type: "paragraph", text: demoted, pageNumber: pageNum, bbox: tableBbox, style: dominantStyle(tableItems) });
2457
2702
  }
2458
- });
2703
+ continue;
2704
+ }
2705
+ blocks.push({ type: "table", table: irTable, pageNumber: pageNum, bbox: tableBbox });
2459
2706
  }
2460
2707
  const remaining = items.filter((i) => !usedItems.has(i));
2461
2708
  if (remaining.length > 0) {
@@ -2467,9 +2714,29 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
2467
2714
  const by = b.bbox ? b.bbox.y + b.bbox.height : 0;
2468
2715
  return by - ay;
2469
2716
  });
2470
- return allBlocks;
2717
+ return mergeAdjacentTableBlocks(allBlocks);
2471
2718
  }
2472
- return blocks;
2719
+ return mergeAdjacentTableBlocks(blocks);
2720
+ }
2721
+ function mergeAdjacentTableBlocks(blocks) {
2722
+ if (blocks.length <= 1) return blocks;
2723
+ const result = [blocks[0]];
2724
+ for (let i = 1; i < blocks.length; i++) {
2725
+ const prev = result[result.length - 1];
2726
+ const curr = blocks[i];
2727
+ if (prev.type === "table" && curr.type === "table" && prev.table && curr.table && prev.table.cols === curr.table.cols) {
2728
+ const merged = {
2729
+ rows: prev.table.rows + curr.table.rows,
2730
+ cols: prev.table.cols,
2731
+ cells: [...prev.table.cells, ...curr.table.cells],
2732
+ hasHeader: prev.table.hasHeader
2733
+ };
2734
+ result[result.length - 1] = { ...prev, table: merged };
2735
+ } else {
2736
+ result.push(curr);
2737
+ }
2738
+ }
2739
+ return result;
2473
2740
  }
2474
2741
  function extractPageBlocksFallback(items, pageNum) {
2475
2742
  if (items.length === 0) return [];
@@ -2492,11 +2759,13 @@ function extractPageBlocksFallback(items, pageNum) {
2492
2759
  }));
2493
2760
  const clusterResults = detectClusterTables(clusterItems, pageNum);
2494
2761
  if (clusterResults.length > 0) {
2762
+ const ciToIdx = /* @__PURE__ */ new Map();
2763
+ for (let ci = 0; ci < clusterItems.length; ci++) ciToIdx.set(clusterItems[ci], ci);
2495
2764
  const usedIndices = /* @__PURE__ */ new Set();
2496
2765
  for (const cr of clusterResults) {
2497
2766
  for (const ci of cr.usedItems) {
2498
- const idx = clusterItems.indexOf(ci);
2499
- if (idx >= 0) usedIndices.add(idx);
2767
+ const idx = ciToIdx.get(ci);
2768
+ if (idx !== void 0) usedIndices.add(idx);
2500
2769
  }
2501
2770
  blocks.push({ type: "table", table: cr.table, pageNumber: pageNum, bbox: cr.bbox });
2502
2771
  }
@@ -2807,7 +3076,8 @@ function mergeLineSimple(items) {
2807
3076
  const gap = sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w);
2808
3077
  const avgFs = (sorted[i].fontSize + sorted[i - 1].fontSize) / 2;
2809
3078
  if (gap > 15) result += " ";
2810
- else if (gap < avgFs * 0.3 && /[가-힣]$/.test(result) && /^[가-힣]/.test(sorted[i].text)) {
3079
+ else if (gap < avgFs * 0.15) {
3080
+ } else if (gap < avgFs * 0.35 && (/[가-힣]$/.test(result) || /^[가-힣]/.test(sorted[i].text))) {
2811
3081
  } else if (gap > 3) result += " ";
2812
3082
  result += sorted[i].text;
2813
3083
  }
@@ -2815,8 +3085,8 @@ function mergeLineSimple(items) {
2815
3085
  }
2816
3086
  function cleanPdfText(text) {
2817
3087
  return mergeKoreanLines(
2818
- text.replace(/^[\s]*[-–—]\s*\d+\s*[-–—][\s]*$/gm, "").replace(/^\s*\d+\s*\/\s*\d+\s*$/gm, "")
2819
- ).replace(/\n{3,}/g, "\n\n").trim();
3088
+ 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}$/, "")
3089
+ ).replace(/^(?!\|).{3,30}$/gm, (line) => collapseEvenSpacing(line)).replace(/\n{3,}/g, "\n\n").trim();
2820
3090
  }
2821
3091
  function startsWithMarker(line) {
2822
3092
  const t = line.trimStart();
@@ -2830,15 +3100,13 @@ function detectListBlocks(blocks) {
2830
3100
  for (let i = 0; i < blocks.length; i++) {
2831
3101
  const block = blocks[i];
2832
3102
  if (block.type === "paragraph" && block.text) {
2833
- const match = block.text.match(/^(\d+)\.\s/);
2834
- if (match) {
2835
- result.push({
2836
- ...block,
2837
- type: "list",
2838
- listType: "ordered",
2839
- // 원래 번호를 text에 보존 (blocksToMarkdown에서 그대로 출력)
2840
- text: block.text
2841
- });
3103
+ const text = block.text.trim();
3104
+ if (/^\d+\.\s/.test(text)) {
3105
+ result.push({ ...block, type: "list", listType: "ordered", text: block.text });
3106
+ continue;
3107
+ }
3108
+ if (/^[○●·※▶▷◆◇\-]\s/.test(text)) {
3109
+ result.push({ ...block, type: "list", listType: "unordered", text: block.text });
2842
3110
  continue;
2843
3111
  }
2844
3112
  }
@@ -2997,11 +3265,20 @@ function mergeKoreanLines(text) {
2997
3265
  for (let i = 1; i < lines.length; i++) {
2998
3266
  const prev = result[result.length - 1];
2999
3267
  const curr = lines[i];
3000
- if (/^#{1,6}\s/.test(prev) || /^#{1,6}\s/.test(curr)) {
3268
+ const currTrimmed = curr.trim();
3269
+ if (/^#{1,6}\s/.test(prev) || /^#{1,6}\s/.test(curr) || /^\|/.test(currTrimmed) || /^---/.test(currTrimmed)) {
3001
3270
  result.push(curr);
3002
3271
  continue;
3003
3272
  }
3004
- if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !curr.trimStart().startsWith("|") && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
3273
+ if (/,$/.test(prev.trim()) && currTrimmed.length > 0) {
3274
+ result[result.length - 1] = prev + "\n" + curr;
3275
+ continue;
3276
+ }
3277
+ if (/^\(※/.test(currTrimmed)) {
3278
+ result[result.length - 1] = prev + " " + currTrimmed;
3279
+ continue;
3280
+ }
3281
+ if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
3005
3282
  result[result.length - 1] = prev + " " + curr;
3006
3283
  } else {
3007
3284
  result.push(curr);
@@ -3010,6 +3287,716 @@ function mergeKoreanLines(text) {
3010
3287
  return result.join("\n");
3011
3288
  }
3012
3289
 
3290
+ // src/xlsx/parser.ts
3291
+ import JSZip3 from "jszip";
3292
+ import { DOMParser as DOMParser2 } from "@xmldom/xmldom";
3293
+ var MAX_SHEETS = 100;
3294
+ var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
3295
+ var MAX_ROWS2 = 1e4;
3296
+ var MAX_COLS2 = 200;
3297
+ function cleanNumericValue(raw) {
3298
+ if (!/^-?\d+\.\d+$/.test(raw)) return raw;
3299
+ const num = parseFloat(raw);
3300
+ if (!isFinite(num)) return raw;
3301
+ const cleaned = parseFloat(num.toPrecision(15)).toString();
3302
+ return cleaned;
3303
+ }
3304
+ function parseCellRef(ref) {
3305
+ const m = ref.match(/^([A-Z]+)(\d+)$/);
3306
+ if (!m) return null;
3307
+ let col = 0;
3308
+ for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64);
3309
+ return { col: col - 1, row: parseInt(m[2], 10) - 1 };
3310
+ }
3311
+ function parseMergeRef(ref) {
3312
+ const parts = ref.split(":");
3313
+ if (parts.length !== 2) return null;
3314
+ const start = parseCellRef(parts[0]);
3315
+ const end = parseCellRef(parts[1]);
3316
+ if (!start || !end) return null;
3317
+ return { startCol: start.col, startRow: start.row, endCol: end.col, endRow: end.row };
3318
+ }
3319
+ function getElements(parent, tagName) {
3320
+ const nodes = parent.getElementsByTagName(tagName);
3321
+ const result = [];
3322
+ for (let i = 0; i < nodes.length; i++) result.push(nodes[i]);
3323
+ return result;
3324
+ }
3325
+ function getTextContent(el) {
3326
+ return el.textContent?.trim() ?? "";
3327
+ }
3328
+ function parseXml(text) {
3329
+ return new DOMParser2().parseFromString(text, "text/xml");
3330
+ }
3331
+ function parseSharedStrings(xml) {
3332
+ const doc = parseXml(xml);
3333
+ const strings = [];
3334
+ const siList = getElements(doc.documentElement, "si");
3335
+ for (const si of siList) {
3336
+ const tElements = getElements(si, "t");
3337
+ strings.push(tElements.map((t) => t.textContent ?? "").join(""));
3338
+ }
3339
+ return strings;
3340
+ }
3341
+ function parseWorkbook(xml) {
3342
+ const doc = parseXml(xml);
3343
+ const sheets = [];
3344
+ const sheetElements = getElements(doc.documentElement, "sheet");
3345
+ for (const el of sheetElements) {
3346
+ sheets.push({
3347
+ name: el.getAttribute("name") ?? `Sheet${sheets.length + 1}`,
3348
+ sheetId: el.getAttribute("sheetId") ?? "",
3349
+ rId: el.getAttribute("r:id") ?? ""
3350
+ });
3351
+ }
3352
+ return sheets;
3353
+ }
3354
+ function parseRels(xml) {
3355
+ const doc = parseXml(xml);
3356
+ const map = /* @__PURE__ */ new Map();
3357
+ const rels = getElements(doc.documentElement, "Relationship");
3358
+ for (const rel of rels) {
3359
+ const id = rel.getAttribute("Id");
3360
+ const target = rel.getAttribute("Target");
3361
+ if (id && target) map.set(id, target);
3362
+ }
3363
+ return map;
3364
+ }
3365
+ function parseWorksheet(xml, sharedStrings) {
3366
+ const doc = parseXml(xml);
3367
+ const grid = [];
3368
+ let maxRow = 0;
3369
+ let maxCol = 0;
3370
+ const rows = getElements(doc.documentElement, "row");
3371
+ for (const rowEl of rows) {
3372
+ const rowNum = parseInt(rowEl.getAttribute("r") ?? "0", 10) - 1;
3373
+ if (rowNum < 0 || rowNum >= MAX_ROWS2) continue;
3374
+ const cells = getElements(rowEl, "c");
3375
+ for (const cellEl of cells) {
3376
+ const ref = cellEl.getAttribute("r");
3377
+ if (!ref) continue;
3378
+ const pos = parseCellRef(ref);
3379
+ if (!pos || pos.col >= MAX_COLS2) continue;
3380
+ const type = cellEl.getAttribute("t");
3381
+ const vElements = getElements(cellEl, "v");
3382
+ const fElements = getElements(cellEl, "f");
3383
+ let value = "";
3384
+ if (vElements.length > 0) {
3385
+ const raw = getTextContent(vElements[0]);
3386
+ if (type === "s") {
3387
+ const idx = parseInt(raw, 10);
3388
+ value = sharedStrings[idx] ?? "";
3389
+ } else if (type === "b") {
3390
+ value = raw === "1" ? "TRUE" : "FALSE";
3391
+ } else {
3392
+ value = cleanNumericValue(raw);
3393
+ }
3394
+ } else if (type === "inlineStr") {
3395
+ const isEl = getElements(cellEl, "is");
3396
+ if (isEl.length > 0) {
3397
+ const tElements = getElements(isEl[0], "t");
3398
+ value = tElements.map((t) => t.textContent ?? "").join("");
3399
+ }
3400
+ }
3401
+ if (!value && fElements.length > 0) {
3402
+ value = `=${getTextContent(fElements[0])}`;
3403
+ }
3404
+ while (grid.length <= pos.row) grid.push([]);
3405
+ while (grid[pos.row].length <= pos.col) grid[pos.row].push("");
3406
+ grid[pos.row][pos.col] = value;
3407
+ if (pos.row > maxRow) maxRow = pos.row;
3408
+ if (pos.col > maxCol) maxCol = pos.col;
3409
+ }
3410
+ }
3411
+ const merges = [];
3412
+ const mergeCellElements = getElements(doc.documentElement, "mergeCell");
3413
+ for (const el of mergeCellElements) {
3414
+ const ref = el.getAttribute("ref");
3415
+ if (!ref) continue;
3416
+ const m = parseMergeRef(ref);
3417
+ if (m) merges.push(m);
3418
+ }
3419
+ return { grid, merges, maxRow, maxCol };
3420
+ }
3421
+ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex) {
3422
+ const blocks = [];
3423
+ if (sheetName) {
3424
+ blocks.push({
3425
+ type: "heading",
3426
+ text: sheetName,
3427
+ level: 2,
3428
+ pageNumber: sheetIndex + 1
3429
+ });
3430
+ }
3431
+ if (maxRow < 0 || maxCol < 0 || grid.length === 0) return blocks;
3432
+ const mergeMap = /* @__PURE__ */ new Map();
3433
+ const mergeSkip = /* @__PURE__ */ new Set();
3434
+ for (const m of merges) {
3435
+ const colSpan = m.endCol - m.startCol + 1;
3436
+ const rowSpan = m.endRow - m.startRow + 1;
3437
+ mergeMap.set(`${m.startRow},${m.startCol}`, { colSpan, rowSpan });
3438
+ for (let r = m.startRow; r <= m.endRow; r++) {
3439
+ for (let c = m.startCol; c <= m.endCol; c++) {
3440
+ if (r !== m.startRow || c !== m.startCol) {
3441
+ mergeSkip.add(`${r},${c}`);
3442
+ }
3443
+ }
3444
+ }
3445
+ }
3446
+ let firstRow = -1;
3447
+ let lastRow = -1;
3448
+ for (let r = 0; r <= maxRow; r++) {
3449
+ const row = grid[r];
3450
+ if (row && row.some((cell) => cell !== "")) {
3451
+ if (firstRow === -1) firstRow = r;
3452
+ lastRow = r;
3453
+ }
3454
+ }
3455
+ if (firstRow === -1) return blocks;
3456
+ const cellRows = [];
3457
+ for (let r = firstRow; r <= lastRow; r++) {
3458
+ const row = [];
3459
+ for (let c = 0; c <= maxCol; c++) {
3460
+ const key = `${r},${c}`;
3461
+ if (mergeSkip.has(key)) continue;
3462
+ const text = (grid[r] && grid[r][c]) ?? "";
3463
+ const merge = mergeMap.get(key);
3464
+ row.push({
3465
+ text,
3466
+ colSpan: merge?.colSpan ?? 1,
3467
+ rowSpan: merge?.rowSpan ?? 1
3468
+ });
3469
+ }
3470
+ cellRows.push(row);
3471
+ }
3472
+ if (cellRows.length > 0) {
3473
+ const table = buildTable(cellRows);
3474
+ if (table.rows > 0) {
3475
+ blocks.push({ type: "table", table, pageNumber: sheetIndex + 1 });
3476
+ }
3477
+ }
3478
+ return blocks;
3479
+ }
3480
+ async function parseXlsxDocument(buffer, options) {
3481
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE3);
3482
+ const zip = await JSZip3.loadAsync(buffer);
3483
+ const warnings = [];
3484
+ const workbookFile = zip.file("xl/workbook.xml");
3485
+ if (!workbookFile) {
3486
+ throw new KordocError("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 XLSX \uD30C\uC77C: xl/workbook.xml\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
3487
+ }
3488
+ let sharedStrings = [];
3489
+ const ssFile = zip.file("xl/sharedStrings.xml");
3490
+ if (ssFile) {
3491
+ sharedStrings = parseSharedStrings(await ssFile.async("text"));
3492
+ }
3493
+ const sheets = parseWorkbook(await workbookFile.async("text"));
3494
+ if (sheets.length === 0) {
3495
+ throw new KordocError("XLSX \uD30C\uC77C\uC5D0 \uC2DC\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4");
3496
+ }
3497
+ let relsMap = /* @__PURE__ */ new Map();
3498
+ const relsFile = zip.file("xl/_rels/workbook.xml.rels");
3499
+ if (relsFile) {
3500
+ relsMap = parseRels(await relsFile.async("text"));
3501
+ }
3502
+ let pageFilter = null;
3503
+ if (options?.pages) {
3504
+ const { parsePageRange: parsePageRange2 } = await Promise.resolve().then(() => (init_page_range(), page_range_exports));
3505
+ pageFilter = parsePageRange2(options.pages, sheets.length);
3506
+ }
3507
+ const blocks = [];
3508
+ const processedSheets = Math.min(sheets.length, MAX_SHEETS);
3509
+ for (let i = 0; i < processedSheets; i++) {
3510
+ if (pageFilter && !pageFilter.has(i + 1)) continue;
3511
+ const sheet = sheets[i];
3512
+ options?.onProgress?.(i + 1, processedSheets);
3513
+ let sheetPath = relsMap.get(sheet.rId);
3514
+ if (sheetPath) {
3515
+ if (!sheetPath.startsWith("xl/") && !sheetPath.startsWith("/")) {
3516
+ sheetPath = `xl/${sheetPath}`;
3517
+ } else if (sheetPath.startsWith("/")) {
3518
+ sheetPath = sheetPath.slice(1);
3519
+ }
3520
+ } else {
3521
+ sheetPath = `xl/worksheets/sheet${i + 1}.xml`;
3522
+ }
3523
+ const sheetFile = zip.file(sheetPath);
3524
+ if (!sheetFile) {
3525
+ warnings.push({
3526
+ page: i + 1,
3527
+ message: `\uC2DC\uD2B8 "${sheet.name}" \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${sheetPath}`,
3528
+ code: "PARTIAL_PARSE"
3529
+ });
3530
+ continue;
3531
+ }
3532
+ try {
3533
+ const sheetXml = await sheetFile.async("text");
3534
+ const { grid, merges, maxRow, maxCol } = parseWorksheet(sheetXml, sharedStrings);
3535
+ const sheetBlocks = sheetToBlocks(sheet.name, grid, merges, maxRow, maxCol, i);
3536
+ blocks.push(...sheetBlocks);
3537
+ } catch (err) {
3538
+ warnings.push({
3539
+ page: i + 1,
3540
+ message: `\uC2DC\uD2B8 "${sheet.name}" \uD30C\uC2F1 \uC2E4\uD328: ${err instanceof Error ? err.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`,
3541
+ code: "PARTIAL_PARSE"
3542
+ });
3543
+ }
3544
+ }
3545
+ const metadata = {
3546
+ pageCount: processedSheets
3547
+ };
3548
+ const coreFile = zip.file("docProps/core.xml");
3549
+ if (coreFile) {
3550
+ try {
3551
+ const coreXml = await coreFile.async("text");
3552
+ const doc = parseXml(coreXml);
3553
+ const getFirst = (tag) => {
3554
+ const els = doc.getElementsByTagName(tag);
3555
+ return els.length > 0 ? (els[0].textContent ?? "").trim() : void 0;
3556
+ };
3557
+ metadata.title = getFirst("dc:title") || getFirst("dcterms:title");
3558
+ metadata.author = getFirst("dc:creator");
3559
+ metadata.description = getFirst("dc:description");
3560
+ const created = getFirst("dcterms:created");
3561
+ if (created) metadata.createdAt = created;
3562
+ const modified = getFirst("dcterms:modified");
3563
+ if (modified) metadata.modifiedAt = modified;
3564
+ } catch {
3565
+ }
3566
+ }
3567
+ const markdown = blocksToMarkdown(blocks);
3568
+ return { markdown, blocks, metadata, warnings: warnings.length > 0 ? warnings : void 0 };
3569
+ }
3570
+
3571
+ // src/docx/parser.ts
3572
+ import JSZip4 from "jszip";
3573
+ import { DOMParser as DOMParser3 } from "@xmldom/xmldom";
3574
+ var MAX_DECOMPRESS_SIZE4 = 100 * 1024 * 1024;
3575
+ function getChildElements(parent, localName) {
3576
+ const result = [];
3577
+ const children = parent.childNodes;
3578
+ for (let i = 0; i < children.length; i++) {
3579
+ const node = children[i];
3580
+ if (node.nodeType === 1) {
3581
+ const el = node;
3582
+ if (el.localName === localName || el.tagName?.endsWith(`:${localName}`)) {
3583
+ result.push(el);
3584
+ }
3585
+ }
3586
+ }
3587
+ return result;
3588
+ }
3589
+ function findElements(parent, localName) {
3590
+ const result = [];
3591
+ const walk = (node) => {
3592
+ const children = node.childNodes;
3593
+ for (let i = 0; i < children.length; i++) {
3594
+ const child = children[i];
3595
+ if (child.nodeType === 1) {
3596
+ const el = child;
3597
+ if (el.localName === localName || el.tagName?.endsWith(`:${localName}`)) {
3598
+ result.push(el);
3599
+ }
3600
+ walk(el);
3601
+ }
3602
+ }
3603
+ };
3604
+ walk(parent);
3605
+ return result;
3606
+ }
3607
+ function getAttr(el, localName) {
3608
+ const attrs = el.attributes;
3609
+ for (let i = 0; i < attrs.length; i++) {
3610
+ const attr = attrs[i];
3611
+ if (attr.localName === localName || attr.name === localName) return attr.value;
3612
+ }
3613
+ return null;
3614
+ }
3615
+ function parseXml2(text) {
3616
+ return new DOMParser3().parseFromString(text, "text/xml");
3617
+ }
3618
+ function parseStyles(xml) {
3619
+ const doc = parseXml2(xml);
3620
+ const styles = /* @__PURE__ */ new Map();
3621
+ const styleElements = findElements(doc, "style");
3622
+ for (const el of styleElements) {
3623
+ const styleId = getAttr(el, "styleId");
3624
+ if (!styleId) continue;
3625
+ const nameEls = getChildElements(el, "name");
3626
+ const name = nameEls.length > 0 ? getAttr(nameEls[0], "val") ?? "" : "";
3627
+ const basedOnEls = getChildElements(el, "basedOn");
3628
+ const basedOn = basedOnEls.length > 0 ? getAttr(basedOnEls[0], "val") ?? void 0 : void 0;
3629
+ const pPrEls = getChildElements(el, "pPr");
3630
+ let outlineLevel;
3631
+ if (pPrEls.length > 0) {
3632
+ const outlineEls = getChildElements(pPrEls[0], "outlineLvl");
3633
+ if (outlineEls.length > 0) {
3634
+ const val = getAttr(outlineEls[0], "val");
3635
+ if (val !== null) outlineLevel = parseInt(val, 10);
3636
+ }
3637
+ }
3638
+ if (outlineLevel === void 0) {
3639
+ const headingMatch = name.match(/^(?:heading|Heading)\s*(\d+)$/i);
3640
+ if (headingMatch) outlineLevel = parseInt(headingMatch[1], 10) - 1;
3641
+ }
3642
+ styles.set(styleId, { name, basedOn, outlineLevel });
3643
+ }
3644
+ return styles;
3645
+ }
3646
+ function parseNumbering(xml) {
3647
+ const doc = parseXml2(xml);
3648
+ const abstractNums = /* @__PURE__ */ new Map();
3649
+ const abstractElements = findElements(doc, "abstractNum");
3650
+ for (const el of abstractElements) {
3651
+ const abstractNumId = getAttr(el, "abstractNumId");
3652
+ if (!abstractNumId) continue;
3653
+ const levels = /* @__PURE__ */ new Map();
3654
+ const lvlElements = getChildElements(el, "lvl");
3655
+ for (const lvl of lvlElements) {
3656
+ const ilvl = parseInt(getAttr(lvl, "ilvl") ?? "0", 10);
3657
+ const numFmtEls = getChildElements(lvl, "numFmt");
3658
+ const numFmt = numFmtEls.length > 0 ? getAttr(numFmtEls[0], "val") ?? "bullet" : "bullet";
3659
+ levels.set(ilvl, { numFmt, level: ilvl });
3660
+ }
3661
+ abstractNums.set(abstractNumId, levels);
3662
+ }
3663
+ const nums = /* @__PURE__ */ new Map();
3664
+ const numElements = findElements(doc, "num");
3665
+ for (const el of numElements) {
3666
+ const numId = getAttr(el, "numId");
3667
+ if (!numId) continue;
3668
+ const abstractRefs = getChildElements(el, "abstractNumId");
3669
+ if (abstractRefs.length > 0) {
3670
+ const ref = getAttr(abstractRefs[0], "val");
3671
+ if (ref && abstractNums.has(ref)) {
3672
+ nums.set(numId, abstractNums.get(ref));
3673
+ }
3674
+ }
3675
+ }
3676
+ return nums;
3677
+ }
3678
+ function parseRels2(xml) {
3679
+ const doc = parseXml2(xml);
3680
+ const map = /* @__PURE__ */ new Map();
3681
+ const rels = findElements(doc, "Relationship");
3682
+ for (const rel of rels) {
3683
+ const id = getAttr(rel, "Id");
3684
+ const target = getAttr(rel, "Target");
3685
+ if (id && target) map.set(id, target);
3686
+ }
3687
+ return map;
3688
+ }
3689
+ function parseFootnotes(xml) {
3690
+ const doc = parseXml2(xml);
3691
+ const notes = /* @__PURE__ */ new Map();
3692
+ const fnElements = findElements(doc, "footnote");
3693
+ for (const fn of fnElements) {
3694
+ const id = getAttr(fn, "id");
3695
+ if (!id || id === "0" || id === "-1") continue;
3696
+ const texts = [];
3697
+ const pElements = findElements(fn, "p");
3698
+ for (const p of pElements) {
3699
+ const runs = findElements(p, "r");
3700
+ for (const r of runs) {
3701
+ const tElements = getChildElements(r, "t");
3702
+ for (const t of tElements) texts.push(t.textContent ?? "");
3703
+ }
3704
+ }
3705
+ notes.set(id, texts.join("").trim());
3706
+ }
3707
+ return notes;
3708
+ }
3709
+ function extractRun(r) {
3710
+ const tElements = getChildElements(r, "t");
3711
+ const text = tElements.map((t) => t.textContent ?? "").join("");
3712
+ let bold = false;
3713
+ let italic = false;
3714
+ const rPrEls = getChildElements(r, "rPr");
3715
+ if (rPrEls.length > 0) {
3716
+ bold = getChildElements(rPrEls[0], "b").length > 0;
3717
+ italic = getChildElements(rPrEls[0], "i").length > 0;
3718
+ }
3719
+ return { text, bold, italic };
3720
+ }
3721
+ function parseParagraph(p, styles, numbering, footnotes, rels) {
3722
+ const pPrEls = getChildElements(p, "pPr");
3723
+ let styleId = "";
3724
+ let numId = "";
3725
+ let ilvl = 0;
3726
+ if (pPrEls.length > 0) {
3727
+ const pStyleEls = getChildElements(pPrEls[0], "pStyle");
3728
+ if (pStyleEls.length > 0) styleId = getAttr(pStyleEls[0], "val") ?? "";
3729
+ const numPrEls = getChildElements(pPrEls[0], "numPr");
3730
+ if (numPrEls.length > 0) {
3731
+ const numIdEls = getChildElements(numPrEls[0], "numId");
3732
+ const ilvlEls = getChildElements(numPrEls[0], "ilvl");
3733
+ numId = numIdEls.length > 0 ? getAttr(numIdEls[0], "val") ?? "" : "";
3734
+ ilvl = ilvlEls.length > 0 ? parseInt(getAttr(ilvlEls[0], "val") ?? "0", 10) : 0;
3735
+ }
3736
+ }
3737
+ const parts = [];
3738
+ let hasBold = false;
3739
+ let hasItalic = false;
3740
+ let href;
3741
+ let footnoteText;
3742
+ const hyperlinks = getChildElements(p, "hyperlink");
3743
+ const hyperlinkTexts = /* @__PURE__ */ new Set();
3744
+ for (const hl of hyperlinks) {
3745
+ const rId = getAttr(hl, "id");
3746
+ const hlText = [];
3747
+ const runs2 = findElements(hl, "r");
3748
+ for (const r of runs2) {
3749
+ const result = extractRun(r);
3750
+ hlText.push(result.text);
3751
+ }
3752
+ const text2 = hlText.join("");
3753
+ if (text2) {
3754
+ hyperlinkTexts.add(text2);
3755
+ if (rId && rels.has(rId)) {
3756
+ href = rels.get(rId);
3757
+ parts.push(text2);
3758
+ } else {
3759
+ parts.push(text2);
3760
+ }
3761
+ }
3762
+ }
3763
+ const runs = getChildElements(p, "r");
3764
+ for (const r of runs) {
3765
+ if (r.parentNode && r.parentNode.localName === "hyperlink") continue;
3766
+ const result = extractRun(r);
3767
+ if (result.bold) hasBold = true;
3768
+ if (result.italic) hasItalic = true;
3769
+ const fnRefEls = getChildElements(r, "footnoteReference");
3770
+ if (fnRefEls.length > 0) {
3771
+ const fnId = getAttr(fnRefEls[0], "id");
3772
+ if (fnId && footnotes.has(fnId)) {
3773
+ footnoteText = footnotes.get(fnId);
3774
+ }
3775
+ }
3776
+ if (result.text) parts.push(result.text);
3777
+ }
3778
+ const text = parts.join("").trim();
3779
+ if (!text) return null;
3780
+ const style = styles.get(styleId);
3781
+ if (style?.outlineLevel !== void 0 && style.outlineLevel >= 0 && style.outlineLevel <= 5) {
3782
+ return {
3783
+ type: "heading",
3784
+ text,
3785
+ level: style.outlineLevel + 1
3786
+ };
3787
+ }
3788
+ if (numId && numId !== "0") {
3789
+ const numDef = numbering.get(numId);
3790
+ const levelInfo = numDef?.get(ilvl);
3791
+ const listType = levelInfo?.numFmt === "bullet" ? "unordered" : "ordered";
3792
+ return { type: "list", text, listType };
3793
+ }
3794
+ const block = { type: "paragraph", text };
3795
+ if (hasBold || hasItalic) {
3796
+ block.style = { bold: hasBold || void 0, italic: hasItalic || void 0 };
3797
+ }
3798
+ if (href) block.href = href;
3799
+ if (footnoteText) block.footnoteText = footnoteText;
3800
+ return block;
3801
+ }
3802
+ function parseTable(tbl, styles, numbering, footnotes, rels) {
3803
+ const trElements = getChildElements(tbl, "tr");
3804
+ if (trElements.length === 0) return null;
3805
+ const rows = [];
3806
+ let maxCols = 0;
3807
+ for (const tr of trElements) {
3808
+ const tcElements = getChildElements(tr, "tc");
3809
+ const row = [];
3810
+ for (const tc of tcElements) {
3811
+ let colSpan = 1;
3812
+ let rowSpan = 1;
3813
+ const tcPrEls = getChildElements(tc, "tcPr");
3814
+ if (tcPrEls.length > 0) {
3815
+ const gridSpanEls = getChildElements(tcPrEls[0], "gridSpan");
3816
+ if (gridSpanEls.length > 0) {
3817
+ colSpan = parseInt(getAttr(gridSpanEls[0], "val") ?? "1", 10);
3818
+ }
3819
+ const vMergeEls = getChildElements(tcPrEls[0], "vMerge");
3820
+ if (vMergeEls.length > 0) {
3821
+ const val = getAttr(vMergeEls[0], "val");
3822
+ if (val !== "restart" && val !== null) {
3823
+ row.push({ text: "", colSpan, rowSpan: 0 });
3824
+ continue;
3825
+ }
3826
+ }
3827
+ }
3828
+ const cellTexts = [];
3829
+ const pElements = getChildElements(tc, "p");
3830
+ for (const p of pElements) {
3831
+ const block = parseParagraph(p, styles, numbering, footnotes, rels);
3832
+ if (block?.text) cellTexts.push(block.text);
3833
+ }
3834
+ row.push({ text: cellTexts.join("\n"), colSpan, rowSpan });
3835
+ }
3836
+ rows.push(row);
3837
+ if (row.length > maxCols) maxCols = row.length;
3838
+ }
3839
+ for (let c = 0; c < maxCols; c++) {
3840
+ for (let r = 0; r < rows.length; r++) {
3841
+ const cell = rows[r][c];
3842
+ if (!cell || cell.rowSpan === 0) continue;
3843
+ let span = 1;
3844
+ for (let nr = r + 1; nr < rows.length; nr++) {
3845
+ if (rows[nr][c]?.rowSpan === 0) span++;
3846
+ else break;
3847
+ }
3848
+ cell.rowSpan = span;
3849
+ }
3850
+ }
3851
+ const cleanRows = [];
3852
+ for (const row of rows) {
3853
+ const clean = row.filter((cell) => cell.rowSpan !== 0);
3854
+ cleanRows.push(clean);
3855
+ }
3856
+ if (cleanRows.length === 0) return null;
3857
+ let cols = 0;
3858
+ for (const row of cleanRows) {
3859
+ let c = 0;
3860
+ for (const cell of row) c += cell.colSpan;
3861
+ if (c > cols) cols = c;
3862
+ }
3863
+ const table = {
3864
+ rows: cleanRows.length,
3865
+ cols,
3866
+ cells: cleanRows,
3867
+ hasHeader: cleanRows.length > 1
3868
+ };
3869
+ return { type: "table", table };
3870
+ }
3871
+ async function extractImages(zip, rels, doc) {
3872
+ const blocks = [];
3873
+ const images = [];
3874
+ const drawingElements = findElements(doc.documentElement, "drawing");
3875
+ let imgIdx = 0;
3876
+ for (const drawing of drawingElements) {
3877
+ const blips = findElements(drawing, "blip");
3878
+ for (const blip of blips) {
3879
+ const embedId = getAttr(blip, "embed");
3880
+ if (!embedId) continue;
3881
+ const target = rels.get(embedId);
3882
+ if (!target) continue;
3883
+ const imgPath = target.startsWith("/") ? target.slice(1) : target.startsWith("word/") ? target : `word/${target}`;
3884
+ const imgFile = zip.file(imgPath);
3885
+ if (!imgFile) continue;
3886
+ try {
3887
+ const data = await imgFile.async("uint8array");
3888
+ imgIdx++;
3889
+ const ext = imgPath.split(".").pop()?.toLowerCase() ?? "png";
3890
+ const mimeMap = {
3891
+ png: "image/png",
3892
+ jpg: "image/jpeg",
3893
+ jpeg: "image/jpeg",
3894
+ gif: "image/gif",
3895
+ bmp: "image/bmp",
3896
+ wmf: "image/wmf",
3897
+ emf: "image/emf"
3898
+ };
3899
+ const filename = `image_${String(imgIdx).padStart(3, "0")}.${ext}`;
3900
+ images.push({ filename, data, mimeType: mimeMap[ext] ?? "image/png" });
3901
+ blocks.push({ type: "image", text: filename });
3902
+ } catch {
3903
+ }
3904
+ }
3905
+ }
3906
+ return { blocks, images };
3907
+ }
3908
+ async function parseDocxDocument(buffer, options) {
3909
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
3910
+ const zip = await JSZip4.loadAsync(buffer);
3911
+ const warnings = [];
3912
+ const docFile = zip.file("word/document.xml");
3913
+ if (!docFile) {
3914
+ throw new KordocError("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 DOCX \uD30C\uC77C: word/document.xml\uC774 \uC5C6\uC2B5\uB2C8\uB2E4");
3915
+ }
3916
+ let rels = /* @__PURE__ */ new Map();
3917
+ const relsFile = zip.file("word/_rels/document.xml.rels");
3918
+ if (relsFile) {
3919
+ rels = parseRels2(await relsFile.async("text"));
3920
+ }
3921
+ let styles = /* @__PURE__ */ new Map();
3922
+ const stylesFile = zip.file("word/styles.xml");
3923
+ if (stylesFile) {
3924
+ try {
3925
+ styles = parseStyles(await stylesFile.async("text"));
3926
+ } catch {
3927
+ }
3928
+ }
3929
+ let numbering = /* @__PURE__ */ new Map();
3930
+ const numFile = zip.file("word/numbering.xml");
3931
+ if (numFile) {
3932
+ try {
3933
+ numbering = parseNumbering(await numFile.async("text"));
3934
+ } catch {
3935
+ }
3936
+ }
3937
+ let footnotes = /* @__PURE__ */ new Map();
3938
+ const fnFile = zip.file("word/footnotes.xml");
3939
+ if (fnFile) {
3940
+ try {
3941
+ footnotes = parseFootnotes(await fnFile.async("text"));
3942
+ } catch {
3943
+ }
3944
+ }
3945
+ const docXml = await docFile.async("text");
3946
+ const doc = parseXml2(docXml);
3947
+ const body = findElements(doc, "body");
3948
+ if (body.length === 0) {
3949
+ throw new KordocError("DOCX \uBCF8\uBB38(w:body)\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
3950
+ }
3951
+ const blocks = [];
3952
+ const bodyEl = body[0];
3953
+ const children = bodyEl.childNodes;
3954
+ for (let i = 0; i < children.length; i++) {
3955
+ const node = children[i];
3956
+ if (node.nodeType !== 1) continue;
3957
+ const el = node;
3958
+ const localName = el.localName ?? el.tagName?.split(":").pop();
3959
+ if (localName === "p") {
3960
+ const block = parseParagraph(el, styles, numbering, footnotes, rels);
3961
+ if (block) blocks.push(block);
3962
+ } else if (localName === "tbl") {
3963
+ const block = parseTable(el, styles, numbering, footnotes, rels);
3964
+ if (block) blocks.push(block);
3965
+ }
3966
+ }
3967
+ const { blocks: imgBlocks, images } = await extractImages(zip, rels, doc);
3968
+ const metadata = {};
3969
+ const coreFile = zip.file("docProps/core.xml");
3970
+ if (coreFile) {
3971
+ try {
3972
+ const coreXml = await coreFile.async("text");
3973
+ const coreDoc = parseXml2(coreXml);
3974
+ const getFirst = (tag) => {
3975
+ const els = coreDoc.getElementsByTagName(tag);
3976
+ return els.length > 0 ? (els[0].textContent ?? "").trim() : void 0;
3977
+ };
3978
+ metadata.title = getFirst("dc:title") || getFirst("dcterms:title");
3979
+ metadata.author = getFirst("dc:creator");
3980
+ metadata.description = getFirst("dc:description");
3981
+ const created = getFirst("dcterms:created");
3982
+ if (created) metadata.createdAt = created;
3983
+ const modified = getFirst("dcterms:modified");
3984
+ if (modified) metadata.modifiedAt = modified;
3985
+ } catch {
3986
+ }
3987
+ }
3988
+ const outline = blocks.filter((b) => b.type === "heading").map((b) => ({ level: b.level ?? 2, text: b.text ?? "" }));
3989
+ const markdown = blocksToMarkdown(blocks);
3990
+ return {
3991
+ markdown,
3992
+ blocks,
3993
+ metadata,
3994
+ outline: outline.length > 0 ? outline : void 0,
3995
+ warnings: warnings.length > 0 ? warnings : void 0,
3996
+ images: images.length > 0 ? images : void 0
3997
+ };
3998
+ }
3999
+
3013
4000
  // src/diff/text-diff.ts
3014
4001
  function similarity(a, b) {
3015
4002
  if (a === b) return 1;
@@ -3308,12 +4295,12 @@ function extractInlineFields(text) {
3308
4295
  }
3309
4296
 
3310
4297
  // src/hwpx/generator.ts
3311
- import JSZip2 from "jszip";
4298
+ import JSZip5 from "jszip";
3312
4299
  var HWPML_NS = "http://www.hancom.co.kr/hwpml/2016/HwpMl";
3313
4300
  async function markdownToHwpx(markdown) {
3314
4301
  const blocks = parseMarkdownToBlocks(markdown);
3315
4302
  const sectionXml = blocksToSectionXml(blocks);
3316
- const zip = new JSZip2();
4303
+ const zip = new JSZip5();
3317
4304
  zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
3318
4305
  zip.file("Contents/content.hpf", generateManifest());
3319
4306
  zip.file("Contents/section0.xml", sectionXml);
@@ -3423,8 +4410,12 @@ async function parse(input, options) {
3423
4410
  }
3424
4411
  const format = detectFormat(buffer);
3425
4412
  switch (format) {
3426
- case "hwpx":
4413
+ case "hwpx": {
4414
+ const zipFormat = await detectZipFormat(buffer);
4415
+ if (zipFormat === "xlsx") return parseXlsx(buffer, options);
4416
+ if (zipFormat === "docx") return parseDocx(buffer, options);
3427
4417
  return parseHwpx(buffer, options);
4418
+ }
3428
4419
  case "hwp":
3429
4420
  return parseHwp(buffer, options);
3430
4421
  case "pdf":
@@ -3451,9 +4442,27 @@ async function parseHwp(buffer, options) {
3451
4442
  }
3452
4443
  async function parsePdf(buffer, options) {
3453
4444
  try {
3454
- return await parsePdfDocument(buffer, options);
4445
+ const { markdown, blocks, metadata, outline, warnings, isImageBased } = await parsePdfDocument(buffer, options);
4446
+ return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased };
4447
+ } catch (err) {
4448
+ const isImageBased = err instanceof Error && "isImageBased" in err ? true : void 0;
4449
+ return { success: false, fileType: "pdf", error: err instanceof Error ? err.message : "PDF \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err), isImageBased };
4450
+ }
4451
+ }
4452
+ async function parseXlsx(buffer, options) {
4453
+ try {
4454
+ const { markdown, blocks, metadata, warnings } = await parseXlsxDocument(buffer, options);
4455
+ return { success: true, fileType: "xlsx", markdown, blocks, metadata, warnings };
4456
+ } catch (err) {
4457
+ return { success: false, fileType: "xlsx", error: err instanceof Error ? err.message : "XLSX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
4458
+ }
4459
+ }
4460
+ async function parseDocx(buffer, options) {
4461
+ try {
4462
+ const { markdown, blocks, metadata, outline, warnings, images } = await parseDocxDocument(buffer, options);
4463
+ return { success: true, fileType: "docx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
3455
4464
  } catch (err) {
3456
- return { success: false, fileType: "pdf", error: err instanceof Error ? err.message : "PDF \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
4465
+ return { success: false, fileType: "docx", error: err instanceof Error ? err.message : "DOCX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
3457
4466
  }
3458
4467
  }
3459
4468
  export {
@@ -3461,15 +4470,19 @@ export {
3461
4470
  blocksToMarkdown,
3462
4471
  compare,
3463
4472
  detectFormat,
4473
+ detectZipFormat,
3464
4474
  diffBlocks,
3465
4475
  extractFormFields,
3466
4476
  isHwpxFile,
3467
4477
  isOldHwpFile,
3468
4478
  isPdfFile,
4479
+ isZipFile,
3469
4480
  markdownToHwpx,
3470
4481
  parse,
4482
+ parseDocx,
3471
4483
  parseHwp,
3472
4484
  parseHwpx,
3473
- parsePdf
4485
+ parsePdf,
4486
+ parseXlsx
3474
4487
  };
3475
4488
  //# sourceMappingURL=index.js.map