kordoc 3.7.0 → 3.8.1

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.
Files changed (30) hide show
  1. package/README.md +14 -2
  2. package/dist/{-7UC4ZWBS.js → -LD4BZDDJ.js} +3 -3
  3. package/dist/{chunk-IJJMVTU5.js → chunk-IFYJFWD2.js} +2 -2
  4. package/dist/{chunk-NXRABCWW.js → chunk-KT2BCHXI.js} +803 -703
  5. package/dist/chunk-KT2BCHXI.js.map +1 -0
  6. package/dist/{chunk-QZCP3UWU.cjs → chunk-LFCS3UVG.cjs} +2 -2
  7. package/dist/{chunk-QZCP3UWU.cjs.map → chunk-LFCS3UVG.cjs.map} +1 -1
  8. package/dist/{chunk-MEVKYW55.js → chunk-PELBIL4K.js} +2 -2
  9. package/dist/cli.js +4 -4
  10. package/dist/index.cjs +968 -868
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.js +802 -702
  13. package/dist/index.js.map +1 -1
  14. package/dist/mcp.js +3 -3
  15. package/dist/{parser-DR5CTZ74.js → parser-FFEBMLSH.js} +1830 -1803
  16. package/dist/parser-FFEBMLSH.js.map +1 -0
  17. package/dist/{parser-RFLPUZ7P.cjs → parser-IXK5V7YG.cjs} +1830 -1803
  18. package/dist/parser-IXK5V7YG.cjs.map +1 -0
  19. package/dist/{parser-ZHJFQR44.js → parser-XEDROIM7.js} +1830 -1803
  20. package/dist/parser-XEDROIM7.js.map +1 -0
  21. package/dist/{watch-UIX447QV.js → watch-MAWCDNFI.js} +3 -3
  22. package/package.json +2 -2
  23. package/dist/chunk-NXRABCWW.js.map +0 -1
  24. package/dist/parser-DR5CTZ74.js.map +0 -1
  25. package/dist/parser-RFLPUZ7P.cjs.map +0 -1
  26. package/dist/parser-ZHJFQR44.js.map +0 -1
  27. /package/dist/{-7UC4ZWBS.js.map → -LD4BZDDJ.js.map} +0 -0
  28. /package/dist/{chunk-IJJMVTU5.js.map → chunk-IFYJFWD2.js.map} +0 -0
  29. /package/dist/{chunk-MEVKYW55.js.map → chunk-PELBIL4K.js.map} +0 -0
  30. /package/dist/{watch-UIX447QV.js.map → watch-MAWCDNFI.js.map} +0 -0
@@ -25,7 +25,7 @@ import {
25
25
  sanitizeHref,
26
26
  stripDtd,
27
27
  toArrayBuffer
28
- } from "./chunk-IJJMVTU5.js";
28
+ } from "./chunk-IFYJFWD2.js";
29
29
  import {
30
30
  parsePageRange
31
31
  } from "./chunk-MOL7MDBG.js";
@@ -34,9 +34,7 @@ import {
34
34
  import { readFile } from "fs/promises";
35
35
 
36
36
  // src/hwpx/parser.ts
37
- import JSZip from "jszip";
38
- import { inflateRawSync } from "zlib";
39
- import { DOMParser } from "@xmldom/xmldom";
37
+ import JSZip2 from "jszip";
40
38
 
41
39
  // src/hwpx/com-fallback.ts
42
40
  import { execFileSync } from "child_process";
@@ -145,6 +143,245 @@ function comResultToParseResult(pages, pageCount, warnings) {
145
143
  };
146
144
  }
147
145
 
146
+ // src/hwpx/parser-shared.ts
147
+ import { DOMParser } from "@xmldom/xmldom";
148
+ var MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024;
149
+ var MAX_ZIP_ENTRIES = 500;
150
+ function clampSpan(val, max) {
151
+ return Math.max(1, Math.min(val, max));
152
+ }
153
+ var MAX_XML_DEPTH = 200;
154
+ function createSectionShared() {
155
+ return { numState: /* @__PURE__ */ new Map(), pageText: { headers: [], footers: [] }, track: { deleteDepth: 0, warned: false } };
156
+ }
157
+ function createXmlParser(warnings) {
158
+ return new DOMParser({
159
+ onError(level, msg2) {
160
+ if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg2}`);
161
+ warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg2}` });
162
+ }
163
+ });
164
+ }
165
+ function applyPageText(blocks, shared) {
166
+ const { headers, footers } = shared.pageText;
167
+ if (headers.length > 0) {
168
+ blocks.unshift(...headers.map((t) => ({ type: "paragraph", text: t, pageNumber: 1 })));
169
+ }
170
+ if (footers.length > 0) {
171
+ blocks.push(...footers.map((t) => ({ type: "paragraph", text: t })));
172
+ }
173
+ }
174
+ function findChildByLocalName(parent, name) {
175
+ const children = parent.childNodes;
176
+ if (!children) return null;
177
+ for (let i = 0; i < children.length; i++) {
178
+ const ch = children[i];
179
+ if (ch.nodeType !== 1) continue;
180
+ const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
181
+ if (tag === name) return ch;
182
+ }
183
+ return null;
184
+ }
185
+ function extractTextFromNode(node) {
186
+ let result = "";
187
+ const children = node.childNodes;
188
+ if (!children) return result;
189
+ for (let i = 0; i < children.length; i++) {
190
+ const child = children[i];
191
+ if (child.nodeType === 3) result += child.textContent || "";
192
+ else if (child.nodeType === 1) result += extractTextFromNode(child);
193
+ }
194
+ return result.trim();
195
+ }
196
+
197
+ // src/hwpx/styles.ts
198
+ async function extractHwpxStyles(zip, decompressed) {
199
+ const result = {
200
+ charProperties: /* @__PURE__ */ new Map(),
201
+ styles: /* @__PURE__ */ new Map(),
202
+ numberings: /* @__PURE__ */ new Map(),
203
+ bullets: /* @__PURE__ */ new Map(),
204
+ paraHeadings: /* @__PURE__ */ new Map()
205
+ };
206
+ const headerPaths = ["Contents/header.xml", "header.xml", "Contents/head.xml", "head.xml"];
207
+ for (const hp of headerPaths) {
208
+ const hpLower = hp.toLowerCase();
209
+ const file = zip.file(hp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === hpLower) || null;
210
+ if (!file) continue;
211
+ try {
212
+ const xml = await file.async("text");
213
+ if (decompressed) {
214
+ decompressed.total += xml.length * 2;
215
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
216
+ }
217
+ const parser = createXmlParser();
218
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
219
+ if (!doc.documentElement) continue;
220
+ parseCharProperties(doc, result.charProperties);
221
+ parseStyleElements(doc, result.styles);
222
+ const domDoc = doc;
223
+ parseNumberings(domDoc, result.numberings);
224
+ parseBullets(domDoc, result.bullets);
225
+ parseParaHeadings(domDoc, result.paraHeadings);
226
+ break;
227
+ } catch {
228
+ continue;
229
+ }
230
+ }
231
+ return result;
232
+ }
233
+ function parseCharProperties(doc, map) {
234
+ const tagNames = ["hh:charPr", "charPr", "hp:charPr"];
235
+ for (const tagName of tagNames) {
236
+ const elements = doc.getElementsByTagName(tagName);
237
+ for (let i = 0; i < elements.length; i++) {
238
+ const el = elements[i];
239
+ const id = el.getAttribute("id") || el.getAttribute("IDRef") || "";
240
+ if (!id) continue;
241
+ const prop = {};
242
+ const height = el.getAttribute("height");
243
+ if (height) {
244
+ const parsedHeight = parseInt(height, 10);
245
+ if (!isNaN(parsedHeight) && parsedHeight > 0) {
246
+ prop.fontSize = parsedHeight / 100;
247
+ }
248
+ }
249
+ const bold = el.getAttribute("bold");
250
+ if (bold === "true" || bold === "1") prop.bold = true;
251
+ const italic = el.getAttribute("italic");
252
+ if (italic === "true" || italic === "1") prop.italic = true;
253
+ const fontFaces = el.getElementsByTagName("*");
254
+ for (let j = 0; j < fontFaces.length; j++) {
255
+ const ff = fontFaces[j];
256
+ const localTag = (ff.tagName || "").replace(/^[^:]+:/, "");
257
+ if (localTag === "fontface" || localTag === "fontRef") {
258
+ const face = ff.getAttribute("face") || ff.getAttribute("FontFace");
259
+ if (face) {
260
+ prop.fontName = face;
261
+ break;
262
+ }
263
+ }
264
+ }
265
+ map.set(id, prop);
266
+ }
267
+ }
268
+ }
269
+ function parseStyleElements(doc, map) {
270
+ const tagNames = ["hh:style", "style", "hp:style"];
271
+ for (const tagName of tagNames) {
272
+ const elements = doc.getElementsByTagName(tagName);
273
+ for (let i = 0; i < elements.length; i++) {
274
+ const el = elements[i];
275
+ const id = el.getAttribute("id") || el.getAttribute("IDRef") || String(i);
276
+ const name = el.getAttribute("name") || el.getAttribute("engName") || "";
277
+ const charPrId = el.getAttribute("charPrIDRef") || void 0;
278
+ const paraPrId = el.getAttribute("paraPrIDRef") || void 0;
279
+ map.set(id, { name, charPrId, paraPrId });
280
+ }
281
+ }
282
+ }
283
+ function parseNumberings(doc, map) {
284
+ const tagNames = ["hh:numbering", "numbering"];
285
+ for (const tagName of tagNames) {
286
+ const elements = doc.getElementsByTagName(tagName);
287
+ for (let i = 0; i < elements.length; i++) {
288
+ const el = elements[i];
289
+ const id = el.getAttribute("id") || "";
290
+ if (!id) continue;
291
+ const def = { heads: /* @__PURE__ */ new Map() };
292
+ const children = el.childNodes;
293
+ for (let j = 0; j < children.length; j++) {
294
+ const ch = children[j];
295
+ if (ch.nodeType !== 1) continue;
296
+ const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
297
+ if (tag !== "paraHead") continue;
298
+ const level = parseInt(ch.getAttribute("level") || "", 10);
299
+ if (isNaN(level) || level < 1 || level > 10) continue;
300
+ const start = parseInt(ch.getAttribute("start") || "1", 10);
301
+ def.heads.set(level, {
302
+ numFormat: ch.getAttribute("numFormat") || "DIGIT",
303
+ text: ch.textContent || "",
304
+ start: isNaN(start) ? 1 : start
305
+ });
306
+ }
307
+ if (def.heads.size > 0) map.set(id, def);
308
+ }
309
+ if (map.size > 0) break;
310
+ }
311
+ }
312
+ function parseBullets(doc, map) {
313
+ const tagNames = ["hh:bullet", "bullet"];
314
+ for (const tagName of tagNames) {
315
+ const elements = doc.getElementsByTagName(tagName);
316
+ for (let i = 0; i < elements.length; i++) {
317
+ const el = elements[i];
318
+ const id = el.getAttribute("id") || "";
319
+ const char = el.getAttribute("char") || "";
320
+ if (id && char) map.set(id, char);
321
+ }
322
+ if (map.size > 0) break;
323
+ }
324
+ }
325
+ function parseParaHeadings(doc, map) {
326
+ const tagNames = ["hh:paraPr", "paraPr"];
327
+ for (const tagName of tagNames) {
328
+ const elements = doc.getElementsByTagName(tagName);
329
+ for (let i = 0; i < elements.length; i++) {
330
+ const el = elements[i];
331
+ const id = el.getAttribute("id") || "";
332
+ if (!id) continue;
333
+ const heading = findChildByLocalName(el, "heading");
334
+ if (!heading) continue;
335
+ const type = heading.getAttribute("type") || "NONE";
336
+ if (type !== "NUMBER" && type !== "BULLET" && type !== "OUTLINE") continue;
337
+ const level = parseInt(heading.getAttribute("level") || "0", 10);
338
+ map.set(id, {
339
+ type,
340
+ idRef: heading.getAttribute("idRef") || "0",
341
+ level: isNaN(level) ? 0 : Math.max(0, Math.min(level, 9))
342
+ });
343
+ }
344
+ if (map.size > 0) break;
345
+ }
346
+ }
347
+ function detectHwpxHeadings(blocks, styleMap) {
348
+ if (blocks.some((b) => b.type === "heading")) return;
349
+ let baseFontSize = 0;
350
+ const sizeFreq = /* @__PURE__ */ new Map();
351
+ for (const b of blocks) {
352
+ if (b.style?.fontSize) {
353
+ sizeFreq.set(b.style.fontSize, (sizeFreq.get(b.style.fontSize) || 0) + 1);
354
+ }
355
+ }
356
+ let maxCount = 0;
357
+ for (const [size, count] of sizeFreq) {
358
+ if (count > maxCount) {
359
+ maxCount = count;
360
+ baseFontSize = size;
361
+ }
362
+ }
363
+ for (const block of blocks) {
364
+ if (block.type !== "paragraph" || !block.text) continue;
365
+ const text = block.text.trim();
366
+ if (text.length === 0 || text.length > 200 || /^\d+$/.test(text)) continue;
367
+ let level = 0;
368
+ if (baseFontSize > 0 && block.style?.fontSize) {
369
+ const ratio = block.style.fontSize / baseFontSize;
370
+ if (ratio >= HEADING_RATIO_H1) level = 1;
371
+ else if (ratio >= HEADING_RATIO_H2) level = 2;
372
+ else if (ratio >= HEADING_RATIO_H3) level = 3;
373
+ }
374
+ const compactText = text.replace(/\s+/g, "");
375
+ if (/^제\d+[조장절편]/.test(compactText) && text.length <= 50) {
376
+ if (level === 0) level = 3;
377
+ }
378
+ if (level > 0) {
379
+ block.type = "heading";
380
+ block.level = level;
381
+ }
382
+ }
383
+ }
384
+
148
385
  // src/hwpx/equation.ts
149
386
  var CONVERT_MAP = {
150
387
  TIMES: "\\times",
@@ -584,173 +821,7 @@ function hmlToLatex(hmlEqStr) {
584
821
  return out;
585
822
  }
586
823
 
587
- // src/hwpx/parser.ts
588
- var MAX_DECOMPRESS_SIZE = 100 * 1024 * 1024;
589
- var MAX_ZIP_ENTRIES = 500;
590
- function clampSpan(val, max) {
591
- return Math.max(1, Math.min(val, max));
592
- }
593
- var MAX_XML_DEPTH = 200;
594
- function createSectionShared() {
595
- return { numState: /* @__PURE__ */ new Map(), pageText: { headers: [], footers: [] }, track: { deleteDepth: 0, warned: false } };
596
- }
597
- function createXmlParser(warnings) {
598
- return new DOMParser({
599
- onError(level, msg2) {
600
- if (level === "fatalError") throw new KordocError(`XML \uD30C\uC2F1 \uC2E4\uD328: ${msg2}`);
601
- warnings?.push({ code: "MALFORMED_XML", message: `XML ${level === "warn" ? "\uACBD\uACE0" : "\uC624\uB958"}: ${msg2}` });
602
- }
603
- });
604
- }
605
- async function extractHwpxStyles(zip, decompressed) {
606
- const result = {
607
- charProperties: /* @__PURE__ */ new Map(),
608
- styles: /* @__PURE__ */ new Map(),
609
- numberings: /* @__PURE__ */ new Map(),
610
- bullets: /* @__PURE__ */ new Map(),
611
- paraHeadings: /* @__PURE__ */ new Map()
612
- };
613
- const headerPaths = ["Contents/header.xml", "header.xml", "Contents/head.xml", "head.xml"];
614
- for (const hp of headerPaths) {
615
- const hpLower = hp.toLowerCase();
616
- const file = zip.file(hp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === hpLower) || null;
617
- if (!file) continue;
618
- try {
619
- const xml = await file.async("text");
620
- if (decompressed) {
621
- decompressed.total += xml.length * 2;
622
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
623
- }
624
- const parser = createXmlParser();
625
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
626
- if (!doc.documentElement) continue;
627
- parseCharProperties(doc, result.charProperties);
628
- parseStyleElements(doc, result.styles);
629
- const domDoc = doc;
630
- parseNumberings(domDoc, result.numberings);
631
- parseBullets(domDoc, result.bullets);
632
- parseParaHeadings(domDoc, result.paraHeadings);
633
- break;
634
- } catch {
635
- continue;
636
- }
637
- }
638
- return result;
639
- }
640
- function parseCharProperties(doc, map) {
641
- const tagNames = ["hh:charPr", "charPr", "hp:charPr"];
642
- for (const tagName of tagNames) {
643
- const elements = doc.getElementsByTagName(tagName);
644
- for (let i = 0; i < elements.length; i++) {
645
- const el = elements[i];
646
- const id = el.getAttribute("id") || el.getAttribute("IDRef") || "";
647
- if (!id) continue;
648
- const prop = {};
649
- const height = el.getAttribute("height");
650
- if (height) {
651
- const parsedHeight = parseInt(height, 10);
652
- if (!isNaN(parsedHeight) && parsedHeight > 0) {
653
- prop.fontSize = parsedHeight / 100;
654
- }
655
- }
656
- const bold = el.getAttribute("bold");
657
- if (bold === "true" || bold === "1") prop.bold = true;
658
- const italic = el.getAttribute("italic");
659
- if (italic === "true" || italic === "1") prop.italic = true;
660
- const fontFaces = el.getElementsByTagName("*");
661
- for (let j = 0; j < fontFaces.length; j++) {
662
- const ff = fontFaces[j];
663
- const localTag = (ff.tagName || "").replace(/^[^:]+:/, "");
664
- if (localTag === "fontface" || localTag === "fontRef") {
665
- const face = ff.getAttribute("face") || ff.getAttribute("FontFace");
666
- if (face) {
667
- prop.fontName = face;
668
- break;
669
- }
670
- }
671
- }
672
- map.set(id, prop);
673
- }
674
- }
675
- }
676
- function parseStyleElements(doc, map) {
677
- const tagNames = ["hh:style", "style", "hp:style"];
678
- for (const tagName of tagNames) {
679
- const elements = doc.getElementsByTagName(tagName);
680
- for (let i = 0; i < elements.length; i++) {
681
- const el = elements[i];
682
- const id = el.getAttribute("id") || el.getAttribute("IDRef") || String(i);
683
- const name = el.getAttribute("name") || el.getAttribute("engName") || "";
684
- const charPrId = el.getAttribute("charPrIDRef") || void 0;
685
- const paraPrId = el.getAttribute("paraPrIDRef") || void 0;
686
- map.set(id, { name, charPrId, paraPrId });
687
- }
688
- }
689
- }
690
- function parseNumberings(doc, map) {
691
- const tagNames = ["hh:numbering", "numbering"];
692
- for (const tagName of tagNames) {
693
- const elements = doc.getElementsByTagName(tagName);
694
- for (let i = 0; i < elements.length; i++) {
695
- const el = elements[i];
696
- const id = el.getAttribute("id") || "";
697
- if (!id) continue;
698
- const def = { heads: /* @__PURE__ */ new Map() };
699
- const children = el.childNodes;
700
- for (let j = 0; j < children.length; j++) {
701
- const ch = children[j];
702
- if (ch.nodeType !== 1) continue;
703
- const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
704
- if (tag !== "paraHead") continue;
705
- const level = parseInt(ch.getAttribute("level") || "", 10);
706
- if (isNaN(level) || level < 1 || level > 10) continue;
707
- const start = parseInt(ch.getAttribute("start") || "1", 10);
708
- def.heads.set(level, {
709
- numFormat: ch.getAttribute("numFormat") || "DIGIT",
710
- text: ch.textContent || "",
711
- start: isNaN(start) ? 1 : start
712
- });
713
- }
714
- if (def.heads.size > 0) map.set(id, def);
715
- }
716
- if (map.size > 0) break;
717
- }
718
- }
719
- function parseBullets(doc, map) {
720
- const tagNames = ["hh:bullet", "bullet"];
721
- for (const tagName of tagNames) {
722
- const elements = doc.getElementsByTagName(tagName);
723
- for (let i = 0; i < elements.length; i++) {
724
- const el = elements[i];
725
- const id = el.getAttribute("id") || "";
726
- const char = el.getAttribute("char") || "";
727
- if (id && char) map.set(id, char);
728
- }
729
- if (map.size > 0) break;
730
- }
731
- }
732
- function parseParaHeadings(doc, map) {
733
- const tagNames = ["hh:paraPr", "paraPr"];
734
- for (const tagName of tagNames) {
735
- const elements = doc.getElementsByTagName(tagName);
736
- for (let i = 0; i < elements.length; i++) {
737
- const el = elements[i];
738
- const id = el.getAttribute("id") || "";
739
- if (!id) continue;
740
- const heading = findChildByLocalName(el, "heading");
741
- if (!heading) continue;
742
- const type = heading.getAttribute("type") || "NONE";
743
- if (type !== "NUMBER" && type !== "BULLET" && type !== "OUTLINE") continue;
744
- const level = parseInt(heading.getAttribute("level") || "0", 10);
745
- map.set(id, {
746
- type,
747
- idRef: heading.getAttribute("idRef") || "0",
748
- level: isNaN(level) ? 0 : Math.max(0, Math.min(level, 9))
749
- });
750
- }
751
- if (map.size > 0) break;
752
- }
753
- }
824
+ // src/hwpx/para-heading.ts
754
825
  var HANGUL_SYLLABLE_SEQ = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
755
826
  var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
756
827
  function toRoman(n) {
@@ -773,448 +844,78 @@ function toRoman(n) {
773
844
  let out = "";
774
845
  for (const [v, s] of table) {
775
846
  while (n >= v) {
776
- out += s;
777
- n -= v;
778
- }
779
- }
780
- return out;
781
- }
782
- function formatHeadNumber(n, numFormat) {
783
- if (n <= 0) n = 1;
784
- switch (numFormat) {
785
- case "DIGIT":
786
- return String(n);
787
- case "CIRCLED_DIGIT":
788
- return n <= 20 ? String.fromCodePoint(9312 + n - 1) : `(${n})`;
789
- case "HANGUL_SYLLABLE":
790
- return HANGUL_SYLLABLE_SEQ[(n - 1) % HANGUL_SYLLABLE_SEQ.length];
791
- case "CIRCLED_HANGUL_SYLLABLE":
792
- return n <= 14 ? String.fromCodePoint(12910 + n - 1) : HANGUL_SYLLABLE_SEQ[(n - 1) % 14];
793
- case "HANGUL_JAMO":
794
- return HANGUL_JAMO_SEQ[(n - 1) % HANGUL_JAMO_SEQ.length];
795
- case "CIRCLED_HANGUL_JAMO":
796
- return n <= 14 ? String.fromCodePoint(12896 + n - 1) : HANGUL_JAMO_SEQ[(n - 1) % 14];
797
- case "LATIN_CAPITAL":
798
- return String.fromCharCode(65 + (n - 1) % 26);
799
- case "LATIN_SMALL":
800
- return String.fromCharCode(97 + (n - 1) % 26);
801
- case "CIRCLED_LATIN_CAPITAL":
802
- return n <= 26 ? String.fromCodePoint(9398 + n - 1) : String.fromCharCode(65 + (n - 1) % 26);
803
- case "CIRCLED_LATIN_SMALL":
804
- return n <= 26 ? String.fromCodePoint(9424 + n - 1) : String.fromCharCode(97 + (n - 1) % 26);
805
- case "ROMAN_CAPITAL":
806
- return toRoman(n);
807
- case "ROMAN_SMALL":
808
- return toRoman(n).toLowerCase();
809
- default:
810
- return String(n);
811
- }
812
- }
813
- function resolveParaHeading(paraEl, ctx) {
814
- const sm = ctx.styleMap;
815
- if (!sm) return null;
816
- const prId = paraEl.getAttribute("paraPrIDRef");
817
- if (!prId) return null;
818
- const ref = sm.paraHeadings.get(prId);
819
- if (!ref) return null;
820
- if (ref.type === "BULLET") {
821
- const char = sm.bullets.get(ref.idRef);
822
- return char ? { prefix: char } : null;
823
- }
824
- const numId = ref.type === "OUTLINE" ? ctx.outlineNumId || "1" : ref.idRef;
825
- const level = Math.min(ref.level + 1, 10);
826
- const headingLevel = ref.type === "OUTLINE" ? Math.min(ref.level + 1, 6) : void 0;
827
- const numDef = sm.numberings.get(numId);
828
- if (!numDef) return headingLevel ? { headingLevel } : null;
829
- let counters = ctx.shared.numState.get(numId);
830
- if (!counters) {
831
- counters = new Array(11).fill(0);
832
- ctx.shared.numState.set(numId, counters);
833
- }
834
- const head = numDef.heads.get(level);
835
- counters[level] = counters[level] === 0 ? head?.start ?? 1 : counters[level] + 1;
836
- for (let l = level + 1; l <= 10; l++) counters[l] = 0;
837
- const fmtText = head?.text?.trim() || `^${level}.`;
838
- const prefix = fmtText.replace(/\^(10|[1-9])/g, (_, d) => {
839
- const lv = parseInt(d, 10);
840
- const refHead = numDef.heads.get(lv);
841
- const n = counters[lv] || refHead?.start || 1;
842
- return formatHeadNumber(n, refHead?.numFormat || "DIGIT");
843
- });
844
- return { prefix, headingLevel };
845
- }
846
- async function parseHwpxDocument(buffer, options) {
847
- precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
848
- let zip;
849
- try {
850
- zip = await JSZip.loadAsync(buffer);
851
- } catch {
852
- return extractFromBrokenZip(buffer);
853
- }
854
- const actualEntryCount = Object.keys(zip.files).length;
855
- if (actualEntryCount > MAX_ZIP_ENTRIES) {
856
- throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
857
- }
858
- const manifestFile = zip.file("META-INF/manifest.xml");
859
- if (manifestFile) {
860
- const manifestXml = await manifestFile.async("text");
861
- if (isEncryptedHwpx(manifestXml)) {
862
- if (isComFallbackAvailable() && options?.filePath) {
863
- const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
864
- if (pages.some((p) => p && p.trim().length > 0)) {
865
- return comResultToParseResult(pages, pageCount, warnings2);
866
- }
867
- }
868
- throw new KordocError("DRM \uC554\uD638\uD654\uB41C HWPX \uD30C\uC77C\uC785\uB2C8\uB2E4. Windows + \uD55C\uCEF4 \uC624\uD53C\uC2A4 \uC124\uCE58 \uC2DC \uC790\uB3D9 \uCD94\uCD9C\uB429\uB2C8\uB2E4.");
869
- }
870
- }
871
- const decompressed = { total: 0 };
872
- const metadata = {};
873
- await extractHwpxMetadata(zip, metadata, decompressed);
874
- const styleMap = await extractHwpxStyles(zip, decompressed);
875
- const warnings = [];
876
- const sectionPaths = await resolveSectionPaths(zip);
877
- if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
878
- metadata.pageCount = sectionPaths.length;
879
- const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
880
- const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
881
- const blocks = [];
882
- const shared = createSectionShared();
883
- let parsedSections = 0;
884
- for (let si = 0; si < sectionPaths.length; si++) {
885
- if (pageFilter && !pageFilter.has(si + 1)) continue;
886
- const file = zip.file(sectionPaths[si]);
887
- if (!file) continue;
888
- try {
889
- const xml = await file.async("text");
890
- decompressed.total += xml.length * 2;
891
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
892
- blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
893
- parsedSections++;
894
- options?.onProgress?.(parsedSections, totalTarget);
895
- } catch (secErr) {
896
- if (secErr instanceof KordocError) throw secErr;
897
- 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" });
898
- }
899
- }
900
- applyPageText(blocks, shared);
901
- const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
902
- detectHwpxHeadings(blocks, styleMap);
903
- const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
904
- const markdown = blocksToMarkdown(blocks);
905
- 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 };
906
- }
907
- function applyPageText(blocks, shared) {
908
- const { headers, footers } = shared.pageText;
909
- if (headers.length > 0) {
910
- blocks.unshift(...headers.map((t) => ({ type: "paragraph", text: t, pageNumber: 1 })));
911
- }
912
- if (footers.length > 0) {
913
- blocks.push(...footers.map((t) => ({ type: "paragraph", text: t })));
914
- }
915
- }
916
- function imageExtToMime(ext) {
917
- switch (ext.toLowerCase()) {
918
- case "jpg":
919
- case "jpeg":
920
- return "image/jpeg";
921
- case "png":
922
- return "image/png";
923
- case "gif":
924
- return "image/gif";
925
- case "bmp":
926
- return "image/bmp";
927
- case "tif":
928
- case "tiff":
929
- return "image/tiff";
930
- case "wmf":
931
- return "image/wmf";
932
- case "emf":
933
- return "image/emf";
934
- case "svg":
935
- return "image/svg+xml";
936
- default:
937
- return "application/octet-stream";
938
- }
939
- }
940
- function mimeToExt(mime) {
941
- if (mime.includes("jpeg")) return "jpg";
942
- if (mime.includes("png")) return "png";
943
- if (mime.includes("gif")) return "gif";
944
- if (mime.includes("bmp")) return "bmp";
945
- if (mime.includes("tiff")) return "tif";
946
- if (mime.includes("wmf")) return "wmf";
947
- if (mime.includes("emf")) return "emf";
948
- if (mime.includes("svg")) return "svg";
949
- return "bin";
950
- }
951
- function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
952
- if (depth > MAX_XML_DEPTH) return;
953
- for (const block of blocks) {
954
- if (block.type === "image") {
955
- out.push({ block, ownerCell });
956
- } else if (block.type === "table" && block.table) {
957
- for (const row of block.table.cells) {
958
- for (const cell of row) {
959
- if (cell.blocks?.length) collectImageBlocks(cell.blocks, out, cell, depth + 1);
960
- }
961
- }
962
- }
963
- }
964
- }
965
- async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
966
- const images = [];
967
- let imageIndex = 0;
968
- const imageBlocks = [];
969
- collectImageBlocks(blocks, imageBlocks);
970
- for (const { block, ownerCell } of imageBlocks) {
971
- if (block.type !== "image" || !block.text) continue;
972
- const ref = block.text;
973
- const candidates = [
974
- `BinData/${ref}`,
975
- `Contents/BinData/${ref}`,
976
- ref
977
- // 절대 경로일 수도 있음
978
- ];
979
- let resolvedPath = null;
980
- if (!ref.includes(".")) {
981
- const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
982
- for (const prefix of prefixes) {
983
- const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
984
- if (match.length > 0) {
985
- resolvedPath = match[0].name;
986
- break;
987
- }
988
- }
989
- }
990
- let found = false;
991
- const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
992
- for (const path of allCandidates) {
993
- if (isPathTraversal(path)) continue;
994
- const file = zip.file(path);
995
- if (!file) continue;
996
- try {
997
- const data = await file.async("uint8array");
998
- decompressed.total += data.length;
999
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1000
- const actualPath = path;
1001
- const ext = actualPath.includes(".") ? actualPath.split(".").pop() || "png" : "png";
1002
- const mimeType = imageExtToMime(ext);
1003
- imageIndex++;
1004
- const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
1005
- images.push({ filename, data, mimeType });
1006
- block.text = filename;
1007
- block.imageData = { data, mimeType, filename: ref };
1008
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `![image](${filename})`);
1009
- found = true;
1010
- break;
1011
- } catch (err) {
1012
- if (err instanceof KordocError) throw err;
1013
- }
1014
- }
1015
- if (!found) {
1016
- warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
1017
- block.type = "paragraph";
1018
- block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
1019
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\uC774\uBBF8\uC9C0: ${ref}]`);
1020
- }
1021
- }
1022
- return images;
1023
- }
1024
- async function extractHwpxMetadata(zip, metadata, decompressed) {
1025
- try {
1026
- const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
1027
- for (const mp of metaPaths) {
1028
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
1029
- if (!file) continue;
1030
- const xml = await file.async("text");
1031
- if (decompressed) {
1032
- decompressed.total += xml.length * 2;
1033
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1034
- }
1035
- parseDublinCoreMetadata(xml, metadata);
1036
- if (metadata.title || metadata.author) return;
1037
- }
1038
- } catch {
1039
- }
1040
- }
1041
- function parseDublinCoreMetadata(xml, metadata) {
1042
- const parser = createXmlParser();
1043
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1044
- if (!doc.documentElement) return;
1045
- const getText = (tagNames) => {
1046
- for (const tag of tagNames) {
1047
- const els = doc.getElementsByTagName(tag);
1048
- if (els.length > 0) {
1049
- const text = els[0].textContent?.trim();
1050
- if (text) return text;
1051
- }
1052
- }
1053
- return void 0;
1054
- };
1055
- metadata.title = metadata.title || getText(["dc:title", "title"]);
1056
- metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
1057
- metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
1058
- metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
1059
- metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
1060
- const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
1061
- if (keywords && !metadata.keywords) {
1062
- metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
1063
- }
1064
- }
1065
- async function extractHwpxMetadataOnly(buffer) {
1066
- let zip;
1067
- try {
1068
- zip = await JSZip.loadAsync(buffer);
1069
- } catch {
1070
- throw new KordocError("HWPX ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1071
- }
1072
- const metadata = {};
1073
- await extractHwpxMetadata(zip, metadata);
1074
- const sectionPaths = await resolveSectionPaths(zip);
1075
- metadata.pageCount = sectionPaths.length;
1076
- return metadata;
1077
- }
1078
- function extractFromBrokenZip(buffer) {
1079
- const data = new Uint8Array(buffer);
1080
- const view = new DataView(buffer);
1081
- let pos = 0;
1082
- const blocks = [];
1083
- const warnings = [
1084
- { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
1085
- ];
1086
- let totalDecompressed = 0;
1087
- let entryCount = 0;
1088
- let sectionNum = 0;
1089
- const shared = createSectionShared();
1090
- while (pos < data.length - 30) {
1091
- if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
1092
- pos++;
1093
- while (pos < data.length - 30) {
1094
- if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
1095
- pos++;
1096
- }
1097
- continue;
1098
- }
1099
- if (++entryCount > MAX_ZIP_ENTRIES) break;
1100
- const method = view.getUint16(pos + 8, true);
1101
- const compSize = view.getUint32(pos + 18, true);
1102
- const nameLen = view.getUint16(pos + 26, true);
1103
- const extraLen = view.getUint16(pos + 28, true);
1104
- if (nameLen > 1024 || extraLen > 65535) {
1105
- pos += 30 + nameLen + extraLen;
1106
- continue;
1107
- }
1108
- const fileStart = pos + 30 + nameLen + extraLen;
1109
- if (fileStart + compSize > data.length) break;
1110
- if (compSize === 0 && method !== 0) {
1111
- pos = fileStart;
1112
- continue;
1113
- }
1114
- const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
1115
- const name = new TextDecoder().decode(nameBytes);
1116
- if (isPathTraversal(name)) {
1117
- pos = fileStart + compSize;
1118
- continue;
1119
- }
1120
- const fileData = data.slice(fileStart, fileStart + compSize);
1121
- pos = fileStart + compSize;
1122
- if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
1123
- try {
1124
- let content;
1125
- if (method === 0) {
1126
- content = new TextDecoder().decode(fileData);
1127
- } else if (method === 8) {
1128
- const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
1129
- content = new TextDecoder().decode(decompressed);
1130
- } else {
1131
- continue;
1132
- }
1133
- totalDecompressed += content.length * 2;
1134
- if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
1135
- sectionNum++;
1136
- blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
1137
- } catch {
1138
- continue;
1139
- }
1140
- }
1141
- 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");
1142
- applyPageText(blocks, shared);
1143
- const markdown = blocksToMarkdown(blocks);
1144
- return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
1145
- }
1146
- async function resolveSectionPaths(zip) {
1147
- const manifestPaths = ["Contents/content.hpf", "content.hpf"];
1148
- for (const mp of manifestPaths) {
1149
- const mpLower = mp.toLowerCase();
1150
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
1151
- if (!file) continue;
1152
- const xml = await file.async("text");
1153
- const paths = parseSectionPathsFromManifest(xml);
1154
- if (paths.length > 0) return paths;
1155
- }
1156
- const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
1157
- return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
1158
- }
1159
- function parseSectionPathsFromManifest(xml) {
1160
- const parser = createXmlParser();
1161
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1162
- const items = doc.getElementsByTagName("opf:item");
1163
- const spine = doc.getElementsByTagName("opf:itemref");
1164
- const idToHref = /* @__PURE__ */ new Map();
1165
- for (let i = 0; i < items.length; i++) {
1166
- const item = items[i];
1167
- const id = item.getAttribute("id") || "";
1168
- const href = normalizeSectionHref(item.getAttribute("href") || "");
1169
- if (id && href) idToHref.set(id, href);
1170
- }
1171
- if (spine.length > 0) {
1172
- const ordered = [];
1173
- for (let i = 0; i < spine.length; i++) {
1174
- const href = idToHref.get(spine[i].getAttribute("idref") || "");
1175
- if (href) ordered.push(href);
847
+ out += s;
848
+ n -= v;
1176
849
  }
1177
- if (ordered.length > 0) return ordered;
1178
850
  }
1179
- return Array.from(idToHref.values()).sort(compareSectionPaths);
851
+ return out;
1180
852
  }
1181
- function detectHwpxHeadings(blocks, styleMap) {
1182
- if (blocks.some((b) => b.type === "heading")) return;
1183
- let baseFontSize = 0;
1184
- const sizeFreq = /* @__PURE__ */ new Map();
1185
- for (const b of blocks) {
1186
- if (b.style?.fontSize) {
1187
- sizeFreq.set(b.style.fontSize, (sizeFreq.get(b.style.fontSize) || 0) + 1);
1188
- }
853
+ function formatHeadNumber(n, numFormat) {
854
+ if (n <= 0) n = 1;
855
+ switch (numFormat) {
856
+ case "DIGIT":
857
+ return String(n);
858
+ case "CIRCLED_DIGIT":
859
+ return n <= 20 ? String.fromCodePoint(9312 + n - 1) : `(${n})`;
860
+ case "HANGUL_SYLLABLE":
861
+ return HANGUL_SYLLABLE_SEQ[(n - 1) % HANGUL_SYLLABLE_SEQ.length];
862
+ case "CIRCLED_HANGUL_SYLLABLE":
863
+ return n <= 14 ? String.fromCodePoint(12910 + n - 1) : HANGUL_SYLLABLE_SEQ[(n - 1) % 14];
864
+ case "HANGUL_JAMO":
865
+ return HANGUL_JAMO_SEQ[(n - 1) % HANGUL_JAMO_SEQ.length];
866
+ case "CIRCLED_HANGUL_JAMO":
867
+ return n <= 14 ? String.fromCodePoint(12896 + n - 1) : HANGUL_JAMO_SEQ[(n - 1) % 14];
868
+ case "LATIN_CAPITAL":
869
+ return String.fromCharCode(65 + (n - 1) % 26);
870
+ case "LATIN_SMALL":
871
+ return String.fromCharCode(97 + (n - 1) % 26);
872
+ case "CIRCLED_LATIN_CAPITAL":
873
+ return n <= 26 ? String.fromCodePoint(9398 + n - 1) : String.fromCharCode(65 + (n - 1) % 26);
874
+ case "CIRCLED_LATIN_SMALL":
875
+ return n <= 26 ? String.fromCodePoint(9424 + n - 1) : String.fromCharCode(97 + (n - 1) % 26);
876
+ case "ROMAN_CAPITAL":
877
+ return toRoman(n);
878
+ case "ROMAN_SMALL":
879
+ return toRoman(n).toLowerCase();
880
+ default:
881
+ return String(n);
1189
882
  }
1190
- let maxCount = 0;
1191
- for (const [size, count] of sizeFreq) {
1192
- if (count > maxCount) {
1193
- maxCount = count;
1194
- baseFontSize = size;
1195
- }
883
+ }
884
+ function resolveParaHeading(paraEl, ctx) {
885
+ const sm = ctx.styleMap;
886
+ if (!sm) return null;
887
+ const prId = paraEl.getAttribute("paraPrIDRef");
888
+ if (!prId) return null;
889
+ const ref = sm.paraHeadings.get(prId);
890
+ if (!ref) return null;
891
+ if (ref.type === "BULLET") {
892
+ const char = sm.bullets.get(ref.idRef);
893
+ return char ? { prefix: char } : null;
1196
894
  }
1197
- for (const block of blocks) {
1198
- if (block.type !== "paragraph" || !block.text) continue;
1199
- const text = block.text.trim();
1200
- if (text.length === 0 || text.length > 200 || /^\d+$/.test(text)) continue;
1201
- let level = 0;
1202
- if (baseFontSize > 0 && block.style?.fontSize) {
1203
- const ratio = block.style.fontSize / baseFontSize;
1204
- if (ratio >= HEADING_RATIO_H1) level = 1;
1205
- else if (ratio >= HEADING_RATIO_H2) level = 2;
1206
- else if (ratio >= HEADING_RATIO_H3) level = 3;
1207
- }
1208
- const compactText = text.replace(/\s+/g, "");
1209
- if (/^제\d+[조장절편]/.test(compactText) && text.length <= 50) {
1210
- if (level === 0) level = 3;
1211
- }
1212
- if (level > 0) {
1213
- block.type = "heading";
1214
- block.level = level;
1215
- }
895
+ const numId = ref.type === "OUTLINE" ? ctx.outlineNumId || "1" : ref.idRef;
896
+ const level = Math.min(ref.level + 1, 10);
897
+ const headingLevel = ref.type === "OUTLINE" ? Math.min(ref.level + 1, 6) : void 0;
898
+ const numDef = sm.numberings.get(numId);
899
+ if (!numDef) return headingLevel ? { headingLevel } : null;
900
+ let counters = ctx.shared.numState.get(numId);
901
+ if (!counters) {
902
+ counters = new Array(11).fill(0);
903
+ ctx.shared.numState.set(numId, counters);
1216
904
  }
905
+ const head = numDef.heads.get(level);
906
+ counters[level] = counters[level] === 0 ? head?.start ?? 1 : counters[level] + 1;
907
+ for (let l = level + 1; l <= 10; l++) counters[l] = 0;
908
+ const fmtText = head?.text?.trim() || `^${level}.`;
909
+ const prefix = fmtText.replace(/\^(10|[1-9])/g, (_, d) => {
910
+ const lv = parseInt(d, 10);
911
+ const refHead = numDef.heads.get(lv);
912
+ const n = counters[lv] || refHead?.start || 1;
913
+ return formatHeadNumber(n, refHead?.numFormat || "DIGIT");
914
+ });
915
+ return { prefix, headingLevel };
1217
916
  }
917
+
918
+ // src/hwpx/table-build.ts
1218
919
  function buildTableWithCellMeta(state) {
1219
920
  const table = buildTable(state.rows);
1220
921
  if (state.caption) table.caption = state.caption;
@@ -1294,6 +995,8 @@ function completeTable(newTable, tableStack, blocks, ctx) {
1294
995
  }
1295
996
  return parentTable;
1296
997
  }
998
+
999
+ // src/hwpx/section-walker.ts
1297
1000
  function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
1298
1001
  const parser = createXmlParser(warnings);
1299
1002
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
@@ -1827,49 +1530,370 @@ function extractParagraphInfo(para, styleMap, ctx) {
1827
1530
  break;
1828
1531
  }
1829
1532
  }
1830
- };
1831
- walk(para);
1832
- const leaderIdx = text.indexOf("");
1833
- if (leaderIdx >= 0) text = text.substring(0, leaderIdx);
1834
- let cleanText = text.replace(/[ \t]+/g, " ").trim();
1835
- if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
1836
- cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
1837
- cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
1838
- let style;
1839
- if (styleMap && charPrId) {
1840
- const charProp = styleMap.charProperties.get(charPrId);
1841
- if (charProp) {
1842
- style = {};
1843
- if (charProp.fontSize) style.fontSize = charProp.fontSize;
1844
- if (charProp.bold) style.bold = true;
1845
- if (charProp.italic) style.italic = true;
1846
- if (charProp.fontName) style.fontName = charProp.fontName;
1847
- if (!style.fontSize && !style.bold && !style.italic) style = void 0;
1533
+ };
1534
+ walk(para);
1535
+ const leaderIdx = text.indexOf("");
1536
+ if (leaderIdx >= 0) text = text.substring(0, leaderIdx);
1537
+ let cleanText = text.replace(/[ \t]+/g, " ").trim();
1538
+ if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
1539
+ cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
1540
+ cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
1541
+ let style;
1542
+ if (styleMap && charPrId) {
1543
+ const charProp = styleMap.charProperties.get(charPrId);
1544
+ if (charProp) {
1545
+ style = {};
1546
+ if (charProp.fontSize) style.fontSize = charProp.fontSize;
1547
+ if (charProp.bold) style.bold = true;
1548
+ if (charProp.italic) style.italic = true;
1549
+ if (charProp.fontName) style.fontName = charProp.fontName;
1550
+ if (!style.fontSize && !style.bold && !style.italic) style = void 0;
1551
+ }
1552
+ }
1553
+ return { text: cleanText, href, footnote, style };
1554
+ }
1555
+
1556
+ // src/hwpx/images.ts
1557
+ function imageExtToMime(ext) {
1558
+ switch (ext.toLowerCase()) {
1559
+ case "jpg":
1560
+ case "jpeg":
1561
+ return "image/jpeg";
1562
+ case "png":
1563
+ return "image/png";
1564
+ case "gif":
1565
+ return "image/gif";
1566
+ case "bmp":
1567
+ return "image/bmp";
1568
+ case "tif":
1569
+ case "tiff":
1570
+ return "image/tiff";
1571
+ case "wmf":
1572
+ return "image/wmf";
1573
+ case "emf":
1574
+ return "image/emf";
1575
+ case "svg":
1576
+ return "image/svg+xml";
1577
+ default:
1578
+ return "application/octet-stream";
1579
+ }
1580
+ }
1581
+ function mimeToExt(mime) {
1582
+ if (mime.includes("jpeg")) return "jpg";
1583
+ if (mime.includes("png")) return "png";
1584
+ if (mime.includes("gif")) return "gif";
1585
+ if (mime.includes("bmp")) return "bmp";
1586
+ if (mime.includes("tiff")) return "tif";
1587
+ if (mime.includes("wmf")) return "wmf";
1588
+ if (mime.includes("emf")) return "emf";
1589
+ if (mime.includes("svg")) return "svg";
1590
+ return "bin";
1591
+ }
1592
+ function collectImageBlocks(blocks, out, ownerCell, depth = 0) {
1593
+ if (depth > MAX_XML_DEPTH) return;
1594
+ for (const block of blocks) {
1595
+ if (block.type === "image") {
1596
+ out.push({ block, ownerCell });
1597
+ } else if (block.type === "table" && block.table) {
1598
+ for (const row of block.table.cells) {
1599
+ for (const cell of row) {
1600
+ if (cell.blocks?.length) collectImageBlocks(cell.blocks, out, cell, depth + 1);
1601
+ }
1602
+ }
1603
+ }
1604
+ }
1605
+ }
1606
+ async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
1607
+ const images = [];
1608
+ let imageIndex = 0;
1609
+ const imageBlocks = [];
1610
+ collectImageBlocks(blocks, imageBlocks);
1611
+ const resolved = /* @__PURE__ */ new Map();
1612
+ for (const { block, ownerCell } of imageBlocks) {
1613
+ if (block.type !== "image" || !block.text) continue;
1614
+ const ref = block.text;
1615
+ let img = resolved.get(ref);
1616
+ if (img === void 0) {
1617
+ img = null;
1618
+ const candidates = [
1619
+ `BinData/${ref}`,
1620
+ `Contents/BinData/${ref}`,
1621
+ ref
1622
+ // 절대 경로일 수도 있음
1623
+ ];
1624
+ let resolvedPath = null;
1625
+ if (!ref.includes(".")) {
1626
+ const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
1627
+ for (const prefix of prefixes) {
1628
+ const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
1629
+ if (match.length > 0) {
1630
+ resolvedPath = match[0].name;
1631
+ break;
1632
+ }
1633
+ }
1634
+ }
1635
+ const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
1636
+ for (const path of allCandidates) {
1637
+ if (isPathTraversal(path)) continue;
1638
+ const file = zip.file(path);
1639
+ if (!file) continue;
1640
+ try {
1641
+ const data = await file.async("uint8array");
1642
+ decompressed.total += data.length;
1643
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1644
+ const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
1645
+ const mimeType = imageExtToMime(ext);
1646
+ imageIndex++;
1647
+ const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
1648
+ img = { filename, data, mimeType };
1649
+ images.push(img);
1650
+ break;
1651
+ } catch (err) {
1652
+ if (err instanceof KordocError) throw err;
1653
+ }
1654
+ }
1655
+ if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
1656
+ resolved.set(ref, img);
1657
+ }
1658
+ if (!img) {
1659
+ block.type = "paragraph";
1660
+ block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
1661
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\uC774\uBBF8\uC9C0: ${ref}]`);
1662
+ continue;
1663
+ }
1664
+ block.text = img.filename;
1665
+ block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
1666
+ if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `![image](${img.filename})`);
1667
+ }
1668
+ return images;
1669
+ }
1670
+
1671
+ // src/hwpx/metadata.ts
1672
+ import JSZip from "jszip";
1673
+
1674
+ // src/hwpx/zip-sections.ts
1675
+ import { inflateRawSync } from "zlib";
1676
+ function extractFromBrokenZip(buffer) {
1677
+ const data = new Uint8Array(buffer);
1678
+ const view = new DataView(buffer);
1679
+ let pos = 0;
1680
+ const blocks = [];
1681
+ const warnings = [
1682
+ { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
1683
+ ];
1684
+ let totalDecompressed = 0;
1685
+ let entryCount = 0;
1686
+ let sectionNum = 0;
1687
+ const shared = createSectionShared();
1688
+ while (pos < data.length - 30) {
1689
+ if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
1690
+ pos++;
1691
+ while (pos < data.length - 30) {
1692
+ if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
1693
+ pos++;
1694
+ }
1695
+ continue;
1696
+ }
1697
+ if (++entryCount > MAX_ZIP_ENTRIES) break;
1698
+ const method = view.getUint16(pos + 8, true);
1699
+ const compSize = view.getUint32(pos + 18, true);
1700
+ const nameLen = view.getUint16(pos + 26, true);
1701
+ const extraLen = view.getUint16(pos + 28, true);
1702
+ if (nameLen > 1024 || extraLen > 65535) {
1703
+ pos += 30 + nameLen + extraLen;
1704
+ continue;
1705
+ }
1706
+ const fileStart = pos + 30 + nameLen + extraLen;
1707
+ if (fileStart + compSize > data.length) break;
1708
+ if (compSize === 0 && method !== 0) {
1709
+ pos = fileStart;
1710
+ continue;
1711
+ }
1712
+ const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
1713
+ const name = new TextDecoder().decode(nameBytes);
1714
+ if (isPathTraversal(name)) {
1715
+ pos = fileStart + compSize;
1716
+ continue;
1717
+ }
1718
+ const fileData = data.slice(fileStart, fileStart + compSize);
1719
+ pos = fileStart + compSize;
1720
+ if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
1721
+ try {
1722
+ let content;
1723
+ if (method === 0) {
1724
+ content = new TextDecoder().decode(fileData);
1725
+ } else if (method === 8) {
1726
+ const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
1727
+ content = new TextDecoder().decode(decompressed);
1728
+ } else {
1729
+ continue;
1730
+ }
1731
+ totalDecompressed += content.length * 2;
1732
+ if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
1733
+ sectionNum++;
1734
+ blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
1735
+ } catch {
1736
+ continue;
1737
+ }
1738
+ }
1739
+ 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");
1740
+ applyPageText(blocks, shared);
1741
+ const markdown = blocksToMarkdown(blocks);
1742
+ return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
1743
+ }
1744
+ async function resolveSectionPaths(zip) {
1745
+ const manifestPaths = ["Contents/content.hpf", "content.hpf"];
1746
+ for (const mp of manifestPaths) {
1747
+ const mpLower = mp.toLowerCase();
1748
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
1749
+ if (!file) continue;
1750
+ const xml = await file.async("text");
1751
+ const paths = parseSectionPathsFromManifest(xml);
1752
+ if (paths.length > 0) return paths;
1753
+ }
1754
+ const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
1755
+ return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
1756
+ }
1757
+ function parseSectionPathsFromManifest(xml) {
1758
+ const parser = createXmlParser();
1759
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1760
+ const items = doc.getElementsByTagName("opf:item");
1761
+ const spine = doc.getElementsByTagName("opf:itemref");
1762
+ const idToHref = /* @__PURE__ */ new Map();
1763
+ for (let i = 0; i < items.length; i++) {
1764
+ const item = items[i];
1765
+ const id = item.getAttribute("id") || "";
1766
+ const href = normalizeSectionHref(item.getAttribute("href") || "");
1767
+ if (id && href) idToHref.set(id, href);
1768
+ }
1769
+ if (spine.length > 0) {
1770
+ const ordered = [];
1771
+ for (let i = 0; i < spine.length; i++) {
1772
+ const href = idToHref.get(spine[i].getAttribute("idref") || "");
1773
+ if (href) ordered.push(href);
1848
1774
  }
1775
+ if (ordered.length > 0) return ordered;
1849
1776
  }
1850
- return { text: cleanText, href, footnote, style };
1777
+ return Array.from(idToHref.values()).sort(compareSectionPaths);
1851
1778
  }
1852
- function findChildByLocalName(parent, name) {
1853
- const children = parent.childNodes;
1854
- if (!children) return null;
1855
- for (let i = 0; i < children.length; i++) {
1856
- const ch = children[i];
1857
- if (ch.nodeType !== 1) continue;
1858
- const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1859
- if (tag === name) return ch;
1779
+
1780
+ // src/hwpx/metadata.ts
1781
+ async function extractHwpxMetadata(zip, metadata, decompressed) {
1782
+ try {
1783
+ const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
1784
+ for (const mp of metaPaths) {
1785
+ const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
1786
+ if (!file) continue;
1787
+ const xml = await file.async("text");
1788
+ if (decompressed) {
1789
+ decompressed.total += xml.length * 2;
1790
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1791
+ }
1792
+ parseDublinCoreMetadata(xml, metadata);
1793
+ if (metadata.title || metadata.author) return;
1794
+ }
1795
+ } catch {
1860
1796
  }
1861
- return null;
1862
1797
  }
1863
- function extractTextFromNode(node) {
1864
- let result = "";
1865
- const children = node.childNodes;
1866
- if (!children) return result;
1867
- for (let i = 0; i < children.length; i++) {
1868
- const child = children[i];
1869
- if (child.nodeType === 3) result += child.textContent || "";
1870
- else if (child.nodeType === 1) result += extractTextFromNode(child);
1798
+ function parseDublinCoreMetadata(xml, metadata) {
1799
+ const parser = createXmlParser();
1800
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1801
+ if (!doc.documentElement) return;
1802
+ const getText = (tagNames) => {
1803
+ for (const tag of tagNames) {
1804
+ const els = doc.getElementsByTagName(tag);
1805
+ if (els.length > 0) {
1806
+ const text = els[0].textContent?.trim();
1807
+ if (text) return text;
1808
+ }
1809
+ }
1810
+ return void 0;
1811
+ };
1812
+ metadata.title = metadata.title || getText(["dc:title", "title"]);
1813
+ metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
1814
+ metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
1815
+ metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
1816
+ metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
1817
+ const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
1818
+ if (keywords && !metadata.keywords) {
1819
+ metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
1871
1820
  }
1872
- return result.trim();
1821
+ }
1822
+ async function extractHwpxMetadataOnly(buffer) {
1823
+ let zip;
1824
+ try {
1825
+ zip = await JSZip.loadAsync(buffer);
1826
+ } catch {
1827
+ throw new KordocError("HWPX ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1828
+ }
1829
+ const metadata = {};
1830
+ await extractHwpxMetadata(zip, metadata);
1831
+ const sectionPaths = await resolveSectionPaths(zip);
1832
+ metadata.pageCount = sectionPaths.length;
1833
+ return metadata;
1834
+ }
1835
+
1836
+ // src/hwpx/parser.ts
1837
+ async function parseHwpxDocument(buffer, options) {
1838
+ precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
1839
+ let zip;
1840
+ try {
1841
+ zip = await JSZip2.loadAsync(buffer);
1842
+ } catch {
1843
+ return extractFromBrokenZip(buffer);
1844
+ }
1845
+ const actualEntryCount = Object.keys(zip.files).length;
1846
+ if (actualEntryCount > MAX_ZIP_ENTRIES) {
1847
+ throw new KordocError("ZIP \uC5D4\uD2B8\uB9AC \uC218 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1848
+ }
1849
+ const manifestFile = zip.file("META-INF/manifest.xml");
1850
+ if (manifestFile) {
1851
+ const manifestXml = await manifestFile.async("text");
1852
+ if (isEncryptedHwpx(manifestXml)) {
1853
+ if (isComFallbackAvailable() && options?.filePath) {
1854
+ const { pages, pageCount, warnings: warnings2 } = extractTextViaCom(options.filePath);
1855
+ if (pages.some((p) => p && p.trim().length > 0)) {
1856
+ return comResultToParseResult(pages, pageCount, warnings2);
1857
+ }
1858
+ }
1859
+ throw new KordocError("DRM \uC554\uD638\uD654\uB41C HWPX \uD30C\uC77C\uC785\uB2C8\uB2E4. Windows + \uD55C\uCEF4 \uC624\uD53C\uC2A4 \uC124\uCE58 \uC2DC \uC790\uB3D9 \uCD94\uCD9C\uB429\uB2C8\uB2E4.");
1860
+ }
1861
+ }
1862
+ const decompressed = { total: 0 };
1863
+ const metadata = {};
1864
+ await extractHwpxMetadata(zip, metadata, decompressed);
1865
+ const styleMap = await extractHwpxStyles(zip, decompressed);
1866
+ const warnings = [];
1867
+ const sectionPaths = await resolveSectionPaths(zip);
1868
+ if (sectionPaths.length === 0) throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1869
+ metadata.pageCount = sectionPaths.length;
1870
+ const pageFilter = options?.pages ? parsePageRange(options.pages, sectionPaths.length) : null;
1871
+ const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
1872
+ const blocks = [];
1873
+ const shared = createSectionShared();
1874
+ let parsedSections = 0;
1875
+ for (let si = 0; si < sectionPaths.length; si++) {
1876
+ if (pageFilter && !pageFilter.has(si + 1)) continue;
1877
+ const file = zip.file(sectionPaths[si]);
1878
+ if (!file) continue;
1879
+ try {
1880
+ const xml = await file.async("text");
1881
+ decompressed.total += xml.length * 2;
1882
+ if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1883
+ blocks.push(...parseSectionXml(xml, styleMap, warnings, si + 1, shared));
1884
+ parsedSections++;
1885
+ options?.onProgress?.(parsedSections, totalTarget);
1886
+ } catch (secErr) {
1887
+ if (secErr instanceof KordocError) throw secErr;
1888
+ 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" });
1889
+ }
1890
+ }
1891
+ applyPageText(blocks, shared);
1892
+ const images = await extractImagesFromZip(zip, blocks, decompressed, warnings);
1893
+ detectHwpxHeadings(blocks, styleMap);
1894
+ const outline = blocks.filter((b) => b.type === "heading" && b.level && b.text).map((b) => ({ level: b.level, text: b.text, pageNumber: b.pageNumber }));
1895
+ const markdown = blocksToMarkdown(blocks);
1896
+ 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 };
1873
1897
  }
1874
1898
 
1875
1899
  // src/hwp5/record.ts
@@ -2396,32 +2420,42 @@ function resolveImageBlocks(binDataMap, blocks, warnings) {
2396
2420
  if (imageBlocks.length === 0) return [];
2397
2421
  const images = [];
2398
2422
  const renamed = /* @__PURE__ */ new Map();
2423
+ const resolved = /* @__PURE__ */ new Map();
2399
2424
  let imageIndex = 0;
2400
2425
  for (const block of imageBlocks) {
2401
2426
  if (!block.text) continue;
2402
2427
  const storageId = parseInt(block.text, 10);
2403
2428
  if (isNaN(storageId)) continue;
2404
- const bin = binDataMap.get(storageId);
2405
- if (!bin) {
2406
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
2407
- block.type = "paragraph";
2408
- block.text = `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
2409
- continue;
2429
+ let img = resolved.get(storageId);
2430
+ if (img === void 0) {
2431
+ const bin = binDataMap.get(storageId);
2432
+ if (!bin) {
2433
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId} \uC5C6\uC74C`, code: "SKIPPED_IMAGE" });
2434
+ resolved.set(storageId, null);
2435
+ } else {
2436
+ const mime = detectImageMime(bin.data);
2437
+ if (!mime) {
2438
+ warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
2439
+ resolved.set(storageId, null);
2440
+ } else {
2441
+ imageIndex++;
2442
+ const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
2443
+ img = { filename: `image_${String(imageIndex).padStart(3, "0")}.${ext}`, data: new Uint8Array(bin.data), mime };
2444
+ resolved.set(storageId, img);
2445
+ images.push({ filename: img.filename, data: img.data, mimeType: img.mime });
2446
+ renamed.set(storageId, img.filename);
2447
+ }
2448
+ }
2449
+ img = resolved.get(storageId);
2410
2450
  }
2411
- const mime = detectImageMime(bin.data);
2412
- if (!mime) {
2413
- warnings.push({ page: block.pageNumber, message: `BinData ${storageId}: \uC54C \uC218 \uC5C6\uB294 \uC774\uBBF8\uC9C0 \uD615\uC2DD`, code: "SKIPPED_IMAGE" });
2451
+ if (!img) {
2452
+ const bin = binDataMap.get(storageId);
2414
2453
  block.type = "paragraph";
2415
- block.text = `[\uC774\uBBF8\uC9C0: ${bin.name}]`;
2454
+ block.text = bin ? `[\uC774\uBBF8\uC9C0: ${bin.name}]` : `[\uC774\uBBF8\uC9C0: BinData ${storageId}]`;
2416
2455
  continue;
2417
2456
  }
2418
- imageIndex++;
2419
- const ext = mime.includes("jpeg") ? "jpg" : mime.includes("png") ? "png" : mime.includes("gif") ? "gif" : mime.includes("bmp") ? "bmp" : "bin";
2420
- const filename = `image_${String(imageIndex).padStart(3, "0")}.${ext}`;
2421
- images.push({ filename, data: new Uint8Array(bin.data), mimeType: mime });
2422
- renamed.set(storageId, filename);
2423
- block.text = filename;
2424
- block.imageData = { data: new Uint8Array(bin.data), mimeType: mime, filename: bin.name };
2457
+ block.text = img.filename;
2458
+ block.imageData = { data: img.data, mimeType: img.mime, filename: binDataMap.get(storageId).name };
2425
2459
  }
2426
2460
  resolveCellImageSentinels(blocks, renamed);
2427
2461
  return images;
@@ -16515,7 +16549,7 @@ function isDistributionSentinel(markdown) {
16515
16549
  }
16516
16550
 
16517
16551
  // src/xlsx/parser.ts
16518
- import JSZip2 from "jszip";
16552
+ import JSZip3 from "jszip";
16519
16553
  import { DOMParser as DOMParser2 } from "@xmldom/xmldom";
16520
16554
  var MAX_SHEETS = 100;
16521
16555
  var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
@@ -16706,7 +16740,7 @@ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex) {
16706
16740
  }
16707
16741
  async function parseXlsxDocument(buffer, options) {
16708
16742
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE3);
16709
- const zip = await JSZip2.loadAsync(buffer);
16743
+ const zip = await JSZip3.loadAsync(buffer);
16710
16744
  const warnings = [];
16711
16745
  const workbookFile = zip.file("xl/workbook.xml");
16712
16746
  if (!workbookFile) {
@@ -17412,7 +17446,7 @@ async function parseXlsDocument(buffer, options) {
17412
17446
  }
17413
17447
 
17414
17448
  // src/docx/parser.ts
17415
- import JSZip3 from "jszip";
17449
+ import JSZip4 from "jszip";
17416
17450
  import { DOMParser as DOMParser3 } from "@xmldom/xmldom";
17417
17451
 
17418
17452
  // src/docx/equation.ts
@@ -18123,7 +18157,7 @@ function parseTable(tbl, styles, numbering, footnotes, rels) {
18123
18157
  };
18124
18158
  return { type: "table", table };
18125
18159
  }
18126
- async function extractImages(zip, rels, doc) {
18160
+ async function extractImages(zip, rels, doc, warnings) {
18127
18161
  const blocks = [];
18128
18162
  const images = [];
18129
18163
  const drawingElements = findElements(doc.documentElement, "drawing");
@@ -18154,7 +18188,11 @@ async function extractImages(zip, rels, doc) {
18154
18188
  const filename = `image_${String(imgIdx).padStart(3, "0")}.${ext}`;
18155
18189
  images.push({ filename, data, mimeType: mimeMap[ext] ?? "image/png" });
18156
18190
  blocks.push({ type: "image", text: filename });
18157
- } catch {
18191
+ } catch (err) {
18192
+ warnings.push({
18193
+ code: "SKIPPED_IMAGE",
18194
+ message: `DOCX \uC774\uBBF8\uC9C0 \uCD94\uCD9C \uC2E4\uD328 (${imgPath}): ${err instanceof Error ? err.message : String(err)}`
18195
+ });
18158
18196
  }
18159
18197
  }
18160
18198
  }
@@ -18162,7 +18200,7 @@ async function extractImages(zip, rels, doc) {
18162
18200
  }
18163
18201
  async function parseDocxDocument(buffer, options) {
18164
18202
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
18165
- const zip = await JSZip3.loadAsync(buffer);
18203
+ const zip = await JSZip4.loadAsync(buffer);
18166
18204
  const warnings = [];
18167
18205
  const docFile = zip.file("word/document.xml");
18168
18206
  if (!docFile) {
@@ -18178,7 +18216,11 @@ async function parseDocxDocument(buffer, options) {
18178
18216
  if (stylesFile) {
18179
18217
  try {
18180
18218
  styles = parseStyles(await stylesFile.async("text"));
18181
- } catch {
18219
+ } catch (err) {
18220
+ warnings.push({
18221
+ code: "PARTIAL_PARSE",
18222
+ message: `DOCX \uC2A4\uD0C0\uC77C(styles.xml) \uD30C\uC2F1 \uC2E4\uD328 \u2014 \uAE30\uBCF8 \uC2A4\uD0C0\uC77C\uB85C \uACC4\uC18D: ${err instanceof Error ? err.message : String(err)}`
18223
+ });
18182
18224
  }
18183
18225
  }
18184
18226
  let numbering = /* @__PURE__ */ new Map();
@@ -18186,7 +18228,11 @@ async function parseDocxDocument(buffer, options) {
18186
18228
  if (numFile) {
18187
18229
  try {
18188
18230
  numbering = parseNumbering(await numFile.async("text"));
18189
- } catch {
18231
+ } catch (err) {
18232
+ warnings.push({
18233
+ code: "PARTIAL_PARSE",
18234
+ message: `DOCX \uBC88\uD638\uB9E4\uAE30\uAE30(numbering.xml) \uD30C\uC2F1 \uC2E4\uD328 \u2014 \uBAA9\uB85D \uBC88\uD638 \uC0DD\uB7B5: ${err instanceof Error ? err.message : String(err)}`
18235
+ });
18190
18236
  }
18191
18237
  }
18192
18238
  let footnotes = /* @__PURE__ */ new Map();
@@ -18194,7 +18240,11 @@ async function parseDocxDocument(buffer, options) {
18194
18240
  if (fnFile) {
18195
18241
  try {
18196
18242
  footnotes = parseFootnotes(await fnFile.async("text"));
18197
- } catch {
18243
+ } catch (err) {
18244
+ warnings.push({
18245
+ code: "PARTIAL_PARSE",
18246
+ message: `DOCX \uAC01\uC8FC(footnotes.xml) \uD30C\uC2F1 \uC2E4\uD328 \u2014 \uAC01\uC8FC \uC0DD\uB7B5: ${err instanceof Error ? err.message : String(err)}`
18247
+ });
18198
18248
  }
18199
18249
  }
18200
18250
  const docXml = await docFile.async("text");
@@ -18216,7 +18266,7 @@ async function parseDocxDocument(buffer, options) {
18216
18266
  if (block) blocks.push(block);
18217
18267
  }
18218
18268
  }
18219
- const { blocks: imgBlocks, images } = await extractImages(zip, rels, doc);
18269
+ const { blocks: imgBlocks, images } = await extractImages(zip, rels, doc, warnings);
18220
18270
  const metadata = {};
18221
18271
  const coreFile = zip.file("docProps/core.xml");
18222
18272
  if (coreFile) {
@@ -18234,7 +18284,11 @@ async function parseDocxDocument(buffer, options) {
18234
18284
  if (created) metadata.createdAt = created;
18235
18285
  const modified = getFirst("dcterms:modified");
18236
18286
  if (modified) metadata.modifiedAt = modified;
18237
- } catch {
18287
+ } catch (err) {
18288
+ warnings.push({
18289
+ code: "PARTIAL_PARSE",
18290
+ message: `DOCX \uBA54\uD0C0\uB370\uC774\uD130(core.xml) \uD30C\uC2F1 \uC2E4\uD328: ${err instanceof Error ? err.message : String(err)}`
18291
+ });
18238
18292
  }
18239
18293
  }
18240
18294
  const outline = blocks.filter((b) => b.type === "heading").map((b) => ({ level: b.level ?? 2, text: b.text ?? "" }));
@@ -19064,7 +19118,7 @@ function fillInlineFields(text, values, filled, matchedLabels) {
19064
19118
  }
19065
19119
 
19066
19120
  // src/form/filler-hwpx.ts
19067
- import JSZip4 from "jszip";
19121
+ import JSZip5 from "jszip";
19068
19122
 
19069
19123
  // src/roundtrip/source-map.ts
19070
19124
  function escapeXmlText(text) {
@@ -19738,7 +19792,7 @@ function patchZipEntries(original, replacements) {
19738
19792
  // src/form/filler-hwpx.ts
19739
19793
  async function fillHwpx(hwpxBuffer, values) {
19740
19794
  const u8 = new Uint8Array(hwpxBuffer);
19741
- const zip = await JSZip4.loadAsync(hwpxBuffer);
19795
+ const zip = await JSZip5.loadAsync(hwpxBuffer);
19742
19796
  const sectionPaths = Object.keys(zip.files).filter((name) => /[Ss]ection\d+\.xml$/i.test(name)).sort();
19743
19797
  if (sectionPaths.length === 0) {
19744
19798
  throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -19979,7 +20033,7 @@ async function fillHwpx(hwpxBuffer, values) {
19979
20033
  }
19980
20034
 
19981
20035
  // src/hwpx/generator.ts
19982
- import JSZip5 from "jszip";
20036
+ import JSZip6 from "jszip";
19983
20037
 
19984
20038
  // src/hwpx/text-metrics.ts
19985
20039
  var ASCII_W = [
@@ -20815,7 +20869,7 @@ async function markdownToHwpx(markdown, options) {
20815
20869
  const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
20816
20870
  const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
20817
20871
  const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit);
20818
- const zip = new JSZip5();
20872
+ const zip = new JSZip6();
20819
20873
  zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
20820
20874
  zip.file("META-INF/container.xml", generateContainerXml());
20821
20875
  zip.file("Contents/content.hpf", generateManifest());
@@ -21359,9 +21413,22 @@ function precomputeGongmunList(blocks, gongmun) {
21359
21413
  continue;
21360
21414
  }
21361
21415
  const run = [];
21362
- while (i < blocks.length && blocks[i].type === "list_item") {
21363
- run.push(i);
21364
- i++;
21416
+ while (i < blocks.length) {
21417
+ const t = blocks[i].type;
21418
+ if (t === "list_item") {
21419
+ run.push(i);
21420
+ i++;
21421
+ continue;
21422
+ }
21423
+ if (t === "table" || t === "html_table") {
21424
+ let j = i + 1;
21425
+ while (j < blocks.length && (blocks[j].type === "table" || blocks[j].type === "html_table")) j++;
21426
+ if (j < blocks.length && blocks[j].type === "list_item") {
21427
+ i = j;
21428
+ continue;
21429
+ }
21430
+ }
21431
+ break;
21365
21432
  }
21366
21433
  const depths = run.map((bi) => Math.min(Math.max(blocks[bi].indent || 0, 0), GONGMUN_LIST_LEVELS - 1));
21367
21434
  const suppress = gongmun.numbering === "standard" ? computeSuppression(depths) : depths.map(() => false);
@@ -21629,7 +21696,7 @@ function diffTableCells(a, b) {
21629
21696
  }
21630
21697
 
21631
21698
  // src/roundtrip/patcher.ts
21632
- import JSZip6 from "jszip";
21699
+ import JSZip7 from "jszip";
21633
21700
 
21634
21701
  // src/roundtrip/table-rows.ts
21635
21702
  var ROW_OBJECT_RE = /<(?:[A-Za-z0-9_]+:)?(?:tbl|pic|equation|ole|container|shape|drawingObject|drawText|video|chart|fieldBegin|fieldEnd|ctrl)\b/;
@@ -22267,7 +22334,7 @@ async function patchHwpx(original, editedMarkdown, options) {
22267
22334
  }
22268
22335
  let zip;
22269
22336
  try {
22270
- zip = await JSZip6.loadAsync(original);
22337
+ zip = await JSZip7.loadAsync(original);
22271
22338
  } catch {
22272
22339
  return { success: false, applied: 0, skipped, error: "ZIP \uB85C\uB4DC \uC2E4\uD328" };
22273
22340
  }
@@ -22930,21 +22997,24 @@ function readRecordsStrict(stream) {
22930
22997
  }
22931
22998
  return recs;
22932
22999
  }
22933
- function serializeRecords(recs, repl) {
23000
+ function serializeRecords(recs, repl, inserts) {
22934
23001
  const parts = [];
22935
- for (let i = 0; i < recs.length; i++) {
22936
- const data = repl?.get(i) ?? recs[i].data;
23002
+ const push = (tagId, level, data) => {
22937
23003
  const ext = data.length >= 4095;
22938
23004
  const header = Buffer.alloc(ext ? 8 : 4);
22939
- header.writeUInt32LE((recs[i].tagId & 1023 | (recs[i].level & 1023) << 10 | (ext ? 4095 : data.length) << 20) >>> 0, 0);
23005
+ header.writeUInt32LE((tagId & 1023 | (level & 1023) << 10 | (ext ? 4095 : data.length) << 20) >>> 0, 0);
22940
23006
  if (ext) header.writeUInt32LE(data.length, 4);
22941
23007
  parts.push(header, data);
23008
+ };
23009
+ for (let i = 0; i < recs.length; i++) {
23010
+ for (const ins of inserts?.get(i) ?? []) push(ins.tagId, ins.level, ins.data);
23011
+ push(recs[i].tagId, recs[i].level, repl?.get(i) ?? recs[i].data);
22942
23012
  }
22943
23013
  return Buffer.concat(parts);
22944
23014
  }
22945
23015
  function scanSection(stream, sectionIndex, compressed) {
22946
23016
  const records = readRecordsStrict(stream);
22947
- if (!records) return { records: [], safe: false, paras: [], tables: [], compressed, repl: /* @__PURE__ */ new Map() };
23017
+ if (!records) return { records: [], safe: false, paras: [], tables: [], compressed, repl: /* @__PURE__ */ new Map(), inserts: /* @__PURE__ */ new Map() };
22948
23018
  const safe = serializeRecords(records).equals(stream);
22949
23019
  const parent = new Int32Array(records.length).fill(-1);
22950
23020
  const stack = [];
@@ -23043,7 +23113,7 @@ function scanSection(stream, sectionIndex, compressed) {
23043
23113
  }
23044
23114
  tables.push({ sectionIndex, rows, cols, cells });
23045
23115
  }
23046
- return { records, safe, paras, tables, compressed, repl: /* @__PURE__ */ new Map() };
23116
+ return { records, safe, paras, tables, compressed, repl: /* @__PURE__ */ new Map(), inserts: /* @__PURE__ */ new Map() };
23047
23117
  }
23048
23118
  async function patchHwp(original, editedMarkdown, options) {
23049
23119
  const skipped = [];
@@ -23111,15 +23181,15 @@ async function patchHwp(original, editedMarkdown, options) {
23111
23181
  }
23112
23182
  }
23113
23183
  let data;
23114
- const dirty = scans.some((s) => s.repl.size > 0);
23184
+ const dirty = scans.some((s) => s.repl.size > 0 || s.inserts.size > 0);
23115
23185
  if (!dirty) {
23116
23186
  data = new Uint8Array(original);
23117
23187
  } else {
23118
23188
  try {
23119
23189
  let out = originalBuf;
23120
23190
  for (let i = 0; i < scans.length; i++) {
23121
- if (scans[i].repl.size === 0) continue;
23122
- const newStream = serializeRecords(scans[i].records, scans[i].repl);
23191
+ if (scans[i].repl.size === 0 && scans[i].inserts.size === 0) continue;
23192
+ const newStream = serializeRecords(scans[i].records, scans[i].repl, scans[i].inserts);
23123
23193
  const content = compressed ? deflateRawSync2(newStream) : newStream;
23124
23194
  out = replaceOleStream(out, sectionPaths[i], content);
23125
23195
  }
@@ -23447,24 +23517,25 @@ function applyCellEdit5(table, scanTable, gridR, gridC, newLines, ctx, before, a
23447
23517
  }
23448
23518
  const unstable = newLines.find((l) => sanitizeText(l) !== l);
23449
23519
  if (unstable !== void 0) return skip("\uACF5\uBC31 \uC815\uADDC\uD654 \uBD88\uC548\uC815 \uD14D\uC2A4\uD2B8 \u2014 \uD328\uCE58 \uC2DC \uC6D0\uBB38 \uBCF4\uC874 \uBD88\uAC00\uB85C \uBBF8\uC9C0\uC6D0");
23450
- if (nonEmpty.length === 0) return skip("\uBE48 \uC140 \uD14D\uC2A4\uD2B8 \uCC44\uC6B0\uAE30\uB294 HWP5 \uBBF8\uC9C0\uC6D0 (v1) \u2014 \uBB38\uB2E8 \uC0DD\uC131 \uD544\uC694");
23520
+ const targets = nonEmpty.length > 0 ? nonEmpty : cell.paras;
23521
+ if (targets.length === 0) return skip("\uC140\uC5D0 \uBB38\uB2E8\uC774 \uC5C6\uC74C \u2014 \uBBF8\uC9C0\uC6D0");
23451
23522
  const assigned = [];
23452
- for (let i = 0; i < nonEmpty.length; i++) {
23523
+ for (let i = 0; i < targets.length; i++) {
23453
23524
  if (i < newLines.length) {
23454
- assigned.push(i === nonEmpty.length - 1 && newLines.length > nonEmpty.length ? newLines.slice(i).join(" ") : newLines[i]);
23525
+ assigned.push(i === targets.length - 1 && newLines.length > targets.length ? newLines.slice(i).join(" ") : newLines[i]);
23455
23526
  } else {
23456
23527
  assigned.push("");
23457
23528
  }
23458
23529
  }
23459
- if (newLines.length > nonEmpty.length) {
23530
+ if (newLines.length > targets.length) {
23460
23531
  ctx.skipped.push({ reason: "\uC140 \uB0B4 \uC904 \uCD94\uAC00\uB294 \uBB38\uB2E8 \uC0DD\uC131 \uBBF8\uC9C0\uC6D0 \u2014 \uB9C8\uC9C0\uB9C9 \uBB38\uB2E8\uC5D0 \uBCD1\uD569 \uC801\uC6A9", after: summarize(after), partial: true });
23461
23532
  } else if (newLines.length < nonEmpty.length && nonEmpty.length > 1) {
23462
23533
  ctx.skipped.push({ reason: "\uC140 \uB0B4 \uC904 \uC0AD\uC81C\uB294 \uBB38\uB2E8 \uC81C\uAC70 \uBBF8\uC9C0\uC6D0 \u2014 \uBE48 \uBB38\uB2E8 \uC794\uC874(\uBDF0\uC5B4\uC5D0 \uBE48 \uC904 \uD45C\uC2DC \uAC00\uB2A5)", before: summarize(before), after: summarize(after), partial: true });
23463
23534
  }
23464
23535
  let staged = 0;
23465
- for (let i = 0; i < nonEmpty.length; i++) {
23466
- if (assigned[i] === nonEmpty[i].rawText || normForMatch(assigned[i]) === normForMatch(nonEmpty[i].rawText)) continue;
23467
- staged += stageParaPatch(ctx.scans[nonEmpty[i].sectionIndex], nonEmpty[i], assigned[i], skip);
23536
+ for (let i = 0; i < targets.length; i++) {
23537
+ if (assigned[i] === targets[i].rawText || normForMatch(assigned[i]) === normForMatch(targets[i].rawText)) continue;
23538
+ staged += stageParaPatch(ctx.scans[targets[i].sectionIndex], targets[i], assigned[i], skip);
23468
23539
  }
23469
23540
  return staged > 0 ? 1 : 0;
23470
23541
  }
@@ -23534,7 +23605,21 @@ function splitParaText(data) {
23534
23605
  if (firstP < 0) firstP = k;
23535
23606
  lastP = k;
23536
23607
  }
23537
- if (firstP < 0) return null;
23608
+ if (firstP < 0) {
23609
+ if (toks.some((t) => t.visible)) return null;
23610
+ let cut = data.length;
23611
+ for (const t of toks) if (data.readUInt16LE(t.start) === 13) {
23612
+ cut = t.start;
23613
+ break;
23614
+ }
23615
+ return {
23616
+ prefix: data.subarray(0, cut),
23617
+ prefixUnits: cut / 2,
23618
+ core: "",
23619
+ suffix: data.subarray(cut),
23620
+ suffixUnits: (data.length - cut) / 2
23621
+ };
23622
+ }
23538
23623
  for (let k = firstP; k <= lastP; k++) if (!toks[k].plain) return null;
23539
23624
  for (let k = 0; k < firstP; k++) if (toks[k].visible) return null;
23540
23625
  for (let k = lastP + 1; k < toks.length; k++) if (toks[k].visible) return null;
@@ -23567,7 +23652,6 @@ function rebuildCharShape(csData, coreStartUnit) {
23567
23652
  }
23568
23653
  function stageParaPatch(scan, para, newPlain, skip) {
23569
23654
  if (!scan.safe) return skip("\uC139\uC158 \uB808\uCF54\uB4DC \uC7AC\uC9C1\uB82C\uD654 \uBD88\uC77C\uCE58 \u2014 \uC548\uC804\uC744 \uC704\uD574 \uC774 \uC139\uC158\uC740 \uBBF8\uC9C0\uC6D0");
23570
- if (para.textIdx === -1) return skip("\uBE48 \uBB38\uB2E8 \uD14D\uC2A4\uD2B8 \uCD94\uAC00\uB294 \uBBF8\uC9C0\uC6D0 (v1)");
23571
23655
  if (para.textIdx === -2) return skip("\uBCF5\uC218 PARA_TEXT \uB808\uCF54\uB4DC \uBB38\uB2E8 \u2014 \uBBF8\uC9C0\uC6D0 (v1)");
23572
23656
  if (para.rangeTagCount > 0) return skip("\uBC94\uC704 \uD0DC\uADF8(\uD615\uAD11\uD39C/\uAD50\uC815\uBD80\uD638) \uBB38\uB2E8 \u2014 \uBBF8\uC9C0\uC6D0 (v1)");
23573
23657
  if (para.charShapeIdx < 0 || para.lineSegIdx < 0) return skip("\uBB38\uB2E8 \uB808\uCF54\uB4DC \uAD6C\uC131 \uBE44\uC815\uD615 \u2014 \uBBF8\uC9C0\uC6D0");
@@ -23575,11 +23659,27 @@ function stageParaPatch(scan, para, newPlain, skip) {
23575
23659
  if (/[\u0000-\u001f]/.test(newPlain)) return skip("\uC0C8 \uD14D\uC2A4\uD2B8\uC5D0 \uC81C\uC5B4\uBB38\uC790 \uD3EC\uD568 \u2014 \uBBF8\uC9C0\uC6D0");
23576
23660
  const records = scan.records;
23577
23661
  const headerRec = records[para.headerIdx];
23578
- const textRec = records[para.textIdx];
23579
23662
  const charShapeRec = records[para.charShapeIdx];
23580
23663
  if (charShapeRec.data.length < 8) {
23581
23664
  return skip("CHAR_SHAPE \uB808\uCF54\uB4DC \uBE44\uC815\uD615 \u2014 \uBBF8\uC9C0\uC6D0");
23582
23665
  }
23666
+ if (para.textIdx === -1) {
23667
+ const nCharsLow = para.nCharsRaw & 2147483647;
23668
+ if (nCharsLow > 1) return skip("PARA_TEXT \uC5C6\uB294 \uBB38\uB2E8\uC758 nChars \uBE44\uC815\uD615 \u2014 \uBBF8\uC9C0\uC6D0");
23669
+ const paraEnd = nCharsLow === 1 ? Buffer.from([13, 0]) : Buffer.alloc(0);
23670
+ const at = para.headerIdx + 1;
23671
+ const list = scan.inserts.get(at) ?? [];
23672
+ list.push({ tagId: TAG_PARA_TEXT, level: headerRec.level + 1, data: Buffer.concat([Buffer.from(newPlain, "utf16le"), paraEnd]) });
23673
+ scan.inserts.set(at, list);
23674
+ const newHeader2 = Buffer.from(headerRec.data);
23675
+ newHeader2.writeUInt32LE((para.nCharsRaw & 2147483648 | newPlain.length + nCharsLow) >>> 0, 0);
23676
+ const cs2 = rebuildCharShape(charShapeRec.data, 0);
23677
+ scan.repl.set(para.charShapeIdx, cs2.buf);
23678
+ newHeader2.writeUInt16LE(cs2.count, 12);
23679
+ scan.repl.set(para.headerIdx, newHeader2);
23680
+ return 1;
23681
+ }
23682
+ const textRec = records[para.textIdx];
23583
23683
  const seg = splitParaText(textRec.data);
23584
23684
  if (!seg) {
23585
23685
  return skip(para.ctrlMask !== 0 ? "\uCEE8\uD2B8\uB864 \uBB38\uC790(\uD0ED/\uD544\uB4DC/\uD2B9\uC218\uACF5\uBC31 \uB4F1 \uD14D\uC2A4\uD2B8 \uC911\uAC04) \uD3EC\uD568 \uBB38\uB2E8 \u2014 \uBBF8\uC9C0\uC6D0 (v1)" : "PARA_TEXT \uC7AC\uAD6C\uC131 \uBD88\uC77C\uCE58 \u2014 \uC6D0\uBB38 \uBCF4\uC874 \uBD88\uAC00\uB85C \uBBF8\uC9C0\uC6D0");
@@ -23601,10 +23701,10 @@ function stageParaPatch(scan, para, newPlain, skip) {
23601
23701
  }
23602
23702
 
23603
23703
  // src/roundtrip/session.ts
23604
- import JSZip7 from "jszip";
23704
+ import JSZip8 from "jszip";
23605
23705
  async function buildState(bytes) {
23606
23706
  const parsed = await parseHwpxDocument(u8ToArrayBuffer2(bytes));
23607
- const zip = await JSZip7.loadAsync(bytes);
23707
+ const zip = await JSZip8.loadAsync(bytes);
23608
23708
  const sectionPaths = await resolveSectionEntryNames(zip);
23609
23709
  if (sectionPaths.length === 0) {
23610
23710
  throw new Error("HWPX \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -24185,7 +24285,7 @@ async function parseHwp(buffer, options) {
24185
24285
  async function parsePdf(buffer, options) {
24186
24286
  let parsePdfDocument;
24187
24287
  try {
24188
- const mod = await import("./parser-DR5CTZ74.js");
24288
+ const mod = await import("./parser-FFEBMLSH.js");
24189
24289
  parsePdfDocument = mod.parsePdfDocument;
24190
24290
  } catch {
24191
24291
  return {
@@ -24320,4 +24420,4 @@ export {
24320
24420
  parseHwpml,
24321
24421
  fillForm
24322
24422
  };
24323
- //# sourceMappingURL=chunk-NXRABCWW.js.map
24423
+ //# sourceMappingURL=chunk-KT2BCHXI.js.map