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/README.md +36 -15
- package/dist/chunk-MOL7MDBG.js +35 -0
- package/dist/chunk-MOL7MDBG.js.map +1 -0
- package/dist/{chunk-NJ3R7LNR.js → chunk-QQ6PZADA.js} +1120 -230
- package/dist/chunk-QQ6PZADA.js.map +1 -0
- package/dist/chunk-UUKFY5P5.js +93 -0
- package/dist/chunk-UUKFY5P5.js.map +1 -0
- package/dist/cli.js +11 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1208 -191
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -5
- package/dist/index.d.ts +17 -5
- package/dist/index.js +1203 -190
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +10 -7
- package/dist/mcp.js.map +1 -1
- package/dist/page-range-737B4EZW.js +8 -0
- package/dist/page-range-737B4EZW.js.map +1 -0
- package/dist/provider-A4FHJSID.js +0 -0
- package/dist/utils-OTCR2KMY.js +22 -0
- package/dist/utils-OTCR2KMY.js.map +1 -0
- package/dist/{watch-AKTZTPVF.js → watch-JFDOENIO.js} +13 -5
- package/dist/watch-JFDOENIO.js.map +1 -0
- package/package.json +77 -75
- package/dist/chunk-NJ3R7LNR.js.map +0 -1
- package/dist/watch-AKTZTPVF.js.map +0 -1
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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}${
|
|
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][
|
|
230
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 >=
|
|
735
|
-
else if (ratio >=
|
|
736
|
-
else if (ratio >=
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
if (
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
if (
|
|
878
|
-
if (tableStack.
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
887
|
-
tableCtx = null;
|
|
984
|
+
tableCtx = tableStack.length > 0 ? tableStack.pop() : null;
|
|
888
985
|
}
|
|
889
|
-
} else {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
} else if (
|
|
897
|
-
|
|
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)
|
|
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
|
-
|
|
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 >=
|
|
1245
|
-
else if (ratio >=
|
|
1246
|
-
else if (ratio >=
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
if (
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
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)
|
|
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 {
|
|
2424
|
+
return { markdown: ocrMarkdown, blocks: ocrBlocks, metadata, warnings, isImageBased: true };
|
|
2238
2425
|
}
|
|
2239
2426
|
} catch {
|
|
2240
2427
|
}
|
|
2241
2428
|
}
|
|
2242
|
-
|
|
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 {
|
|
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 >=
|
|
2317
|
-
else if (ratio >=
|
|
2318
|
-
else if (ratio >=
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
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 =
|
|
2499
|
-
if (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.
|
|
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
|
|
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
|
|
2834
|
-
if (
|
|
2835
|
-
result.push({
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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
|