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.cjs CHANGED
@@ -91,6 +91,7 @@ __export(index_exports, {
91
91
  parsePdf: () => parsePdf
92
92
  });
93
93
  module.exports = __toCommonJS(index_exports);
94
+ var import_promises = require("fs/promises");
94
95
 
95
96
  // src/detect.ts
96
97
  function magicBytes(buffer) {
@@ -122,6 +123,12 @@ var import_zlib = require("zlib");
122
123
  var import_xmldom = require("@xmldom/xmldom");
123
124
 
124
125
  // src/table/builder.ts
126
+ var SAFE_HREF_RE = /^(?:https?:|mailto:|tel:|#)/i;
127
+ function sanitizeHref(href) {
128
+ const trimmed = href.trim();
129
+ if (!trimmed || !SAFE_HREF_RE.test(trimmed)) return null;
130
+ return trimmed;
131
+ }
125
132
  var MAX_COLS = 200;
126
133
  var MAX_ROWS = 1e4;
127
134
  function buildTable(rows) {
@@ -187,6 +194,10 @@ function blocksToMarkdown(blocks) {
187
194
  lines.push("", `${prefix} ${block.text}`, "");
188
195
  continue;
189
196
  }
197
+ if (block.type === "image" && block.text) {
198
+ lines.push("", `![image](${block.text})`, "");
199
+ continue;
200
+ }
190
201
  if (block.type === "separator") {
191
202
  lines.push("", "---", "");
192
203
  continue;
@@ -220,7 +231,8 @@ function blocksToMarkdown(blocks) {
220
231
  continue;
221
232
  }
222
233
  if (block.href) {
223
- text = `[${text}](${block.href})`;
234
+ const href = sanitizeHref(block.href);
235
+ if (href) text = `[${text}](${href})`;
224
236
  }
225
237
  if (block.footnoteText) {
226
238
  text += ` (\uC8FC: ${block.footnoteText})`;
@@ -282,7 +294,13 @@ function tableToMarkdown(table) {
282
294
  }
283
295
 
284
296
  // src/utils.ts
285
- var VERSION = true ? "1.6.0" : "0.0.0-dev";
297
+ var VERSION = true ? "1.7.0" : "0.0.0-dev";
298
+ function toArrayBuffer(buf) {
299
+ if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
300
+ return buf.buffer;
301
+ }
302
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
303
+ }
286
304
  var KordocError = class extends Error {
287
305
  constructor(message) {
288
306
  super(message);
@@ -290,6 +308,7 @@ var KordocError = class extends Error {
290
308
  }
291
309
  };
292
310
  function isPathTraversal(name) {
311
+ if (name.includes("\0")) return true;
293
312
  const normalized = name.replace(/\\/g, "/");
294
313
  return normalized.includes("..") || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized);
295
314
  }
@@ -341,7 +360,16 @@ var MAX_ZIP_ENTRIES = 500;
341
360
  function clampSpan(val, max) {
342
361
  return Math.max(1, Math.min(val, max));
343
362
  }
344
- async function extractHwpxStyles(zip) {
363
+ var MAX_XML_DEPTH = 200;
364
+ function createXmlParser(warnings) {
365
+ return new import_xmldom.DOMParser({
366
+ onError(level, msg) {
367
+ if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg}`);
368
+ warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg}` });
369
+ }
370
+ });
371
+ }
372
+ async function extractHwpxStyles(zip, decompressed) {
345
373
  const result = {
346
374
  charProperties: /* @__PURE__ */ new Map(),
347
375
  styles: /* @__PURE__ */ new Map()
@@ -353,7 +381,11 @@ async function extractHwpxStyles(zip) {
353
381
  if (!file) continue;
354
382
  try {
355
383
  const xml = await file.async("text");
356
- const parser = new import_xmldom.DOMParser();
384
+ if (decompressed) {
385
+ decompressed.total += xml.length * 2;
386
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
387
+ }
388
+ const parser = createXmlParser();
357
389
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
358
390
  if (!doc.documentElement) continue;
359
391
  parseCharProperties(doc, result.charProperties);
@@ -431,37 +463,128 @@ async function parseHwpxDocument(buffer, options) {
431
463
  if (actualEntryCount > MAX_ZIP_ENTRIES) {
432
464
  throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
433
465
  }
466
+ const decompressed = { total: 0 };
434
467
  const metadata = {};
435
- await extractHwpxMetadata(zip, metadata);
436
- const styleMap = await extractHwpxStyles(zip);
468
+ await extractHwpxMetadata(zip, metadata, decompressed);
469
+ const styleMap = await extractHwpxStyles(zip, decompressed);
437
470
  const warnings = [];
438
471
  const sectionPaths = await resolveSectionPaths(zip);
439
472
  if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
440
473
  metadata.pageCount = sectionPaths.length;
441
474
  const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
442
- let totalDecompressed = 0;
475
+ const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
443
476
  const blocks = [];
477
+ let parsedSections = 0;
444
478
  for (let si = 0; si < sectionPaths.length; si++) {
445
479
  if (pageFilter && !pageFilter.has(si + 1)) continue;
446
480
  const file = zip.file(sectionPaths[si]);
447
481
  if (!file) continue;
448
- const xml = await file.async("text");
449
- totalDecompressed += xml.length * 2;
450
- if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
451
- blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1));
482
+ try {
483
+ const xml = await file.async("text");
484
+ decompressed.total += xml.length * 2;
485
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
486
+ blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1));
487
+ parsedSections++;
488
+ options?.onProgress?.(parsedSections, totalTarget);
489
+ } catch (secErr) {
490
+ if (secErr instanceof KordocError) throw secErr;
491
+ 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" });
492
+ }
452
493
  }
494
+ const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
453
495
  detectHwpxHeadings(blocks, styleMap);
454
496
  const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
455
497
  const markdown = blocksToMarkdown(blocks);
456
- return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
498
+ 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 };
499
+ }
500
+ function imageExtToMime(ext) {
501
+ switch (ext.toLowerCase()) {
502
+ case "jpg":
503
+ case "jpeg":
504
+ return "image/jpeg";
505
+ case "png":
506
+ return "image/png";
507
+ case "gif":
508
+ return "image/gif";
509
+ case "bmp":
510
+ return "image/bmp";
511
+ case "tif":
512
+ case "tiff":
513
+ return "image/tiff";
514
+ case "wmf":
515
+ return "image/wmf";
516
+ case "emf":
517
+ return "image/emf";
518
+ case "svg":
519
+ return "image/svg+xml";
520
+ default:
521
+ return "application/octet-stream";
522
+ }
523
+ }
524
+ function mimeToExt(mime) {
525
+ if (mime.includes("jpeg")) return "jpg";
526
+ if (mime.includes("png")) return "png";
527
+ if (mime.includes("gif")) return "gif";
528
+ if (mime.includes("bmp")) return "bmp";
529
+ if (mime.includes("tiff")) return "tif";
530
+ if (mime.includes("wmf")) return "wmf";
531
+ if (mime.includes("emf")) return "emf";
532
+ if (mime.includes("svg")) return "svg";
533
+ return "bin";
534
+ }
535
+ async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
536
+ const images = [];
537
+ let imageIndex = 0;
538
+ for (const block of blocks) {
539
+ if (block.type !== "image" || !block.text) continue;
540
+ const ref = block.text;
541
+ const candidates = [
542
+ `BinData/${ref}`,
543
+ `Contents/BinData/${ref}`,
544
+ ref
545
+ // 절대 경로일 수도 있음
546
+ ];
547
+ let found = false;
548
+ for (const path of candidates) {
549
+ if (isPathTraversal(path)) continue;
550
+ const file = zip.file(path);
551
+ if (!file) continue;
552
+ try {
553
+ const data = await file.async("uint8array");
554
+ decompressed.total += data.length;
555
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
556
+ const ext = ref.includes(".") ? ref.split(".").pop() : "png";
557
+ const mimeType = imageExtToMime(ext);
558
+ imageIndex++;
559
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
560
+ images.push({ filename, data, mimeType });
561
+ block.text = filename;
562
+ block.imageData = { data, mimeType, filename: ref };
563
+ found = true;
564
+ break;
565
+ } catch (err) {
566
+ if (err instanceof KordocError) throw err;
567
+ }
568
+ }
569
+ if (!found) {
570
+ warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
571
+ block.type = "paragraph";
572
+ block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
573
+ }
574
+ }
575
+ return images;
457
576
  }
458
- async function extractHwpxMetadata(zip, metadata) {
577
+ async function extractHwpxMetadata(zip, metadata, decompressed) {
459
578
  try {
460
579
  const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
461
580
  for (const mp of metaPaths) {
462
581
  const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
463
582
  if (!file) continue;
464
583
  const xml = await file.async("text");
584
+ if (decompressed) {
585
+ decompressed.total += xml.length * 2;
586
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
587
+ }
465
588
  parseDublinCoreMetadata(xml, metadata);
466
589
  if (metadata.title || metadata.author) return;
467
590
  }
@@ -469,7 +592,7 @@ async function extractHwpxMetadata(zip, metadata) {
469
592
  }
470
593
  }
471
594
  function parseDublinCoreMetadata(xml, metadata) {
472
- const parser = new import_xmldom.DOMParser();
595
+ const parser = createXmlParser();
473
596
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
474
597
  if (!doc.documentElement) return;
475
598
  const getText = (tagNames) => {
@@ -533,7 +656,14 @@ function extractFromBrokenZip(buffer) {
533
656
  let totalDecompressed = 0;
534
657
  let entryCount = 0;
535
658
  while (pos < data.length - 30) {
536
- if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) break;
659
+ if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
660
+ pos++;
661
+ while (pos < data.length - 30) {
662
+ if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
663
+ pos++;
664
+ }
665
+ continue;
666
+ }
537
667
  if (++entryCount > MAX_ZIP_ENTRIES) break;
538
668
  const method = view.getUint16(pos + 8, true);
539
669
  const compSize = view.getUint32(pos + 18, true);
@@ -593,7 +723,7 @@ async function resolveSectionPaths(zip) {
593
723
  return sectionFiles.map((f) => f.name).sort();
594
724
  }
595
725
  function parseSectionPathsFromManifest(xml) {
596
- const parser = new import_xmldom.DOMParser();
726
+ const parser = createXmlParser();
597
727
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
598
728
  const items = doc.getElementsByTagName("opf:item");
599
729
  const spine = doc.getElementsByTagName("opf:itemref");
@@ -655,14 +785,33 @@ function detectHwpxHeadings(blocks, styleMap) {
655
785
  }
656
786
  }
657
787
  function parseSectionXml(xml, styleMap, warnings, sectionNum) {
658
- const parser = new import_xmldom.DOMParser();
788
+ const parser = createXmlParser(warnings);
659
789
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
660
790
  if (!doc.documentElement) return [];
661
791
  const blocks = [];
662
792
  walkSection(doc.documentElement, blocks, null, [], styleMap, warnings, sectionNum);
663
793
  return blocks;
664
794
  }
665
- function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum) {
795
+ function extractImageRef(el) {
796
+ const children = el.childNodes;
797
+ if (!children) return null;
798
+ for (let i = 0; i < children.length; i++) {
799
+ const child = children[i];
800
+ if (child.nodeType !== 1) continue;
801
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
802
+ if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
803
+ const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
804
+ if (ref) return ref;
805
+ }
806
+ const nested = extractImageRef(child);
807
+ if (nested) return nested;
808
+ }
809
+ const directRef = el.getAttribute("binaryItemIDRef") || "";
810
+ if (directRef) return directRef;
811
+ return null;
812
+ }
813
+ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
814
+ if (depth > MAX_XML_DEPTH) return;
666
815
  const children = node.childNodes;
667
816
  if (!children) return;
668
817
  for (let i = 0; i < children.length; i++) {
@@ -674,7 +823,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
674
823
  case "tbl": {
675
824
  if (tableCtx) tableStack.push(tableCtx);
676
825
  const newTable = { rows: [], currentRow: [], cell: null };
677
- walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
826
+ walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
678
827
  if (newTable.rows.length > 0) {
679
828
  if (tableStack.length > 0) {
680
829
  const parentTable = tableStack.pop();
@@ -695,7 +844,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
695
844
  case "tr":
696
845
  if (tableCtx) {
697
846
  tableCtx.currentRow = [];
698
- walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
847
+ walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
699
848
  if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
700
849
  tableCtx.currentRow = [];
701
850
  }
@@ -703,7 +852,7 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
703
852
  case "tc":
704
853
  if (tableCtx) {
705
854
  tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
706
- walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
855
+ walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
707
856
  if (tableCtx.cell) {
708
857
  tableCtx.currentRow.push(tableCtx.cell);
709
858
  tableCtx.cell = null;
@@ -731,24 +880,29 @@ function walkSection(node, blocks, tableCtx, tableStack, styleMap, warnings, sec
731
880
  blocks.push(block);
732
881
  }
733
882
  }
734
- tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
883
+ tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
735
884
  break;
736
885
  }
737
- // 이미지/그림 — 경고 수집
886
+ // 이미지/그림 — 경로 추출 또는 경고
738
887
  case "pic":
739
888
  case "shape":
740
- case "drawingObject":
741
- if (warnings && sectionNum) {
889
+ case "drawingObject": {
890
+ const imgRef = extractImageRef(el);
891
+ if (imgRef) {
892
+ blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
893
+ } else if (warnings && sectionNum) {
742
894
  warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
743
895
  }
744
896
  break;
897
+ }
745
898
  default:
746
- walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum);
899
+ walkSection(el, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth + 1);
747
900
  break;
748
901
  }
749
902
  }
750
903
  }
751
- function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum) {
904
+ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, warnings, sectionNum, depth = 0) {
905
+ if (depth > MAX_XML_DEPTH) return tableCtx;
752
906
  const children = node.childNodes;
753
907
  if (!children) return tableCtx;
754
908
  for (let i = 0; i < children.length; i++) {
@@ -759,7 +913,7 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
759
913
  if (localTag === "tbl") {
760
914
  if (tableCtx) tableStack.push(tableCtx);
761
915
  const newTable = { rows: [], currentRow: [], cell: null };
762
- walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum);
916
+ walkSection(el, blocks, newTable, tableStack, styleMap, warnings, sectionNum, depth + 1);
763
917
  if (newTable.rows.length > 0) {
764
918
  if (tableStack.length > 0) {
765
919
  const parentTable = tableStack.pop();
@@ -776,7 +930,10 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, styleMap, war
776
930
  tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
777
931
  }
778
932
  } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
779
- if (warnings && sectionNum) {
933
+ const imgRef = extractImageRef(el);
934
+ if (imgRef) {
935
+ blocks.push({ type: "image", text: imgRef, pageNumber: sectionNum });
936
+ } else if (warnings && sectionNum) {
780
937
  warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
781
938
  }
782
939
  }
@@ -992,6 +1149,7 @@ function extractText(data) {
992
1149
  break;
993
1150
  case CHAR_TAB:
994
1151
  result += " ";
1152
+ if (i + 14 <= data.length) i += 14;
995
1153
  break;
996
1154
  case CHAR_HYPHEN:
997
1155
  result += "-";
@@ -1049,24 +1207,34 @@ function parseHwp5Document(buffer, options) {
1049
1207
  if (sections.length === 0) throw new KordocError("\uC139\uC158 \uC2A4\uD2B8\uB9BC\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1050
1208
  metadata.pageCount = sections.length;
1051
1209
  const pageFilter = options?.pages ? parsePageRange(options.pages, sections.length) : null;
1210
+ const totalTarget = pageFilter ? pageFilter.size : sections.length;
1052
1211
  const blocks = [];
1053
1212
  let totalDecompressed = 0;
1213
+ let parsedSections = 0;
1054
1214
  for (let si = 0; si < sections.length; si++) {
1055
1215
  if (pageFilter && !pageFilter.has(si + 1)) continue;
1056
- const sectionData = sections[si];
1057
- const data = compressed ? decompressStream(Buffer.from(sectionData)) : Buffer.from(sectionData);
1058
- totalDecompressed += data.length;
1059
- if (totalDecompressed > MAX_TOTAL_DECOMPRESS) throw new KordocError("\uCD1D \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (decompression bomb \uC758\uC2EC)");
1060
- const records = readRecords(data);
1061
- const sectionBlocks = parseSection(records, docInfo, warnings, si + 1);
1062
- blocks.push(...sectionBlocks);
1063
- }
1216
+ try {
1217
+ const sectionData = sections[si];
1218
+ const data = compressed ? decompressStream(Buffer.from(sectionData)) : Buffer.from(sectionData);
1219
+ totalDecompressed += data.length;
1220
+ if (totalDecompressed > MAX_TOTAL_DECOMPRESS) throw new KordocError("\uCD1D \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (decompression bomb \uC758\uC2EC)");
1221
+ const records = readRecords(data);
1222
+ const sectionBlocks = parseSection(records, docInfo, warnings, si + 1);
1223
+ blocks.push(...sectionBlocks);
1224
+ parsedSections++;
1225
+ options?.onProgress?.(parsedSections, totalTarget);
1226
+ } catch (secErr) {
1227
+ if (secErr instanceof KordocError) throw secErr;
1228
+ 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" });
1229
+ }
1230
+ }
1231
+ const images = extractHwp5Images(cfb, blocks, compressed, warnings);
1064
1232
  if (docInfo) {
1065
1233
  detectHwp5Headings(blocks, docInfo);
1066
1234
  }
1067
1235
  const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
1068
1236
  const markdown = blocksToMarkdown(blocks);
1069
- return { markdown, blocks, metadata, outline: outline.length > 0 ? outline : void 0, warnings: warnings.length > 0 ? warnings : void 0 };
1237
+ 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 };
1070
1238
  }
1071
1239
  function parseDocInfoStream(cfb, compressed) {
1072
1240
  try {
@@ -1176,6 +1344,78 @@ function findSections(cfb) {
1176
1344
  }
1177
1345
  return sections.sort((a, b) => a.idx - b.idx).map((s) => s.content);
1178
1346
  }
1347
+ var TAG_SHAPE_COMPONENT = 74;
1348
+ function extractBinDataId(records, ctrlIdx) {
1349
+ const ctrlLevel = records[ctrlIdx].level;
1350
+ for (let j = ctrlIdx + 1; j < records.length && j < ctrlIdx + 50; j++) {
1351
+ const r = records[j];
1352
+ if (r.level <= ctrlLevel) break;
1353
+ if (r.data.length >= 2) {
1354
+ if (r.tagId > TAG_SHAPE_COMPONENT && r.level > ctrlLevel + 1 && r.data.length >= 4) {
1355
+ const possibleId = r.data.readUInt16LE(0);
1356
+ if (possibleId < 1e4) return possibleId;
1357
+ }
1358
+ }
1359
+ }
1360
+ return -1;
1361
+ }
1362
+ function detectImageMime(data) {
1363
+ if (data.length < 4) return null;
1364
+ if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "image/png";
1365
+ if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "image/jpeg";
1366
+ if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "image/gif";
1367
+ if (data[0] === 66 && data[1] === 77) return "image/bmp";
1368
+ if (data[0] === 215 && data[1] === 205 && data[2] === 198 && data[3] === 154) return "image/wmf";
1369
+ if (data[0] === 1 && data[1] === 0 && data[2] === 0 && data[3] === 0) return "image/emf";
1370
+ return null;
1371
+ }
1372
+ function extractHwp5Images(cfb, blocks, compressed, warnings) {
1373
+ const binDataMap = /* @__PURE__ */ new Map();
1374
+ for (let idx = 0; idx < 1e4; idx++) {
1375
+ const entry = CFB.find(cfb, `/BinData/BIN${String(idx).padStart(4, "0")}`) || CFB.find(cfb, `/BinData/Bin${String(idx).padStart(4, "0")}`);
1376
+ if (!entry?.content) {
1377
+ if (idx > 0) break;
1378
+ continue;
1379
+ }
1380
+ let data = Buffer.from(entry.content);
1381
+ if (compressed) {
1382
+ try {
1383
+ data = decompressStream(data);
1384
+ } catch {
1385
+ }
1386
+ }
1387
+ binDataMap.set(idx, { data, name: entry.name || `BIN${idx}` });
1388
+ }
1389
+ if (binDataMap.size === 0) return [];
1390
+ const images = [];
1391
+ let imageIndex = 0;
1392
+ for (const block of blocks) {
1393
+ if (block.type !== "image" || !block.text) continue;
1394
+ const binId = parseInt(block.text, 10);
1395
+ if (isNaN(binId)) continue;
1396
+ const bin = binDataMap.get(binId);
1397
+ if (!bin) {
1398
+ warnings.push({ page: block.pageNumber, message: `BinData ${binId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
1399
+ block.type = "paragraph";
1400
+ block.text = `[\uC774\uBBF8\uC9C0: BinData ${binId}]`;
1401
+ continue;
1402
+ }
1403
+ const mime = detectImageMime(bin.data);
1404
+ if (!mime) {
1405
+ warnings.push({ page: block.pageNumber, message: `BinData ${binId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
1406
+ block.type = "paragraph";
1407
+ block.text = `[\uC774\uBBF8\uC9C0: ${bin.name}]`;
1408
+ continue;
1409
+ }
1410
+ imageIndex++;
1411
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
1412
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
1413
+ images.push({ filename, data: new Uint8Array(bin.data), mimeType: mime });
1414
+ block.text = filename;
1415
+ block.imageData = { data: new Uint8Array(bin.data), mimeType: mime, filename: bin.name };
1416
+ }
1417
+ return images;
1418
+ }
1179
1419
  function parseSection(records, docInfo, warnings, sectionNum) {
1180
1420
  const blocks = [];
1181
1421
  let i = 0;
@@ -1203,7 +1443,14 @@ function parseSection(records, docInfo, warnings, sectionNum) {
1203
1443
  i = nextIdx;
1204
1444
  continue;
1205
1445
  }
1206
- if (ctrlId === "gso " || ctrlId === " osg" || ctrlId === " elo" || ctrlId === "ole ") {
1446
+ if (ctrlId === "gso " || ctrlId === " osg") {
1447
+ const binId = extractBinDataId(records, i);
1448
+ if (binId >= 0) {
1449
+ blocks.push({ type: "image", text: String(binId), pageNumber: sectionNum });
1450
+ } else {
1451
+ warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
1452
+ }
1453
+ } else if (ctrlId === " elo" || ctrlId === "ole ") {
1207
1454
  warnings.push({ page: sectionNum, message: `\uC2A4\uD0B5\uB41C \uC81C\uC5B4 \uC694\uC18C: ${ctrlId.trim()}`, code: "SKIPPED_IMAGE" });
1208
1455
  }
1209
1456
  }
@@ -1293,9 +1540,13 @@ function parseCellBlock(records, startIdx, tableLevel) {
1293
1540
  const texts = [];
1294
1541
  let colSpan = 1;
1295
1542
  let rowSpan = 1;
1296
- if (rec.data.length >= 14) {
1297
- const cs = rec.data.readUInt16LE(10);
1298
- const rs = rec.data.readUInt16LE(12);
1543
+ let colAddr;
1544
+ let rowAddr;
1545
+ if (rec.data.length >= 16) {
1546
+ colAddr = rec.data.readUInt16LE(8);
1547
+ rowAddr = rec.data.readUInt16LE(10);
1548
+ const cs = rec.data.readUInt16LE(12);
1549
+ const rs = rec.data.readUInt16LE(14);
1299
1550
  if (cs > 0) colSpan = Math.min(cs, MAX_COLS);
1300
1551
  if (rs > 0) rowSpan = Math.min(rs, MAX_ROWS);
1301
1552
  }
@@ -1310,15 +1561,16 @@ function parseCellBlock(records, startIdx, tableLevel) {
1310
1561
  }
1311
1562
  i++;
1312
1563
  }
1313
- return { cell: { text: texts.join("\n"), colSpan, rowSpan }, nextIdx: i };
1564
+ return { cell: { text: texts.join("\n"), colSpan, rowSpan, colAddr, rowAddr }, nextIdx: i };
1314
1565
  }
1315
1566
  function arrangeCells(rows, cols, cells) {
1316
1567
  const grid = Array.from({ length: rows }, () => Array(cols).fill(null));
1317
- let cellIdx = 0;
1318
- for (let r = 0; r < rows && cellIdx < cells.length; r++) {
1319
- for (let c = 0; c < cols && cellIdx < cells.length; c++) {
1320
- if (grid[r][c] !== null) continue;
1321
- const cell = cells[cellIdx++];
1568
+ const hasAddr = cells.some((c) => c.colAddr !== void 0 && c.rowAddr !== void 0);
1569
+ if (hasAddr) {
1570
+ for (const cell of cells) {
1571
+ const r = cell.rowAddr ?? 0;
1572
+ const c = cell.colAddr ?? 0;
1573
+ if (r >= rows || c >= cols) continue;
1322
1574
  grid[r][c] = cell;
1323
1575
  for (let dr = 0; dr < cell.rowSpan; dr++) {
1324
1576
  for (let dc = 0; dc < cell.colSpan; dc++) {
@@ -1328,6 +1580,22 @@ function arrangeCells(rows, cols, cells) {
1328
1580
  }
1329
1581
  }
1330
1582
  }
1583
+ } else {
1584
+ let cellIdx = 0;
1585
+ for (let r = 0; r < rows && cellIdx < cells.length; r++) {
1586
+ for (let c = 0; c < cols && cellIdx < cells.length; c++) {
1587
+ if (grid[r][c] !== null) continue;
1588
+ const cell = cells[cellIdx++];
1589
+ grid[r][c] = cell;
1590
+ for (let dr = 0; dr < cell.rowSpan; dr++) {
1591
+ for (let dc = 0; dc < cell.colSpan; dc++) {
1592
+ if (dr === 0 && dc === 0) continue;
1593
+ if (r + dr < rows && c + dc < cols)
1594
+ grid[r + dr][c + dc] = { text: "", colSpan: 1, rowSpan: 1 };
1595
+ }
1596
+ }
1597
+ }
1598
+ }
1331
1599
  }
1332
1600
  return grid.map((row) => row.map((c) => c || { text: "", colSpan: 1, rowSpan: 1 }));
1333
1601
  }
@@ -1889,13 +2157,26 @@ var import_pdf2 = require("pdfjs-dist/legacy/build/pdf.mjs");
1889
2157
  import_pdf2.GlobalWorkerOptions.workerSrc = "";
1890
2158
  var MAX_PAGES = 5e3;
1891
2159
  var MAX_TOTAL_TEXT = 100 * 1024 * 1024;
1892
- async function parsePdfDocument(buffer, options) {
1893
- const doc = await (0, import_pdf2.getDocument)({
2160
+ var PDF_LOAD_TIMEOUT_MS = 3e4;
2161
+ async function loadPdfWithTimeout(buffer) {
2162
+ const loadingTask = (0, import_pdf2.getDocument)({
1894
2163
  data: new Uint8Array(buffer),
1895
2164
  useSystemFonts: true,
1896
2165
  disableFontFace: true,
1897
2166
  isEvalSupported: false
1898
- }).promise;
2167
+ });
2168
+ return Promise.race([
2169
+ loadingTask.promise,
2170
+ new Promise(
2171
+ (_, reject) => setTimeout(() => {
2172
+ loadingTask.destroy();
2173
+ reject(new KordocError("PDF \uB85C\uB529 \uD0C0\uC784\uC544\uC6C3 (30\uCD08 \uCD08\uACFC)"));
2174
+ }, PDF_LOAD_TIMEOUT_MS)
2175
+ )
2176
+ ]);
2177
+ }
2178
+ async function parsePdfDocument(buffer, options) {
2179
+ const doc = await loadPdfWithTimeout(buffer);
1899
2180
  try {
1900
2181
  const pageCount = doc.numPages;
1901
2182
  if (pageCount === 0) return { success: false, fileType: "pdf", pageCount: 0, error: "PDF\uC5D0 \uD398\uC774\uC9C0\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.", code: "PARSE_ERROR" };
@@ -1907,32 +2188,43 @@ async function parsePdfDocument(buffer, options) {
1907
2188
  let totalTextBytes = 0;
1908
2189
  const effectivePageCount = Math.min(pageCount, MAX_PAGES);
1909
2190
  const pageFilter = options?.pages ? parsePageRange(options.pages, effectivePageCount) : null;
2191
+ const totalTarget = pageFilter ? pageFilter.size : effectivePageCount;
1910
2192
  const allFontSizes = [];
2193
+ const pageHeights = /* @__PURE__ */ new Map();
2194
+ let parsedPages = 0;
1911
2195
  for (let i = 1; i <= effectivePageCount; i++) {
1912
2196
  if (pageFilter && !pageFilter.has(i)) continue;
1913
- const page = await doc.getPage(i);
1914
- const tc = await page.getTextContent();
1915
- const viewport = page.getViewport({ scale: 1 });
1916
- const rawItems = tc.items;
1917
- const items = normalizeItems(rawItems);
1918
- const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
1919
- if (hiddenCount > 0) {
1920
- warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
1921
- }
1922
- for (const item of visible) {
1923
- if (item.fontSize > 0) allFontSizes.push(item.fontSize);
1924
- }
1925
- const opList = await page.getOperatorList();
1926
- const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
1927
- for (const b of pageBlocks) blocks.push(b);
1928
- for (const b of pageBlocks) {
1929
- const t = b.text || "";
1930
- totalChars += t.replace(/\s/g, "").length;
1931
- totalTextBytes += t.length * 2;
1932
- }
1933
- if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
1934
- }
1935
- const parsedPageCount = pageFilter ? pageFilter.size : effectivePageCount;
2197
+ try {
2198
+ const page = await doc.getPage(i);
2199
+ const tc = await page.getTextContent();
2200
+ const viewport = page.getViewport({ scale: 1 });
2201
+ pageHeights.set(i, viewport.height);
2202
+ const rawItems = tc.items;
2203
+ const items = normalizeItems(rawItems);
2204
+ const { visible, hiddenCount } = filterHiddenText(items, viewport.width, viewport.height);
2205
+ if (hiddenCount > 0) {
2206
+ warnings.push({ page: i, message: `${hiddenCount}\uAC1C \uC228\uACA8\uC9C4 \uD14D\uC2A4\uD2B8 \uC694\uC18C \uD544\uD130\uB9C1\uB428`, code: "HIDDEN_TEXT_FILTERED" });
2207
+ }
2208
+ for (const item of visible) {
2209
+ if (item.fontSize > 0) allFontSizes.push(item.fontSize);
2210
+ }
2211
+ const opList = await page.getOperatorList();
2212
+ const pageBlocks = extractPageBlocksWithLines(visible, i, opList, viewport.width, viewport.height);
2213
+ for (const b of pageBlocks) blocks.push(b);
2214
+ for (const b of pageBlocks) {
2215
+ const t = b.text || "";
2216
+ totalChars += t.replace(/\s/g, "").length;
2217
+ totalTextBytes += t.length * 2;
2218
+ }
2219
+ if (totalTextBytes > MAX_TOTAL_TEXT) throw new KordocError("\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uD06C\uAE30 \uCD08\uACFC");
2220
+ parsedPages++;
2221
+ options?.onProgress?.(parsedPages, totalTarget);
2222
+ } catch (pageErr) {
2223
+ if (pageErr instanceof KordocError) throw pageErr;
2224
+ 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" });
2225
+ }
2226
+ }
2227
+ const parsedPageCount = parsedPages || (pageFilter ? pageFilter.size : effectivePageCount);
1936
2228
  if (totalChars / Math.max(parsedPageCount, 1) < 10) {
1937
2229
  if (options?.ocr) {
1938
2230
  try {
@@ -1947,6 +2239,12 @@ async function parsePdfDocument(buffer, options) {
1947
2239
  }
1948
2240
  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" };
1949
2241
  }
2242
+ if (options?.removeHeaderFooter && parsedPageCount >= 3) {
2243
+ const removed = removeHeaderFooterBlocks(blocks, pageHeights, warnings);
2244
+ for (let ri = removed.length - 1; ri >= 0; ri--) {
2245
+ blocks.splice(removed[ri], 1);
2246
+ }
2247
+ }
1950
2248
  const medianFontSize = computeMedianFontSize(allFontSizes);
1951
2249
  if (medianFontSize > 0) {
1952
2250
  detectHeadings(blocks, medianFontSize);
@@ -2128,8 +2426,8 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
2128
2426
  () => Array.from({ length: numCols }, () => ({ text: "", colSpan: 1, rowSpan: 1 }))
2129
2427
  );
2130
2428
  for (const cell of cells) {
2131
- const textItems2 = cellTextMap.get(cell) || [];
2132
- const text = cellTextToString(textItems2);
2429
+ const cellItems = cellTextMap.get(cell) || [];
2430
+ const text = cellTextToString(cellItems);
2133
2431
  irGrid[cell.row][cell.col] = {
2134
2432
  text,
2135
2433
  colSpan: cell.colSpan,
@@ -2547,6 +2845,7 @@ function detectListBlocks(blocks) {
2547
2845
  return result;
2548
2846
  }
2549
2847
  var KOREAN_TABLE_HEADER_RE = /^\(?(구분|항목|종류|분류|유형|대상|내용|기간|금액|비율|방법|절차|요건|조건|근거|목적|범위|기준)\)?[:\s]/;
2848
+ var KV_FALSE_POSITIVE_RE = /\d{1,2}:\d{2}|:\/\/|\d+:\d+/;
2550
2849
  function detectSpecialKoreanTables(blocks) {
2551
2850
  const result = [];
2552
2851
  let kvLines = [];
@@ -2612,16 +2911,18 @@ function detectSpecialKoreanTables(blocks) {
2612
2911
  }
2613
2912
  continue;
2614
2913
  }
2615
- if (kvLines.length > 0 && text.includes(":") && !text.includes("(") && !text.includes(")")) {
2616
- const colonIdx = text.indexOf(":");
2617
- const key = text.slice(0, colonIdx).trim();
2618
- if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
2619
- kvLines.push({
2620
- key,
2621
- value: text.slice(colonIdx + 1).trim(),
2622
- block
2623
- });
2624
- continue;
2914
+ if (kvLines.length > 0 && text.includes(":")) {
2915
+ if (!KV_FALSE_POSITIVE_RE.test(text) && !text.includes("(") && !text.includes(")")) {
2916
+ const colonIdx = text.indexOf(":");
2917
+ const key = text.slice(0, colonIdx).trim();
2918
+ if (/^[가-힣]+$/.test(key) && key.length >= 2 && key.length <= 8) {
2919
+ kvLines.push({
2920
+ key,
2921
+ value: text.slice(colonIdx + 1).trim(),
2922
+ block
2923
+ });
2924
+ continue;
2925
+ }
2625
2926
  }
2626
2927
  }
2627
2928
  flushKvTable();
@@ -2630,6 +2931,62 @@ function detectSpecialKoreanTables(blocks) {
2630
2931
  flushKvTable();
2631
2932
  return result;
2632
2933
  }
2934
+ function removeHeaderFooterBlocks(blocks, pageHeights, warnings) {
2935
+ const ZONE_RATIO = 0.1;
2936
+ const MIN_REPEAT = 3;
2937
+ const headerTexts = /* @__PURE__ */ new Map();
2938
+ const footerTexts = /* @__PURE__ */ new Map();
2939
+ for (let bi = 0; bi < blocks.length; bi++) {
2940
+ const b = blocks[bi];
2941
+ if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
2942
+ const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
2943
+ if (!ph) continue;
2944
+ const blockTop = ph - (b.bbox.y + b.bbox.height);
2945
+ const blockBottom = ph - b.bbox.y;
2946
+ if (blockBottom <= ph * ZONE_RATIO) {
2947
+ const arr = footerTexts.get(b.pageNumber) || [];
2948
+ arr.push(b.text.trim());
2949
+ footerTexts.set(b.pageNumber, arr);
2950
+ } else if (blockTop >= ph * (1 - ZONE_RATIO)) {
2951
+ const arr = headerTexts.get(b.pageNumber) || [];
2952
+ arr.push(b.text.trim());
2953
+ headerTexts.set(b.pageNumber, arr);
2954
+ }
2955
+ }
2956
+ const repeatedPatterns = /* @__PURE__ */ new Set();
2957
+ for (const textsMap of [headerTexts, footerTexts]) {
2958
+ const patternCount = /* @__PURE__ */ new Map();
2959
+ for (const [, texts] of textsMap) {
2960
+ for (const t of texts) {
2961
+ const normalized = t.replace(/\d+/g, "#");
2962
+ patternCount.set(normalized, (patternCount.get(normalized) || 0) + 1);
2963
+ }
2964
+ }
2965
+ for (const [pattern, count] of patternCount) {
2966
+ if (count >= MIN_REPEAT) repeatedPatterns.add(pattern);
2967
+ }
2968
+ }
2969
+ if (repeatedPatterns.size === 0) return [];
2970
+ const removeIndices = [];
2971
+ for (let bi = 0; bi < blocks.length; bi++) {
2972
+ const b = blocks[bi];
2973
+ if (!b.bbox || !b.pageNumber || !b.text?.trim()) continue;
2974
+ const ph = pageHeights.get(b.bbox.page) || pageHeights.get(b.pageNumber);
2975
+ if (!ph) continue;
2976
+ const blockTop = ph - (b.bbox.y + b.bbox.height);
2977
+ const blockBottom = ph - b.bbox.y;
2978
+ const inZone = blockBottom <= ph * ZONE_RATIO || blockTop >= ph * (1 - ZONE_RATIO);
2979
+ if (!inZone) continue;
2980
+ const normalized = b.text.trim().replace(/\d+/g, "#");
2981
+ if (repeatedPatterns.has(normalized)) {
2982
+ removeIndices.push(bi);
2983
+ }
2984
+ }
2985
+ if (removeIndices.length > 0) {
2986
+ warnings.push({ message: `${removeIndices.length}\uAC1C \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC694\uC18C \uC81C\uAC70\uB428`, code: "HIDDEN_TEXT_FILTERED" });
2987
+ }
2988
+ return removeIndices;
2989
+ }
2633
2990
  function mergeKoreanLines(text) {
2634
2991
  if (!text) return "";
2635
2992
  const lines = text.split("\n");
@@ -2642,7 +2999,7 @@ function mergeKoreanLines(text) {
2642
2999
  result.push(curr);
2643
3000
  continue;
2644
3001
  }
2645
- if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
3002
+ if (/[가-힣·,\-]$/.test(prev) && /^[가-힣(]/.test(curr) && !curr.trimStart().startsWith("|") && !startsWithMarker(curr) && !isStandaloneHeader(prev)) {
2646
3003
  result[result.length - 1] = prev + " " + curr;
2647
3004
  } else {
2648
3005
  result.push(curr);
@@ -2665,7 +3022,9 @@ function normalizedSimilarity(a, b) {
2665
3022
  function normalize(s) {
2666
3023
  return s.replace(/\s+/g, " ").trim();
2667
3024
  }
3025
+ var MAX_LEVENSHTEIN_LEN = 1e4;
2668
3026
  function levenshtein(a, b) {
3027
+ if (a.length + b.length > MAX_LEVENSHTEIN_LEN) return Math.abs(a.length - b.length);
2669
3028
  if (a.length > b.length) [a, b] = [b, a];
2670
3029
  const m = a.length;
2671
3030
  const n = b.length;
@@ -2978,7 +3337,7 @@ function parseMarkdownToBlocks(md) {
2978
3337
  const tableRows = [];
2979
3338
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
2980
3339
  const row = lines[i];
2981
- if (/^\|[\s\-:]+\|/.test(row) && !row.includes("---") === false && /^[\s|:\-]+$/.test(row)) {
3340
+ if (/^[\s|:\-]+$/.test(row)) {
2982
3341
  i++;
2983
3342
  continue;
2984
3343
  }
@@ -3042,7 +3401,21 @@ function generateManifest() {
3042
3401
  }
3043
3402
 
3044
3403
  // src/index.ts
3045
- async function parse(buffer, options) {
3404
+ async function parse(input, options) {
3405
+ let buffer;
3406
+ if (typeof input === "string") {
3407
+ try {
3408
+ const buf = await (0, import_promises.readFile)(input);
3409
+ buffer = toArrayBuffer(buf);
3410
+ } catch (err) {
3411
+ 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}`;
3412
+ return { success: false, fileType: "unknown", error: msg, code: "PARSE_ERROR" };
3413
+ }
3414
+ } else if (Buffer.isBuffer(input)) {
3415
+ buffer = toArrayBuffer(input);
3416
+ } else {
3417
+ buffer = input;
3418
+ }
3046
3419
  if (!buffer || buffer.byteLength === 0) {
3047
3420
  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" };
3048
3421
  }
@@ -3060,16 +3433,16 @@ async function parse(buffer, options) {
3060
3433
  }
3061
3434
  async function parseHwpx(buffer, options) {
3062
3435
  try {
3063
- const { markdown, blocks, metadata, outline, warnings } = await parseHwpxDocument(buffer, options);
3064
- return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings };
3436
+ const { markdown, blocks, metadata, outline, warnings, images } = await parseHwpxDocument(buffer, options);
3437
+ return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
3065
3438
  } catch (err) {
3066
3439
  return { success: false, fileType: "hwpx", error: err instanceof Error ? err.message : "HWPX \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
3067
3440
  }
3068
3441
  }
3069
3442
  async function parseHwp(buffer, options) {
3070
3443
  try {
3071
- const { markdown, blocks, metadata, outline, warnings } = parseHwp5Document(Buffer.from(buffer), options);
3072
- return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings };
3444
+ const { markdown, blocks, metadata, outline, warnings, images } = parseHwp5Document(Buffer.from(buffer), options);
3445
+ return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
3073
3446
  } catch (err) {
3074
3447
  return { success: false, fileType: "hwp", error: err instanceof Error ? err.message : "HWP \uD30C\uC2F1 \uC2E4\uD328", code: classifyError(err) };
3075
3448
  }