kordoc 1.6.0 → 1.7.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
@@ -50,6 +50,9 @@ var init_provider = __esm({
50
50
  }
51
51
  });
52
52
 
53
+ // src/index.ts
54
+ import { readFile } from "fs/promises";
55
+
53
56
  // src/detect.ts
54
57
  function magicBytes(buffer) {
55
58
  return new Uint8Array(buffer, 0, Math.min(4, buffer.byteLength));
@@ -80,6 +83,12 @@ import { inflateRawSync } from "zlib";
80
83
  import { DOMParser } from "@xmldom/xmldom";
81
84
 
82
85
  // src/table/builder.ts
86
+ var SAFE_HREF_RE = /^(?:https?:|mailto:|tel:|#)/i;
87
+ function sanitizeHref(href) {
88
+ const trimmed = href.trim();
89
+ if (!trimmed || !SAFE_HREF_RE.test(trimmed)) return null;
90
+ return trimmed;
91
+ }
83
92
  var MAX_COLS = 200;
84
93
  var MAX_ROWS = 1e4;
85
94
  function buildTable(rows) {
@@ -145,6 +154,10 @@ function blocksToMarkdown(blocks) {
145
154
  lines.push("", `${prefix} ${block.text}`, "");
146
155
  continue;
147
156
  }
157
+ if (block.type === "image" && block.text) {
158
+ lines.push("", `![image](${block.text})`, "");
159
+ continue;
160
+ }
148
161
  if (block.type === "separator") {
149
162
  lines.push("", "---", "");
150
163
  continue;
@@ -178,7 +191,8 @@ function blocksToMarkdown(blocks) {
178
191
  continue;
179
192
  }
180
193
  if (block.href) {
181
- text = `[${text}](${block.href})`;
194
+ const href = sanitizeHref(block.href);
195
+ if (href) text = `[${text}](${href})`;
182
196
  }
183
197
  if (block.footnoteText) {
184
198
  text += ` (\uC8FC: ${block.footnoteText})`;
@@ -240,7 +254,13 @@ function tableToMarkdown(table) {
240
254
  }
241
255
 
242
256
  // src/utils.ts
243
- var VERSION = true ? "1.6.0" : "0.0.0-dev";
257
+ var VERSION = true ? "1.7.0" : "0.0.0-dev";
258
+ function toArrayBuffer(buf) {
259
+ if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
260
+ return buf.buffer;
261
+ }
262
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
263
+ }
244
264
  var KordocError = class extends Error {
245
265
  constructor(message) {
246
266
  super(message);
@@ -248,6 +268,7 @@ var KordocError = class extends Error {
248
268
  }
249
269
  };
250
270
  function isPathTraversal(name) {
271
+ if (name.includes("\0")) return true;
251
272
  const normalized = name.replace(/\\/g, "/");
252
273
  return normalized.includes("..") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized);
253
274
  }
@@ -299,7 +320,16 @@ var MAX_ZIP_ENTRIES = 500;
299
320
  function clampSpan(val, max) {
300
321
  return Math.max(1, Math.min(val, max));
301
322
  }
302
- async function extractHwpxStyles(zip) {
323
+ var MAX_XML_DEPTH = 200;
324
+ function createXmlParser(warnings) {
325
+ return new DOMParser({
326
+ onError(level, msg) {
327
+ if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg}`);
328
+ warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg}` });
329
+ }
330
+ });
331
+ }
332
+ async function extractHwpxStyles(zip, decompressed) {
303
333
  const result = {
304
334
  charProperties: /* @__PURE__ */ new Map(),
305
335
  styles: /* @__PURE__ */ new Map()
@@ -311,7 +341,11 @@ async function extractHwpxStyles(zip) {
311
341
  if (!file) continue;
312
342
  try {
313
343
  const xml = await file.async("text");
314
- const parser = new DOMParser();
344
+ if (decompressed) {
345
+ decompressed.total += xml.length * 2;
346
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
347
+ }
348
+ const parser = createXmlParser();
315
349
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
316
350
  if (!doc.documentElement) continue;
317
351
  parseCharProperties(doc, result.charProperties);
@@ -389,37 +423,128 @@ async function parseHwpxDocument(buffer, options) {
389
423
  if (actualEntryCount > MAX_ZIP_ENTRIES) {
390
424
  throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
391
425
  }
426
+ const decompressed = { total: 0 };
392
427
  const metadata = {};
393
- await extractHwpxMetadata(zip, metadata);
394
- const styleMap = await extractHwpxStyles(zip);
428
+ await extractHwpxMetadata(zip, metadata, decompressed);
429
+ const styleMap = await extractHwpxStyles(zip, decompressed);
395
430
  const warnings = [];
396
431
  const sectionPaths = await resolveSectionPaths(zip);
397
432
  if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
398
433
  metadata.pageCount = sectionPaths.length;
399
434
  const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
400
- let totalDecompressed = 0;
435
+ const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
401
436
  const blocks = [];
437
+ let parsedSections = 0;
402
438
  for (let si = 0; si < sectionPaths.length; si++) {
403
439
  if (pageFilter && !pageFilter.has(si + 1)) continue;
404
440
  const file = zip.file(sectionPaths[si]);
405
441
  if (!file) continue;
406
- const xml = await file.async("text");
407
- totalDecompressed += xml.length * 2;
408
- if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
409
- blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1));
442
+ try {
443
+ const xml = await file.async("text");
444
+ decompressed.total += xml.length * 2;
445
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
446
+ blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1));
447
+ parsedSections++;
448
+ options?.onProgress?.(parsedSections, totalTarget);
449
+ } catch (secErr) {
450
+ if (secErr instanceof KordocError) throw secErr;
451
+ warnings.push({ page: si + 1, message: `\uC139\uC158 ${si + 1} \uD30C\uC2F1 \uC2E4\uD328: ${secErr instanceof Error ? secErr.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`, code: "PARTIAL_PARSE" });
452
+ }
410
453
  }
454
+ const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
411
455
  detectHwpxHeadings(blocks, styleMap);
412
456
  const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
413
457
  const markdown = blocksToMarkdown(blocks);
414
- return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
458
+ return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0, images: images.length > 0 ? images : void 0 };
459
+ }
460
+ function imageExtToMime(ext) {
461
+ switch (ext.toLowerCase()) {
462
+ case "jpg":
463
+ case "jpeg":
464
+ return "image/jpeg";
465
+ case "png":
466
+ return "image/png";
467
+ case "gif":
468
+ return "image/gif";
469
+ case "bmp":
470
+ return "image/bmp";
471
+ case "tif":
472
+ case "tiff":
473
+ return "image/tiff";
474
+ case "wmf":
475
+ return "image/wmf";
476
+ case "emf":
477
+ return "image/emf";
478
+ case "svg":
479
+ return "image/svg+xml";
480
+ default:
481
+ return "application/octet-stream";
482
+ }
483
+ }
484
+ function mimeToExt(mime) {
485
+ if (mime.includes("jpeg")) return "jpg";
486
+ if (mime.includes("png")) return "png";
487
+ if (mime.includes("gif")) return "gif";
488
+ if (mime.includes("bmp")) return "bmp";
489
+ if (mime.includes("tiff")) return "tif";
490
+ if (mime.includes("wmf")) return "wmf";
491
+ if (mime.includes("emf")) return "emf";
492
+ if (mime.includes("svg")) return "svg";
493
+ return "bin";
494
+ }
495
+ async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
496
+ const images = [];
497
+ let imageIndex = 0;
498
+ for (const block of blocks) {
499
+ if (block.type !== "image" || !block.text) continue;
500
+ const ref = block.text;
501
+ const candidates = [
502
+ `BinData/${ref}`,
503
+ `Contents/BinData/${ref}`,
504
+ ref
505
+ // 절대 경로일 수도 있음
506
+ ];
507
+ let found = false;
508
+ for (const path of candidates) {
509
+ if (isPathTraversal(path)) continue;
510
+ const file = zip.file(path);
511
+ if (!file) continue;
512
+ try {
513
+ const data = await file.async("uint8array");
514
+ decompressed.total += data.length;
515
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
516
+ const ext = ref.includes(".") ? ref.split(".").pop() : "png";
517
+ const mimeType = imageExtToMime(ext);
518
+ imageIndex++;
519
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
520
+ images.push({ filename, data, mimeType });
521
+ block.text = filename;
522
+ block.imageData = { data, mimeType, filename: ref };
523
+ found = true;
524
+ break;
525
+ } catch (err) {
526
+ if (err instanceof KordocError) throw err;
527
+ }
528
+ }
529
+ if (!found) {
530
+ warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
531
+ block.type = "paragraph";
532
+ block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
533
+ }
534
+ }
535
+ return images;
415
536
  }
416
- async function extractHwpxMetadata(zip, metadata) {
537
+ async function extractHwpxMetadata(zip, metadata, decompressed) {
417
538
  try {
418
539
  const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
419
540
  for (const mp of metaPaths) {
420
541
  const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
421
542
  if (!file) continue;
422
543
  const xml = await file.async("text");
544
+ if (decompressed) {
545
+ decompressed.total += xml.length * 2;
546
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
547
+ }
423
548
  parseDublinCoreMetadata(xml, metadata);
424
549
  if (metadata.title || metadata.author) return;
425
550
  }
@@ -427,7 +552,7 @@ async function extractHwpxMetadata(zip, metadata) {
427
552
  }
428
553
  }
429
554
  function parseDublinCoreMetadata(xml, metadata) {
430
- const parser = new DOMParser();
555
+ const parser = createXmlParser();
431
556
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
432
557
  if (!doc.documentElement) return;
433
558
  const getText = (tagNames) => {
@@ -491,7 +616,14 @@ function extractFromBrokenZip(buffer) {
491
616
  let totalDecompressed = 0;
492
617
  let entryCount = 0;
493
618
  while (pos < data.length - 30) {
494
- if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) break;
619
+ if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
620
+ pos++;
621
+ while (pos < data.length - 30) {
622
+ if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
623
+ pos++;
624
+ }
625
+ continue;
626
+ }
495
627
  if (++entryCount > MAX_ZIP_ENTRIES) break;
496
628
  const method = view.getUint16(pos + 8, true);
497
629
  const compSize = view.getUint32(pos + 18, true);
@@ -551,7 +683,7 @@ async function resolveSectionPaths(zip) {
551
683
  return sectionFiles.map((f) => f.name).sort();
552
684
  }
553
685
  function parseSectionPathsFromManifest(xml) {
554
- const parser = new DOMParser();
686
+ const parser = createXmlParser();
555
687
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
556
688
  const items = doc.getElementsByTagName("opf:item");
557
689
  const spine = doc.getElementsByTagName("opf:itemref");
@@ -613,14 +745,33 @@ function detectHwpxHeadings(blocks, styleMap) {
613
745
  }
614
746
  }
615
747
  function parseSectionXml(xml, styleMap, warnings, sectionNum) {
616
- const parser = new DOMParser();
748
+ const parser = createXmlParser(warnings);
617
749
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
618
750
  if (!doc.documentElement) return [];
619
751
  const blocks = [];
620
752
  walkSection(doc.documentElement, blocks, null, [], styleMap, warnings, sectionNum);
621
753
  return blocks;
622
754
  }
623
- function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum) {
755
+ function extractImageRef(el) {
756
+ const children = el.childNodes;
757
+ if (!children) return null;
758
+ for (let i = 0; i < children.length; i++) {
759
+ const child = children[i];
760
+ if (child.nodeType !== 1) continue;
761
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
762
+ if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
763
+ const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
764
+ if (ref) return ref;
765
+ }
766
+ const nested = extractImageRef(child);
767
+ if (nested) return nested;
768
+ }
769
+ const directRef = el.getAttribute("binaryItemIDRef") || "";
770
+ if (directRef) return directRef;
771
+ return null;
772
+ }
773
+ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
774
+ if (depth > MAX_XML_DEPTH) return;
624
775
  const children = node.childNodes;
625
776
  if (!children) return;
626
777
  for (let i = 0; i < children.length; i++) {
@@ -632,7 +783,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
632
783
  case "tbl": {
633
784
  if (tableCtx) tableStack.push(tableCtx);
634
785
  const newTable = { rows: [], currentRow: [], cell: null };
635
- walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
786
+ walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
636
787
  if (newTable.rows.length > 0) {
637
788
  if (tableStack.length > 0) {
638
789
  const parentTable = tableStack.pop();
@@ -653,7 +804,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
653
804
  case "tr":
654
805
  if (tableCtx) {
655
806
  tableCtx.currentRow = [];
656
- walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
807
+ walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
657
808
  if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
658
809
  tableCtx.currentRow = [];
659
810
  }
@@ -661,7 +812,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
661
812
  case "tc":
662
813
  if (tableCtx) {
663
814
  tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
664
- walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
815
+ walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
665
816
  if (tableCtx.cell) {
666
817
  tableCtx.currentRow.push(tableCtx.cell);
667
818
  tableCtx.cell = null;
@@ -689,24 +840,29 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
689
840
  blocks.push(block);
690
841
  }
691
842
  }
692
- tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
843
+ tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
693
844
  break;
694
845
  }
695
- // 이미지/그림 — 경고 수집
846
+ // 이미지/그림 — 경로 추출 또는 경고
696
847
  case "pic":
697
848
  case "shape":
698
- case "drawingObject":
699
- if (warnings && sectionNum) {
849
+ case "drawingObject": {
850
+ const imgRef = extractImageRef(el);
851
+ if (imgRef) {
852
+ blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
853
+ } else if (warnings && sectionNum) {
700
854
  warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
701
855
  }
702
856
  break;
857
+ }
703
858
  default:
704
- walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
859
+ walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
705
860
  break;
706
861
  }
707
862
  }
708
863
  }
709
- function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum) {
864
+ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
865
+ if (depth > MAX_XML_DEPTH) return tableCtx;
710
866
  const children = node.childNodes;
711
867
  if (!children) return tableCtx;
712
868
  for (let i = 0; i < children.length; i++) {
@@ -717,7 +873,7 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
717
873
  if (localTag === "tbl") {
718
874
  if (tableCtx) tableStack.push(tableCtx);
719
875
  const newTable = { rows: [], currentRow: [], cell: null };
720
- walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
876
+ walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
721
877
  if (newTable.rows.length > 0) {
722
878
  if (tableStack.length > 0) {
723
879
  const parentTable = tableStack.pop();
@@ -734,7 +890,10 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
734
890
  tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
735
891
  }
736
892
  } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
737
- if (warnings && sectionNum) {
893
+ const imgRef = extractImageRef(el);
894
+ if (imgRef) {
895
+ blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
896
+ } else if (warnings && sectionNum) {
738
897
  warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
739
898
  }
740
899
  }
@@ -950,6 +1109,7 @@ function extractText(data) {
950
1109
  break;
951
1110
  case CHAR_TAB:
952
1111
  result += " ";
1112
+ if (i + 14 <= data.length) i += 14;
953
1113
  break;
954
1114
  case CHAR_HYPHEN:
955
1115
  result += "-";
@@ -1006,24 +1166,34 @@ function parseHwp5Document(buffer, options) {
1006
1166
  if (sections.length === 0) throw new KordocError("\uC139\uC158 \uC2A4\uD2B8\uB9BC\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1007
1167
  metadata.pageCount = sections.length;
1008
1168
  const pageFilter = options?.pages ? parsePageRange(options.pages, sections.length) : null;
1169
+ const totalTarget = pageFilter ? pageFilter.size : sections.length;
1009
1170
  const blocks = [];
1010
1171
  let totalDecompressed = 0;
1172
+ let parsedSections = 0;
1011
1173
  for (let si = 0; si < sections.length; si++) {
1012
1174
  if (pageFilter && !pageFilter.has(si + 1)) continue;
1013
- const sectionData = sections[si];
1014
- const data = compressed ? decompressStream(Buffer.from(sectionData)) : Buffer.from(sectionData);
1015
- totalDecompressed += data.length;
1016
- if (totalDecompressed > MAX_TOTAL_DECOMPRESS) throw new KordocError("\uCD1D \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (decompression bomb \uC758\uC2EC)");
1017
- const records = readRecords(data);
1018
- const sectionBlocks = parseSection(records, docInfo, warnings, si + 1);
1019
- blocks.push(...sectionBlocks);
1020
- }
1175
+ try {
1176
+ const sectionData = sections[si];
1177
+ const data = compressed ? decompressStream(Buffer.from(sectionData)) : Buffer.from(sectionData);
1178
+ totalDecompressed += data.length;
1179
+ if (totalDecompressed > MAX_TOTAL_DECOMPRESS) throw new KordocError("\uCD1D \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (decompression bomb \uC758\uC2EC)");
1180
+ const records = readRecords(data);
1181
+ const sectionBlocks = parseSection(records, docInfo, warnings, si + 1);
1182
+ blocks.push(...sectionBlocks);
1183
+ parsedSections++;
1184
+ options?.onProgress?.(parsedSections, totalTarget);
1185
+ } catch (secErr) {
1186
+ if (secErr instanceof KordocError) throw secErr;
1187
+ warnings.push({ page: si + 1, message: `\uC139\uC158 ${si + 1} \uD30C\uC2F1 \uC2E4\uD328: ${secErr instanceof Error ? secErr.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`, code: "PARTIAL_PARSE" });
1188
+ }
1189
+ }
1190
+ const images = extractHwp5Images(cfb, blocks, compressed, warnings);
1021
1191
  if (docInfo) {
1022
1192
  detectHwp5Headings(blocks, docInfo);
1023
1193
  }
1024
1194
  const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
1025
1195
  const markdown = blocksToMarkdown(blocks);
1026
- return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
1196
+ return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0, images: images.length > 0 ? images : void 0 };
1027
1197
  }
1028
1198
  function parseDocInfoStream(cfb, compressed) {
1029
1199
  try {
@@ -1133,6 +1303,78 @@ function findSections(cfb) {
1133
1303
  }
1134
1304
  return sections.sort((a, b) => a.idx - b.idx).map((s) => s.content);
1135
1305
  }
1306
+ var TAG_SHAPE_COMPONENT = 74;
1307
+ function extractBinDataId(records, ctrlIdx) {
1308
+ const ctrlLevel = records[ctrlIdx].level;
1309
+ for (let j = ctrlIdx + 1; j < records.length && j < ctrlIdx + 50; j++) {
1310
+ const r = records[j];
1311
+ if (r.level <= ctrlLevel) break;
1312
+ if (r.data.length >= 2) {
1313
+ if (r.tagId > TAG_SHAPE_COMPONENT && r.level > ctrlLevel + 1 && r.data.length >= 4) {
1314
+ const possibleId = r.data.readUInt16LE(0);
1315
+ if (possibleId < 1e4) return possibleId;
1316
+ }
1317
+ }
1318
+ }
1319
+ return -1;
1320
+ }
1321
+ function detectImageMime(data) {
1322
+ if (data.length < 4) return null;
1323
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
1324
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
1325
+ if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
1326
+ if (data[0] === 66 && data[1] === 77) return "image/bmp";
1327
+ if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
1328
+ if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
1329
+ return null;
1330
+ }
1331
+ function extractHwp5Images(cfb, blocks, compressed, warnings) {
1332
+ 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 {
1344
+ }
1345
+ }
1346
+ binDataMap.set(idx, { data, name: entry.name || `BIN${idx}` });
1347
+ }
1348
+ if (binDataMap.size === 0) return [];
1349
+ const images = [];
1350
+ let imageIndex = 0;
1351
+ for (const block of blocks) {
1352
+ if (block.type !== "image" || !block.text) continue;
1353
+ const binId = parseInt(block.text, 10);
1354
+ if (isNaN(binId)) continue;
1355
+ const bin = binDataMap.get(binId);
1356
+ if (!bin) {
1357
+ warnings.push({ page: block.pageNumber, message: `BinData ${binId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
1358
+ block.type = "paragraph";
1359
+ block.text = `[\uC774\uBBF8\uC9C0: BinData ${binId}]`;
1360
+ continue;
1361
+ }
1362
+ const mime = detectImageMime(bin.data);
1363
+ if (!mime) {
1364
+ warnings.push({ page: block.pageNumber, message: `BinData ${binId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
1365
+ block.type = "paragraph";
1366
+ block.text = `[\uC774\uBBF8\uC9C0: ${bin.name}]`;
1367
+ continue;
1368
+ }
1369
+ imageIndex++;
1370
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
1371
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
1372
+ images.push({ filename, data: new Uint8Array(bin.data), mimeType: mime });
1373
+ block.text = filename;
1374
+ block.imageData = { data: new Uint8Array(bin.data), mimeType: mime, filename: bin.name };
1375
+ }
1376
+ return images;
1377
+ }
1136
1378
  function parseSection(records, docInfo, warnings, sectionNum) {
1137
1379
  const blocks = [];
1138
1380
  let i = 0;
@@ -1160,7 +1402,14 @@ function parseSection(records, docInfo, warnings, sectionNum) {
1160
1402
  i = nextIdx;
1161
1403
  continue;
1162
1404
  }
1163
- if (ctrlId === "gso " || ctrlId === " osg" || ctrlId === " elo" || ctrlId === "ole ") {
1405
+ if (ctrlId === "gso " || ctrlId === " osg") {
1406
+ const binId = extractBinDataId(records, i);
1407
+ if (binId >= 0) {
1408
+ blocks.push({ type: "image", text: String(binId), pageNumber: sectionNum });
1409
+ } else {
1410
+ warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
1411
+ }
1412
+ } else if (ctrlId === " elo" || ctrlId === "ole ") {
1164
1413
  warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
1165
1414
  }
1166
1415
  }
@@ -1250,9 +1499,13 @@ function parseCellBlock(records, startIdx, tableLevel) {
1250
1499
  const texts = [];
1251
1500
  let colSpan = 1;
1252
1501
  let rowSpan = 1;
1253
- if (rec.data.length >= 14) {
1254
- const cs = rec.data.readUInt16LE(10);
1255
- const rs = rec.data.readUInt16LE(12);
1502
+ let colAddr;
1503
+ let rowAddr;
1504
+ if (rec.data.length >= 16) {
1505
+ colAddr = rec.data.readUInt16LE(8);
1506
+ rowAddr = rec.data.readUInt16LE(10);
1507
+ const cs = rec.data.readUInt16LE(12);
1508
+ const rs = rec.data.readUInt16LE(14);
1256
1509
  if (cs > 0) colSpan = Math.min(cs, MAX_COLS);
1257
1510
  if (rs > 0) rowSpan = Math.min(rs, MAX_ROWS);
1258
1511
  }
@@ -1267,15 +1520,16 @@ function parseCellBlock(records, startIdx, tableLevel) {
1267
1520
  }
1268
1521
  i++;
1269
1522
  }
1270
- return { cell: { text: texts.join("\n"), colSpan, rowSpan }, nextIdx: i };
1523
+ return { cell: { text: texts.join("\n"), colSpan, rowSpan, colAddr, rowAddr }, nextIdx: i };
1271
1524
  }
1272
1525
  function arrangeCells(rows, cols, cells) {
1273
1526
  const grid = Array.from({ length: rows }, () => Array(cols).fill(null));
1274
- let cellIdx = 0;
1275
- for (let r = 0; r < rows && cellIdx < cells.length; r++) {
1276
- for (let c = 0; c < cols && cellIdx < cells.length; c++) {
1277
- if (grid[r][c] !== null) continue;
1278
- const cell = cells[cellIdx++];
1527
+ const hasAddr = cells.some((c) => c.colAddr !== void 0 && c.rowAddr !== void 0);
1528
+ if (hasAddr) {
1529
+ for (const cell of cells) {
1530
+ const r = cell.rowAddr ?? 0;
1531
+ const c = cell.colAddr ?? 0;
1532
+ if (r >= rows || c >= cols) continue;
1279
1533
  grid[r][c] = cell;
1280
1534
  for (let dr = 0; dr < cell.rowSpan; dr++) {
1281
1535
  for (let dc = 0; dc < cell.colSpan; dc++) {
@@ -1285,6 +1539,22 @@ function arrangeCells(rows, cols, cells) {
1285
1539
  }
1286
1540
  }
1287
1541
  }
1542
+ } else {
1543
+ let cellIdx = 0;
1544
+ for (let r = 0; r < rows && cellIdx < cells.length; r++) {
1545
+ for (let c = 0; c < cols && cellIdx < cells.length; c++) {
1546
+ if (grid[r][c] !== null) continue;
1547
+ const cell = cells[cellIdx++];
1548
+ grid[r][c] = cell;
1549
+ for (let dr = 0; dr < cell.rowSpan; dr++) {
1550
+ for (let dc = 0; dc < cell.colSpan; dc++) {
1551
+ if (dr === 0 && dc === 0) continue;
1552
+ if (r + dr < rows && c + dc < cols)
1553
+ grid[r + dr][c + dc] = { text: "", colSpan: 1, rowSpan: 1 };
1554
+ }
1555
+ }
1556
+ }
1557
+ }
1288
1558
  }
1289
1559
  return grid.map((row) => row.map((c) => c || { text: "", colSpan: 1, rowSpan: 1 }));
1290
1560
  }
@@ -1846,13 +2116,26 @@ import { getDocument, GlobalWorkerOptions } from "pdfjs-dist/legacy/build/pdf.mj
1846
2116
  GlobalWorkerOptions.workerSrc = "";
1847
2117
  var MAX_PAGES = 5e3;
1848
2118
  var MAX_TOTAL_TEXT = 100 * 1024 * 1024;
1849
- async function parsePdfDocument(buffer, options) {
1850
- const doc = await getDocument({
2119
+ var PDF_LOAD_TIMEOUT_MS = 3e4;
2120
+ async function loadPdfWithTimeout(buffer) {
2121
+ const loadingTask = getDocument({
1851
2122
  data: new Uint8Array(buffer),
1852
2123
  useSystemFonts: true,
1853
2124
  disableFontFace: true,
1854
2125
  isEvalSupported: false
1855
- }).promise;
2126
+ });
2127
+ return Promise.race([
2128
+ loadingTask.promise,
2129
+ new Promise(
2130
+ (_, reject) => setTimeout(() => {
2131
+ loadingTask.destroy();
2132
+ reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
2133
+ }, PDF_LOAD_TIMEOUT_MS)
2134
+ )
2135
+ ]);
2136
+ }
2137
+ async function parsePdfDocument(buffer, options) {
2138
+ const doc = await loadPdfWithTimeout(buffer);
1856
2139
  try {
1857
2140
  const pageCount = doc.numPages;
1858
2141
  if (pageCount === 0) return { success: false, fileType: "pdf", pageCount: 0, error: "PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", code: "PARSE_ERROR" };
@@ -1864,32 +2147,43 @@ async function parsePdfDocument(buffer, options) {
1864
2147
  let totalTextBytes = 0;
1865
2148
  const effectivePageCount = Math.min(pageCount, MAX_PAGES);
1866
2149
  const pageFilter = options?.pages ? parsePageRange(options.pages, effectivePageCount) : null;
2150
+ const totalTarget = pageFilter ? pageFilter.size : effectivePageCount;
1867
2151
  const allFontSizes = [];
2152
+ const pageHeights = /* @__PURE__ */ new Map();
2153
+ let parsedPages = 0;
1868
2154
  for (let i = 1; i <= effectivePageCount; i++) {
1869
2155
  if (pageFilter && !pageFilter.has(i)) continue;
1870
- const page = await doc.getPage(i);
1871
- const tc = await page.getTextContent();
1872
- const viewport = page.getViewport({ scale: 1 });
1873
- const rawItems = tc.items;
1874
- const items = normalizeItems(rawItems);
1875
- const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
1876
- if (hiddenCount > 0) {
1877
- warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
1878
- }
1879
- for (const item of visible) {
1880
- if (item.fontSize > 0) allFontSizes.push(item.fontSize);
1881
- }
1882
- const opList = await page.getOperatorList();
1883
- const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
1884
- for (const b of pageBlocks) blocks.push(b);
1885
- for (const b of pageBlocks) {
1886
- const t = b.text || "";
1887
- totalChars += t.replace(/\s/g, "").length;
1888
- totalTextBytes += t.length * 2;
1889
- }
1890
- if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
1891
- }
1892
- const parsedPageCount = pageFilter ? pageFilter.size : effectivePageCount;
2156
+ try {
2157
+ const page = await doc.getPage(i);
2158
+ const tc = await page.getTextContent();
2159
+ const viewport = page.getViewport({ scale: 1 });
2160
+ pageHeights.set(i, viewport.height);
2161
+ const rawItems = tc.items;
2162
+ const items = normalizeItems(rawItems);
2163
+ const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
2164
+ if (hiddenCount > 0) {
2165
+ warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
2166
+ }
2167
+ for (const item of visible) {
2168
+ if (item.fontSize > 0) allFontSizes.push(item.fontSize);
2169
+ }
2170
+ const opList = await page.getOperatorList();
2171
+ const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
2172
+ for (const b of pageBlocks) blocks.push(b);
2173
+ for (const b of pageBlocks) {
2174
+ const t = b.text || "";
2175
+ totalChars += t.replace(/\s/g, "").length;
2176
+ totalTextBytes += t.length * 2;
2177
+ }
2178
+ if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
2179
+ parsedPages++;
2180
+ options?.onProgress?.(parsedPages, totalTarget);
2181
+ } catch (pageErr) {
2182
+ if (pageErr instanceof KordocError) throw pageErr;
2183
+ warnings.push({ page: i, message: `\uD398\uC774\uC9C0 ${i} \uD30C\uC2F1 \uC2E4\uD328: ${pageErr instanceof Error ? pageErr.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}`, code: "PARTIAL_PARSE" });
2184
+ }
2185
+ }
2186
+ const parsedPageCount = parsedPages || (pageFilter ? pageFilter.size : effectivePageCount);
1893
2187
  if (totalChars / Math.max(parsedPageCount, 1) < 10) {
1894
2188
  if (options?.ocr) {
1895
2189
  try {
@@ -1904,6 +2198,12 @@ async function parsePdfDocument(buffer, options) {
1904
2198
  }
1905
2199
  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" };
1906
2200
  }
2201
+ if (options?.removeHeaderFooter && parsedPageCount >= 3) {
2202
+ const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
2203
+ for (let ri = removed.length - 1; ri >= 0; ri--) {
2204
+ blocks.splice(removed[ri], 1);
2205
+ }
2206
+ }
1907
2207
  const medianFontSize = computeMedianFontSize(allFontSizes);
1908
2208
  if (medianFontSize > 0) {
1909
2209
  detectHeadings(blocks, medianFontSize);
@@ -2085,8 +2385,8 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
2085
2385
  () => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
2086
2386
  );
2087
2387
  for (const cell of cells) {
2088
- const textItems2 = cellTextMap.get(cell) || [];
2089
- const text = cellTextToString(textItems2);
2388
+ const cellItems = cellTextMap.get(cell) || [];
2389
+ const text = cellTextToString(cellItems);
2090
2390
  irGrid[cell.row][cell.col] = {
2091
2391
  text,
2092
2392
  colSpan: cell.colSpan,
@@ -2504,6 +2804,7 @@ function detectListBlocks(blocks) {
2504
2804
  return result;
2505
2805
  }
2506
2806
  var KOREAN_TABLE_HEADER_RE = /^\(?(구분|항목|종류|분류|유형|대상|내용|기간|금액|비율|방법|절차|요건|조건|근거|목적|범위|기준)\)?[:\s]/;
2807
+ var KV_FALSE_POSITIVE_RE = /\d{1,2}:\d{2}|:\/\/|\d+:\d+/;
2507
2808
  function detectSpecialKoreanTables(blocks) {
2508
2809
  const result = [];
2509
2810
  let kvLines = [];
@@ -2569,16 +2870,18 @@ function detectSpecialKoreanTables(blocks) {
2569
2870
  }
2570
2871
  continue;
2571
2872
  }
2572
- if (kvLines.length > 0 && text.includes(":") && !text.includes("(") && !text.includes(")")) {
2573
- const colonIdx = text.indexOf(":");
2574
- const key = text.slice(0, colonIdx).trim();
2575
- if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
2576
- kvLines.push({
2577
- key,
2578
- value: text.slice(colonIdx + 1).trim(),
2579
- block
2580
- });
2581
- continue;
2873
+ if (kvLines.length > 0 && text.includes(":")) {
2874
+ if (!KV_FALSE_POSITIVE_RE.test(text) && !text.includes("(") && !text.includes(")")) {
2875
+ const colonIdx = text.indexOf(":");
2876
+ const key = text.slice(0, colonIdx).trim();
2877
+ if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
2878
+ kvLines.push({
2879
+ key,
2880
+ value: text.slice(colonIdx + 1).trim(),
2881
+ block
2882
+ });
2883
+ continue;
2884
+ }
2582
2885
  }
2583
2886
  }
2584
2887
  flushKvTable();
@@ -2587,6 +2890,62 @@ function detectSpecialKoreanTables(blocks) {
2587
2890
  flushKvTable();
2588
2891
  return result;
2589
2892
  }
2893
+ function removeHeaderFooterBlocks(blocks, pageHeights, warnings) {
2894
+ const ZONE_RATIO = 0.1;
2895
+ const MIN_REPEAT = 3;
2896
+ const headerTexts = /* @__PURE__ */ new Map();
2897
+ const footerTexts = /* @__PURE__ */ new Map();
2898
+ for (let bi = 0; bi < blocks.length; bi++) {
2899
+ const b = blocks[bi];
2900
+ if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
2901
+ const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
2902
+ if (!ph) continue;
2903
+ const blockTop = ph - (b.bbox.y + b.bbox.height);
2904
+ const blockBottom = ph - b.bbox.y;
2905
+ if (blockBottom <= ph * ZONE_RATIO) {
2906
+ const arr = footerTexts.get(b.pageNumber) || [];
2907
+ arr.push(b.text.trim());
2908
+ footerTexts.set(b.pageNumber, arr);
2909
+ } else if (blockTop >= ph * (1 - ZONE_RATIO)) {
2910
+ const arr = headerTexts.get(b.pageNumber) || [];
2911
+ arr.push(b.text.trim());
2912
+ headerTexts.set(b.pageNumber, arr);
2913
+ }
2914
+ }
2915
+ const repeatedPatterns = /* @__PURE__ */ new Set();
2916
+ for (const textsMap of [headerTexts, footerTexts]) {
2917
+ const patternCount = /* @__PURE__ */ new Map();
2918
+ for (const [, texts] of textsMap) {
2919
+ for (const t of texts) {
2920
+ const normalized = t.replace(/\d+/g, "#");
2921
+ patternCount.set(normalized, (patternCount.get(normalized) || 0) + 1);
2922
+ }
2923
+ }
2924
+ for (const [pattern, count] of patternCount) {
2925
+ if (count >= MIN_REPEAT) repeatedPatterns.add(pattern);
2926
+ }
2927
+ }
2928
+ if (repeatedPatterns.size === 0) return [];
2929
+ const removeIndices = [];
2930
+ for (let bi = 0; bi < blocks.length; bi++) {
2931
+ const b = blocks[bi];
2932
+ if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
2933
+ const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
2934
+ if (!ph) continue;
2935
+ const blockTop = ph - (b.bbox.y + b.bbox.height);
2936
+ const blockBottom = ph - b.bbox.y;
2937
+ const inZone = blockBottom <= ph * ZONE_RATIO || blockTop >= ph * (1 - ZONE_RATIO);
2938
+ if (!inZone) continue;
2939
+ const normalized = b.text.trim().replace(/\d+/g, "#");
2940
+ if (repeatedPatterns.has(normalized)) {
2941
+ removeIndices.push(bi);
2942
+ }
2943
+ }
2944
+ if (removeIndices.length > 0) {
2945
+ warnings.push({ message: `${removeIndices.length}\uAC1C \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC694\uC18C \uC81C\uAC70\uB428`, code: "HIDDEN_TEXT_FILTERED" });
2946
+ }
2947
+ return removeIndices;
2948
+ }
2590
2949
  function mergeKoreanLines(text) {
2591
2950
  if (!text) return "";
2592
2951
  const lines = text.split("\n");
@@ -2599,7 +2958,7 @@ function mergeKoreanLines(text) {
2599
2958
  result.push(curr);
2600
2959
  continue;
2601
2960
  }
2602
- if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
2961
+ if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !curr.trimStart().startsWith("|") && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
2603
2962
  result[result.length - 1] = prev + " " + curr;
2604
2963
  } else {
2605
2964
  result.push(curr);
@@ -2622,7 +2981,9 @@ function normalizedSimilarity(a, b) {
2622
2981
  function normalize(s) {
2623
2982
  return s.replace(/\s+/g, " ").trim();
2624
2983
  }
2984
+ var MAX_LEVENSHTEIN_LEN = 1e4;
2625
2985
  function levenshtein(a, b) {
2986
+ if (a.length + b.length > MAX_LEVENSHTEIN_LEN) return Math.abs(a.length - b.length);
2626
2987
  if (a.length > b.length) [a, b] = [b, a];
2627
2988
  const m = a.length;
2628
2989
  const n = b.length;
@@ -2935,7 +3296,7 @@ function parseMarkdownToBlocks(md) {
2935
3296
  const tableRows = [];
2936
3297
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
2937
3298
  const row = lines[i];
2938
- if (/^\|[\s\-:]+\|/.test(row) && !row.includes("---") === false && /^[\s|:\-]+$/.test(row)) {
3299
+ if (/^[\s|:\-]+$/.test(row)) {
2939
3300
  i++;
2940
3301
  continue;
2941
3302
  }
@@ -2999,7 +3360,21 @@ function generateManifest() {
2999
3360
  }
3000
3361
 
3001
3362
  // src/index.ts
3002
- async function parse(buffer, options) {
3363
+ async function parse(input, options) {
3364
+ let buffer;
3365
+ if (typeof input === "string") {
3366
+ try {
3367
+ const buf = await readFile(input);
3368
+ buffer = toArrayBuffer(buf);
3369
+ } catch (err) {
3370
+ const msg = err instanceof Error && "code" in err && err.code === "ENOENT" ? `\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${input}` : `\uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${input}`;
3371
+ return { success: false, fileType: "unknown", error: msg, code: "PARSE_ERROR" };
3372
+ }
3373
+ } else if (Buffer.isBuffer(input)) {
3374
+ buffer = toArrayBuffer(input);
3375
+ } else {
3376
+ buffer = input;
3377
+ }
3003
3378
  if (!buffer || buffer.byteLength === 0) {
3004
3379
  return { success: false, fileType: "unknown", error: "\uBE48 \uBC84\uD37C\uC774\uAC70\uB098 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC785\uB825\uC785\uB2C8\uB2E4.", code: "EMPTY_INPUT" };
3005
3380
  }
@@ -3017,16 +3392,16 @@ async function parse(buffer, options) {
3017
3392
  }
3018
3393
  async function parseHwpx(buffer, options) {
3019
3394
  try {
3020
- const { markdown, blocks, metadata, outline, warnings } = await parseHwpxDocument(buffer, options);
3021
- return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings };
3395
+ const { markdown, blocks, metadata, outline, warnings, images } = await parseHwpxDocument(buffer, options);
3396
+ return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
3022
3397
  } catch (err) {
3023
3398
  return { success: false, fileType: "hwpx", error: err instanceof Error ? err.message : "HWPX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
3024
3399
  }
3025
3400
  }
3026
3401
  async function parseHwp(buffer, options) {
3027
3402
  try {
3028
- const { markdown, blocks, metadata, outline, warnings } = parseHwp5Document(Buffer.from(buffer), options);
3029
- return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings };
3403
+ const { markdown, blocks, metadata, outline, warnings, images } = parseHwp5Document(Buffer.from(buffer), options);
3404
+ return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
3030
3405
  } catch (err) {
3031
3406
  return { success: false, fileType: "hwp", error: err instanceof Error ? err.message : "HWP \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
3032
3407
  }