kordoc 3.8.0 → 3.8.2

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 (32) hide show
  1. package/README.md +11 -1
  2. package/dist/{-ATVQYFSW.js → -FEHSMPVO.js} +3 -3
  3. package/dist/{chunk-QV25HMU7.js → chunk-553VTUVP.js} +2 -2
  4. package/dist/{chunk-SLKF72QF.js → chunk-DP37KF2X.js} +1985 -1954
  5. package/dist/chunk-DP37KF2X.js.map +1 -0
  6. package/dist/{chunk-3R3YK7EM.js → chunk-JHZUFBUV.js} +2 -2
  7. package/dist/{chunk-ITJIALN5.cjs → chunk-YBPNKFJW.cjs} +2 -2
  8. package/dist/{chunk-ITJIALN5.cjs.map → chunk-YBPNKFJW.cjs.map} +1 -1
  9. package/dist/cli.js +4 -4
  10. package/dist/index.cjs +2059 -2028
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.d.cts +17 -8
  13. package/dist/index.d.ts +17 -8
  14. package/dist/index.js +1974 -1943
  15. package/dist/index.js.map +1 -1
  16. package/dist/mcp.js +3 -3
  17. package/dist/{parser-7G5F7PT2.js → parser-KNQDRLZQ.js} +1930 -1863
  18. package/dist/parser-KNQDRLZQ.js.map +1 -0
  19. package/dist/{parser-DCK42RMA.js → parser-NR2TYGO3.js} +1930 -1863
  20. package/dist/parser-NR2TYGO3.js.map +1 -0
  21. package/dist/{parser-GUSJH44K.cjs → parser-NS4ZPD7B.cjs} +1933 -1864
  22. package/dist/parser-NS4ZPD7B.cjs.map +1 -0
  23. package/dist/{watch-GVZESOCE.js → watch-XCWADLPU.js} +3 -3
  24. package/package.json +2 -2
  25. package/dist/chunk-SLKF72QF.js.map +0 -1
  26. package/dist/parser-7G5F7PT2.js.map +0 -1
  27. package/dist/parser-DCK42RMA.js.map +0 -1
  28. package/dist/parser-GUSJH44K.cjs.map +0 -1
  29. /package/dist/{-ATVQYFSW.js.map → -FEHSMPVO.js.map} +0 -0
  30. /package/dist/{chunk-QV25HMU7.js.map → chunk-553VTUVP.js.map} +0 -0
  31. /package/dist/{chunk-3R3YK7EM.js.map → chunk-JHZUFBUV.js.map} +0 -0
  32. /package/dist/{watch-GVZESOCE.js.map → watch-XCWADLPU.js.map} +0 -0
@@ -25,7 +25,7 @@ import {
25
25
  sanitizeHref,
26
26
  stripDtd,
27
27
  toArrayBuffer
28
- } from "./chunk-3R3YK7EM.js";
28
+ } from "./chunk-JHZUFBUV.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) {
@@ -843,1038 +914,986 @@ function resolveParaHeading(paraEl, ctx) {
843
914
  });
844
915
  return { prefix, headingLevel };
845
916
  }
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);
917
+
918
+ // src/hwpx/table-build.ts
919
+ function buildTableWithCellMeta(state) {
920
+ const table = buildTable(state.rows);
921
+ if (state.caption) table.caption = state.caption;
922
+ const anchors = [];
923
+ {
924
+ const covered = /* @__PURE__ */ new Set();
925
+ for (let r = 0; r < table.rows; r++) {
926
+ for (let c = 0; c < table.cols; c++) {
927
+ if (covered.has(`${r},${c}`)) continue;
928
+ const cell = table.cells[r]?.[c];
929
+ if (!cell) continue;
930
+ for (let dr = 0; dr < cell.rowSpan; dr++) {
931
+ for (let dc = 0; dc < cell.colSpan; dc++) {
932
+ if (dr === 0 && dc === 0) continue;
933
+ if (r + dr < table.rows && c + dc < table.cols) covered.add(`${r + dr},${c + dc}`);
934
+ }
866
935
  }
936
+ anchors.push(cell);
937
+ c += cell.colSpan - 1;
867
938
  }
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
939
  }
870
940
  }
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" });
941
+ const srcCount = state.rows.reduce((s, r) => s + r.length, 0);
942
+ const ordinalReliable = anchors.length === srcCount;
943
+ const claimed = /* @__PURE__ */ new Set();
944
+ let flatIdx = -1;
945
+ for (const row of state.rows) {
946
+ for (const src of row) {
947
+ flatIdx++;
948
+ const needsBlocks = src.hasStructure && src.blocks && src.blocks.length > 0;
949
+ if (!needsBlocks && !src.isHeader) continue;
950
+ let target;
951
+ const trimmed = src.text.trim();
952
+ if (src.rowAddr !== void 0 && src.colAddr !== void 0) {
953
+ const cand = table.cells[src.rowAddr]?.[src.colAddr];
954
+ if (cand && cand.text === trimmed && !claimed.has(cand)) target = cand;
955
+ }
956
+ if (!target) {
957
+ outer: for (const irRow of table.cells) {
958
+ for (const cand of irRow) {
959
+ if (!claimed.has(cand) && cand.text === trimmed && cand.colSpan === src.colSpan && cand.rowSpan === src.rowSpan) {
960
+ target = cand;
961
+ break outer;
962
+ }
963
+ }
964
+ }
965
+ }
966
+ if (!target && ordinalReliable) {
967
+ const cand = anchors[flatIdx];
968
+ if (cand && !claimed.has(cand)) target = cand;
969
+ }
970
+ if (!target) continue;
971
+ claimed.add(target);
972
+ if (needsBlocks) target.blocks = src.blocks;
973
+ if (src.isHeader) target.isHeader = true;
898
974
  }
899
975
  }
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 };
976
+ return table;
906
977
  }
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 })));
978
+ function completeTable(newTable, tableStack, blocks, ctx) {
979
+ const parentTable = tableStack.length > 0 ? tableStack.pop() : null;
980
+ if (newTable.rows.length === 0) {
981
+ if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
982
+ return parentTable;
911
983
  }
912
- if (footers.length > 0) {
913
- blocks.push(...footers.map((t) => ({ type: "paragraph", text: t })));
984
+ const ir = buildTableWithCellMeta(newTable);
985
+ const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
986
+ if (parentTable?.cell) {
987
+ const cell = parentTable.cell;
988
+ (cell.blocks ??= []).push(block);
989
+ cell.hasStructure = true;
990
+ let flat = convertTableToText(newTable.rows);
991
+ if (newTable.caption) flat = newTable.caption + (flat ? "\n" + flat : "");
992
+ if (flat) cell.text += (cell.text ? "\n" : "") + flat;
993
+ } else {
994
+ blocks.push(block);
914
995
  }
996
+ return parentTable;
915
997
  }
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";
998
+
999
+ // src/hwpx/section-walker.ts
1000
+ function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
1001
+ const parser = createXmlParser(warnings);
1002
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1003
+ if (!doc.documentElement) return [];
1004
+ const ctx = { styleMap, warnings, sectionNum, shared: shared ?? createSectionShared() };
1005
+ ctx.shared.track.deleteDepth = 0;
1006
+ for (const tagName of ["hp:secPr", "secPr"]) {
1007
+ const els = doc.getElementsByTagName(tagName);
1008
+ if (els.length > 0) {
1009
+ const v = els[0].getAttribute("outlineShapeIDRef");
1010
+ if (v) ctx.outlineNumId = v;
1011
+ break;
1012
+ }
938
1013
  }
1014
+ const blocks = [];
1015
+ walkSection(doc.documentElement, blocks, null, [], ctx);
1016
+ return blocks;
939
1017
  }
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
- }
1018
+ function extractImageRef(el) {
1019
+ const children = el.childNodes;
1020
+ if (!children) return null;
1021
+ for (let i = 0; i < children.length; i++) {
1022
+ const child = children[i];
1023
+ if (child.nodeType !== 1) continue;
1024
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1025
+ if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
1026
+ const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
1027
+ if (ref) return ref;
962
1028
  }
1029
+ const nested = extractImageRef(child);
1030
+ if (nested) return nested;
963
1031
  }
1032
+ const directRef = el.getAttribute("binaryItemIDRef") || "";
1033
+ if (directRef) return directRef;
1034
+ return null;
964
1035
  }
965
- async function extractImagesFromZip(zip, blocks, decompressed, warnings) {
966
- const images = [];
967
- let imageIndex = 0;
968
- const imageBlocks = [];
969
- collectImageBlocks(blocks, imageBlocks);
970
- const resolved = /* @__PURE__ */ new Map();
971
- for (const { block, ownerCell } of imageBlocks) {
972
- if (block.type !== "image" || !block.text) continue;
973
- const ref = block.text;
974
- let img = resolved.get(ref);
975
- if (img === void 0) {
976
- img = null;
977
- const candidates = [
978
- `BinData/${ref}`,
979
- `Contents/BinData/${ref}`,
980
- ref
981
- // 절대 경로일 수도 있음
982
- ];
983
- let resolvedPath = null;
984
- if (!ref.includes(".")) {
985
- const prefixes = [`BinData/${ref}`, `Contents/BinData/${ref}`];
986
- for (const prefix of prefixes) {
987
- const match = zip.file(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.[a-zA-Z0-9]+$`));
988
- if (match.length > 0) {
989
- resolvedPath = match[0].name;
990
- break;
1036
+ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1037
+ if (depth > MAX_XML_DEPTH) return;
1038
+ const children = node.childNodes;
1039
+ if (!children) return;
1040
+ for (let i = 0; i < children.length; i++) {
1041
+ const el = children[i];
1042
+ if (el.nodeType !== 1) continue;
1043
+ const tag = el.tagName || el.localName || "";
1044
+ const localTag = tag.replace(/^[^:]+:/, "");
1045
+ switch (localTag) {
1046
+ case "tbl": {
1047
+ if (tableCtx) tableStack.push(tableCtx);
1048
+ const newTable = { rows: [], currentRow: [], cell: null };
1049
+ walkSection(el, blocks, newTable, tableStack, ctx, depth + 1);
1050
+ tableCtx = completeTable(newTable, tableStack, blocks, ctx);
1051
+ break;
1052
+ }
1053
+ // 표/도표 캡션 — IRTable.caption으로 보존 (v3.0, 기존 무음 드롭 수정)
1054
+ case "caption":
1055
+ if (tableCtx) {
1056
+ const capText = collectSubListText(el, ctx);
1057
+ if (capText) tableCtx.caption = (tableCtx.caption ? tableCtx.caption + "\n" : "") + capText;
1058
+ }
1059
+ break;
1060
+ case "tr":
1061
+ if (tableCtx) {
1062
+ tableCtx.currentRow = [];
1063
+ walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1064
+ if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
1065
+ tableCtx.currentRow = [];
1066
+ }
1067
+ break;
1068
+ case "tc":
1069
+ if (tableCtx) {
1070
+ tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
1071
+ if (el.getAttribute("header") === "1" || el.getAttribute("header") === "true") tableCtx.cell.isHeader = true;
1072
+ walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1073
+ if (tableCtx.cell) {
1074
+ tableCtx.currentRow.push(tableCtx.cell);
1075
+ tableCtx.cell = null;
1076
+ }
1077
+ }
1078
+ break;
1079
+ case "cellAddr":
1080
+ if (tableCtx?.cell) {
1081
+ const ca = parseInt(el.getAttribute("colAddr") || "", 10);
1082
+ const ra = parseInt(el.getAttribute("rowAddr") || "", 10);
1083
+ if (!isNaN(ca)) tableCtx.cell.colAddr = ca;
1084
+ if (!isNaN(ra)) tableCtx.cell.rowAddr = ra;
1085
+ }
1086
+ break;
1087
+ case "cellSpan":
1088
+ if (tableCtx?.cell) {
1089
+ const rawCs = parseInt(el.getAttribute("colSpan") || "1", 10);
1090
+ const cs = isNaN(rawCs) ? 1 : rawCs;
1091
+ const rawRs = parseInt(el.getAttribute("rowSpan") || "1", 10);
1092
+ const rs = isNaN(rawRs) ? 1 : rawRs;
1093
+ tableCtx.cell.colSpan = clampSpan(cs, MAX_COLS);
1094
+ tableCtx.cell.rowSpan = clampSpan(rs, MAX_ROWS);
1095
+ }
1096
+ break;
1097
+ case "p": {
1098
+ const { text: rawText, href, footnote, style } = extractParagraphInfo(el, ctx.styleMap, ctx);
1099
+ let text = rawText;
1100
+ let headingLevel;
1101
+ if (text) {
1102
+ const ph = resolveParaHeading(el, ctx);
1103
+ if (ph?.prefix) text = ph.prefix + " " + text;
1104
+ headingLevel = ph?.headingLevel;
1105
+ }
1106
+ if (text) {
1107
+ if (tableCtx?.cell) {
1108
+ const cell = tableCtx.cell;
1109
+ if (footnote) text += ` (\uC8FC: ${footnote})`;
1110
+ cell.text += (cell.text ? "\n" : "") + text;
1111
+ (cell.blocks ??= []).push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
1112
+ } else if (!tableCtx) {
1113
+ const block = { type: headingLevel ? "heading" : "paragraph", text, pageNumber: ctx.sectionNum };
1114
+ if (headingLevel) block.level = headingLevel;
1115
+ if (style) block.style = style;
1116
+ if (href) block.href = href;
1117
+ if (footnote) block.footnoteText = footnote;
1118
+ blocks.push(block);
1119
+ } else {
1120
+ blocks.push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
991
1121
  }
992
1122
  }
1123
+ tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1124
+ break;
993
1125
  }
994
- const allCandidates = resolvedPath ? [resolvedPath, ...candidates] : candidates;
995
- for (const path of allCandidates) {
996
- if (isPathTraversal(path)) continue;
997
- const file = zip.file(path);
998
- if (!file) continue;
999
- try {
1000
- const data = await file.async("uint8array");
1001
- decompressed.total += data.length;
1002
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1003
- const ext = path.includes(".") ? path.split(".").pop() || "png" : "png";
1004
- const mimeType = imageExtToMime(ext);
1005
- imageIndex++;
1006
- const filename = `image_${String(imageIndex).padStart(3, "0")}.${mimeToExt(mimeType)}`;
1007
- img = { filename, data, mimeType };
1008
- images.push(img);
1009
- break;
1010
- } catch (err) {
1011
- if (err instanceof KordocError) throw err;
1126
+ // 이미지/그림/글상자 이미지·텍스트·캡션 병행 추출
1127
+ case "pic":
1128
+ case "shape":
1129
+ case "drawingObject": {
1130
+ if (tableCtx?.cell) {
1131
+ const sink = [];
1132
+ handleShape(el, sink, ctx);
1133
+ mergeBlocksIntoCell(tableCtx.cell, sink);
1134
+ } else {
1135
+ handleShape(el, blocks, ctx);
1012
1136
  }
1137
+ break;
1013
1138
  }
1014
- if (!img) warnings?.push({ page: block.pageNumber, message: `\uC774\uBBF8\uC9C0 \uD30C\uC77C \uC5C6\uC74C: ${ref}`, code: "SKIPPED_IMAGE" });
1015
- resolved.set(ref, img);
1016
- }
1017
- if (!img) {
1018
- block.type = "paragraph";
1019
- block.text = `[\uC774\uBBF8\uC9C0: ${ref}]`;
1020
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `[\uC774\uBBF8\uC9C0: ${ref}]`);
1021
- continue;
1022
- }
1023
- block.text = img.filename;
1024
- block.imageData = { data: img.data, mimeType: img.mimeType, filename: ref };
1025
- if (ownerCell) ownerCell.text = ownerCell.text.replace(`![image](${ref})`, `![image](${img.filename})`);
1026
- }
1027
- return images;
1028
- }
1029
- async function extractHwpxMetadata(zip, metadata, decompressed) {
1030
- try {
1031
- const metaPaths = ["meta.xml", "META-INF/meta.xml", "docProps/core.xml"];
1032
- for (const mp of metaPaths) {
1033
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mp.toLowerCase()) || null;
1034
- if (!file) continue;
1035
- const xml = await file.async("text");
1036
- if (decompressed) {
1037
- decompressed.total += xml.length * 2;
1038
- if (decompressed.total > MAX_DECOMPRESS_SIZE) throw new KordocError("ZIP \uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC (ZIP bomb \uC758\uC2EC)");
1139
+ // 메모 본문 혼입 차단 (v3.0)
1140
+ case "memogroup":
1141
+ case "memo": {
1142
+ if (ctx.warnings && extractTextFromNode(el)) {
1143
+ ctx.warnings.push({ page: ctx.sectionNum, message: "\uBA54\uBAA8 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38 \uC81C\uC678: memogroup", code: "HIDDEN_TEXT_FILTERED" });
1144
+ }
1145
+ break;
1039
1146
  }
1040
- parseDublinCoreMetadata(xml, metadata);
1041
- if (metadata.title || metadata.author) return;
1147
+ default:
1148
+ walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1149
+ break;
1042
1150
  }
1043
- } catch {
1044
1151
  }
1045
1152
  }
1046
- function parseDublinCoreMetadata(xml, metadata) {
1047
- const parser = createXmlParser();
1048
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1049
- if (!doc.documentElement) return;
1050
- const getText = (tagNames) => {
1051
- for (const tag of tagNames) {
1052
- const els = doc.getElementsByTagName(tag);
1053
- if (els.length > 0) {
1054
- const text = els[0].textContent?.trim();
1055
- if (text) return text;
1056
- }
1057
- }
1058
- return void 0;
1059
- };
1060
- metadata.title = metadata.title || getText(["dc:title", "title"]);
1061
- metadata.author = metadata.author || getText(["dc:creator", "creator", "cp:lastModifiedBy"]);
1062
- metadata.description = metadata.description || getText(["dc:description", "description", "dc:subject", "subject"]);
1063
- metadata.createdAt = metadata.createdAt || getText(["dcterms:created", "meta:creation-date"]);
1064
- metadata.modifiedAt = metadata.modifiedAt || getText(["dcterms:modified", "meta:date"]);
1065
- const keywords = getText(["dc:keyword", "cp:keywords", "meta:keyword"]);
1066
- if (keywords && !metadata.keywords) {
1067
- metadata.keywords = keywords.split(/[,;]/).map((k) => k.trim()).filter(Boolean);
1153
+ function handleShape(el, sink, ctx) {
1154
+ const imgRef = extractImageRef(el);
1155
+ const drawTextChild = findDescendant(el, "drawText");
1156
+ if (imgRef) {
1157
+ const block = { type: "image", text: imgRef, pageNumber: ctx.sectionNum };
1158
+ const alt = userShapeComment(el);
1159
+ if (alt) block.footnoteText = alt;
1160
+ sink.push(block);
1068
1161
  }
1069
- }
1070
- async function extractHwpxMetadataOnly(buffer) {
1071
- let zip;
1072
- try {
1073
- zip = await JSZip.loadAsync(buffer);
1074
- } catch {
1075
- throw new KordocError("HWPX ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
1162
+ if (drawTextChild) {
1163
+ extractDrawTextBlocks(drawTextChild, sink, ctx);
1164
+ }
1165
+ const capEl = findChildByLocalName(el, "caption");
1166
+ if (capEl) {
1167
+ const capText = collectSubListText(capEl, ctx);
1168
+ if (capText) sink.push({ type: "paragraph", text: capText, pageNumber: ctx.sectionNum });
1169
+ }
1170
+ if (!imgRef && !drawTextChild && ctx.warnings && ctx.sectionNum) {
1171
+ const localTag = (el.tagName || el.localName || "").replace(/^[^:]+:/, "");
1172
+ ctx.warnings.push({ page: ctx.sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
1076
1173
  }
1077
- const metadata = {};
1078
- await extractHwpxMetadata(zip, metadata);
1079
- const sectionPaths = await resolveSectionPaths(zip);
1080
- metadata.pageCount = sectionPaths.length;
1081
- return metadata;
1082
1174
  }
1083
- function extractFromBrokenZip(buffer) {
1084
- const data = new Uint8Array(buffer);
1085
- const view = new DataView(buffer);
1086
- let pos = 0;
1087
- const blocks = [];
1088
- const warnings = [
1089
- { code: "BROKEN_ZIP_RECOVERY", message: "\uC190\uC0C1\uB41C ZIP \uAD6C\uC870 \u2014 Local File Header \uAE30\uBC18 \uBCF5\uAD6C \uBAA8\uB4DC" }
1090
- ];
1091
- let totalDecompressed = 0;
1092
- let entryCount = 0;
1093
- let sectionNum = 0;
1094
- const shared = createSectionShared();
1095
- while (pos < data.length - 30) {
1096
- if (data[pos] !== 80 || data[pos + 1] !== 75 || data[pos + 2] !== 3 || data[pos + 3] !== 4) {
1097
- pos++;
1098
- while (pos < data.length - 30) {
1099
- if (data[pos] === 80 && data[pos + 1] === 75 && data[pos + 2] === 3 && data[pos + 3] === 4) break;
1100
- pos++;
1175
+ function userShapeComment(el) {
1176
+ const commentEl = findChildByLocalName(el, "shapeComment");
1177
+ if (!commentEl) return void 0;
1178
+ const text = extractTextFromNode(commentEl);
1179
+ if (!text) return void 0;
1180
+ if (/^그림입니다/.test(text)) return void 0;
1181
+ if (/^(?:모서리가 둥근 |둥근 )?[^\n]{1,20}입니다\.?$/.test(text)) return void 0;
1182
+ return text;
1183
+ }
1184
+ function mergeBlocksIntoCell(cell, sink) {
1185
+ for (const b of sink) {
1186
+ if ((b.type === "paragraph" || b.type === "heading") && b.text) {
1187
+ cell.text += (cell.text ? "\n" : "") + b.text;
1188
+ (cell.blocks ??= []).push(b);
1189
+ } else if (b.type === "image" || b.type === "table") {
1190
+ if (b.type === "image" && b.text) {
1191
+ cell.text += (cell.text ? "\n" : "") + `![image](${b.text})`;
1101
1192
  }
1102
- continue;
1103
- }
1104
- if (++entryCount > MAX_ZIP_ENTRIES) break;
1105
- const method = view.getUint16(pos + 8, true);
1106
- const compSize = view.getUint32(pos + 18, true);
1107
- const nameLen = view.getUint16(pos + 26, true);
1108
- const extraLen = view.getUint16(pos + 28, true);
1109
- if (nameLen > 1024 || extraLen > 65535) {
1110
- pos += 30 + nameLen + extraLen;
1111
- continue;
1193
+ ;
1194
+ (cell.blocks ??= []).push(b);
1195
+ cell.hasStructure = true;
1112
1196
  }
1113
- const fileStart = pos + 30 + nameLen + extraLen;
1114
- if (fileStart + compSize > data.length) break;
1115
- if (compSize === 0 && method !== 0) {
1116
- pos = fileStart;
1197
+ }
1198
+ }
1199
+ function collectSubListText(el, ctx, depth = 0) {
1200
+ if (depth > 10) return "";
1201
+ const parts = [];
1202
+ const children = el.childNodes;
1203
+ if (!children) return "";
1204
+ for (let i = 0; i < children.length; i++) {
1205
+ const ch = children[i];
1206
+ if (ch.nodeType !== 1) continue;
1207
+ const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1208
+ if (tag === "p" || tag === "para") {
1209
+ const t = extractParagraphInfo(ch, ctx.styleMap, ctx).text;
1210
+ if (t) parts.push(t);
1211
+ } else if (tag === "tbl") {
1117
1212
  continue;
1213
+ } else {
1214
+ const t = collectSubListText(ch, ctx, depth + 1);
1215
+ if (t) parts.push(t);
1118
1216
  }
1119
- const nameBytes = data.slice(pos + 30, pos + 30 + nameLen);
1120
- const name = new TextDecoder().decode(nameBytes);
1121
- if (isPathTraversal(name)) {
1122
- pos = fileStart + compSize;
1123
- continue;
1217
+ }
1218
+ return parts.join("\n").trim();
1219
+ }
1220
+ function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1221
+ if (depth > MAX_XML_DEPTH) return tableCtx;
1222
+ const children = node.childNodes;
1223
+ if (!children) return tableCtx;
1224
+ const walkChildren = (parent, d) => {
1225
+ if (d > MAX_XML_DEPTH) return;
1226
+ const kids2 = parent.childNodes;
1227
+ if (!kids2) return;
1228
+ for (let i = 0; i < kids2.length; i++) {
1229
+ const el = kids2[i];
1230
+ if (el.nodeType !== 1) continue;
1231
+ const tag = el.tagName || el.localName || "";
1232
+ const localTag = tag.replace(/^[^:]+:/, "");
1233
+ if (localTag === "tbl") {
1234
+ if (tableCtx) tableStack.push(tableCtx);
1235
+ const newTable = { rows: [], currentRow: [], cell: null };
1236
+ walkSection(el, blocks, newTable, tableStack, ctx, d + 1);
1237
+ tableCtx = completeTable(newTable, tableStack, blocks, ctx);
1238
+ } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
1239
+ if (tableCtx?.cell) {
1240
+ const sink = [];
1241
+ handleShape(el, sink, ctx);
1242
+ mergeBlocksIntoCell(tableCtx.cell, sink);
1243
+ } else {
1244
+ handleShape(el, blocks, ctx);
1245
+ }
1246
+ } else if (localTag === "drawText") {
1247
+ if (tableCtx?.cell) {
1248
+ const sink = [];
1249
+ extractDrawTextBlocks(el, sink, ctx);
1250
+ mergeBlocksIntoCell(tableCtx.cell, sink);
1251
+ } else {
1252
+ extractDrawTextBlocks(el, blocks, ctx);
1253
+ }
1254
+ } else if (localTag === "r" || localTag === "run" || localTag === "ctrl" || localTag === "rect" || localTag === "ellipse" || localTag === "polygon" || localTag === "line" || localTag === "arc" || localTag === "curve" || localTag === "connectLine" || localTag === "container") {
1255
+ walkChildren(el, d + 1);
1256
+ }
1124
1257
  }
1125
- const fileData = data.slice(fileStart, fileStart + compSize);
1126
- pos = fileStart + compSize;
1127
- if (!name.toLowerCase().includes("section") || !name.endsWith(".xml")) continue;
1128
- try {
1129
- let content;
1130
- if (method === 0) {
1131
- content = new TextDecoder().decode(fileData);
1132
- } else if (method === 8) {
1133
- const decompressed = inflateRawSync(Buffer.from(fileData), { maxOutputLength: MAX_DECOMPRESS_SIZE });
1134
- content = new TextDecoder().decode(decompressed);
1258
+ };
1259
+ walkChildren(node, depth);
1260
+ return tableCtx;
1261
+ }
1262
+ function findDescendant(node, targetTag, depth = 0) {
1263
+ if (depth > 5) return null;
1264
+ const children = node.childNodes;
1265
+ if (!children) return null;
1266
+ for (let i = 0; i < children.length; i++) {
1267
+ const child = children[i];
1268
+ if (child.nodeType !== 1) continue;
1269
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1270
+ if (tag === targetTag) return child;
1271
+ const found = findDescendant(child, targetTag, depth + 1);
1272
+ if (found) return found;
1273
+ }
1274
+ return null;
1275
+ }
1276
+ function extractDrawTextBlocks(drawTextNode, blocks, ctx) {
1277
+ const children = drawTextNode.childNodes;
1278
+ if (!children) return;
1279
+ for (let i = 0; i < children.length; i++) {
1280
+ const child = children[i];
1281
+ if (child.nodeType !== 1) continue;
1282
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1283
+ if (tag === "subList" || tag === "p" || tag === "para") {
1284
+ if (tag === "subList") {
1285
+ extractDrawTextBlocks(child, blocks, ctx);
1135
1286
  } else {
1136
- continue;
1287
+ const info = extractParagraphInfo(child, ctx.styleMap, ctx);
1288
+ let text = info.text.trim();
1289
+ if (text) {
1290
+ const ph = resolveParaHeading(child, ctx);
1291
+ if (ph?.prefix) text = ph.prefix + " " + text;
1292
+ const block = { type: "paragraph", text, style: info.style ?? void 0, pageNumber: ctx.sectionNum };
1293
+ if (info.href) block.href = info.href;
1294
+ if (info.footnote) block.footnoteText = info.footnote;
1295
+ blocks.push(block);
1296
+ }
1297
+ walkParagraphChildren(child, blocks, null, [], ctx);
1137
1298
  }
1138
- totalDecompressed += content.length * 2;
1139
- if (totalDecompressed > MAX_DECOMPRESS_SIZE) throw new KordocError("\uC555\uCD95 \uD574\uC81C \uD06C\uAE30 \uCD08\uACFC");
1140
- sectionNum++;
1141
- blocks.push(...parseSectionXml(content, void 0, warnings, sectionNum, shared));
1142
- } catch {
1143
- continue;
1144
1299
  }
1145
1300
  }
1146
- 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");
1147
- applyPageText(blocks, shared);
1148
- const markdown = blocksToMarkdown(blocks);
1149
- return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
1150
1301
  }
1151
- async function resolveSectionPaths(zip) {
1152
- const manifestPaths = ["Contents/content.hpf", "content.hpf"];
1153
- for (const mp of manifestPaths) {
1154
- const mpLower = mp.toLowerCase();
1155
- const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
1156
- if (!file) continue;
1157
- const xml = await file.async("text");
1158
- const paths = parseSectionPathsFromManifest(xml);
1159
- if (paths.length > 0) return paths;
1160
- }
1161
- const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
1162
- return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
1163
- }
1164
- function parseSectionPathsFromManifest(xml) {
1165
- const parser = createXmlParser();
1166
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1167
- const items = doc.getElementsByTagName("opf:item");
1168
- const spine = doc.getElementsByTagName("opf:itemref");
1169
- const idToHref = /* @__PURE__ */ new Map();
1170
- for (let i = 0; i < items.length; i++) {
1171
- const item = items[i];
1172
- const id = item.getAttribute("id") || "";
1173
- const href = normalizeSectionHref(item.getAttribute("href") || "");
1174
- if (id && href) idToHref.set(id, href);
1175
- }
1176
- if (spine.length > 0) {
1177
- const ordered = [];
1178
- for (let i = 0; i < spine.length; i++) {
1179
- const href = idToHref.get(spine[i].getAttribute("idref") || "");
1180
- if (href) ordered.push(href);
1181
- }
1182
- if (ordered.length > 0) return ordered;
1302
+ function extractHyperlinkHref(fieldBegin) {
1303
+ if ((fieldBegin.getAttribute("type") || "").toUpperCase() !== "HYPERLINK") return void 0;
1304
+ const params = findChildByLocalName(fieldBegin, "parameters");
1305
+ if (!params) return void 0;
1306
+ const children = params.childNodes;
1307
+ if (!children) return void 0;
1308
+ for (let i = 0; i < children.length; i++) {
1309
+ const ch = children[i];
1310
+ if (ch.nodeType !== 1) continue;
1311
+ const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1312
+ if (tag !== "stringParam" || ch.getAttribute("name") !== "Path") continue;
1313
+ let url = (ch.textContent || "").trim();
1314
+ if (!url) continue;
1315
+ url = url.replace(/^https?:\/\/(?=https?:\/\/)/i, "");
1316
+ const safe = sanitizeHref(url);
1317
+ if (safe) return safe;
1183
1318
  }
1184
- return Array.from(idToHref.values()).sort(compareSectionPaths);
1319
+ return void 0;
1185
1320
  }
1186
- function detectHwpxHeadings(blocks, styleMap) {
1187
- if (blocks.some((b) => b.type === "heading")) return;
1188
- let baseFontSize = 0;
1189
- const sizeFreq = /* @__PURE__ */ new Map();
1190
- for (const b of blocks) {
1191
- if (b.style?.fontSize) {
1192
- sizeFreq.set(b.style.fontSize, (sizeFreq.get(b.style.fontSize) || 0) + 1);
1193
- }
1194
- }
1195
- let maxCount = 0;
1196
- for (const [size, count] of sizeFreq) {
1197
- if (count > maxCount) {
1198
- maxCount = count;
1199
- baseFontSize = size;
1200
- }
1201
- }
1202
- for (const block of blocks) {
1203
- if (block.type !== "paragraph" || !block.text) continue;
1204
- const text = block.text.trim();
1205
- if (text.length === 0 || text.length > 200 || /^\d+$/.test(text)) continue;
1206
- let level = 0;
1207
- if (baseFontSize > 0 && block.style?.fontSize) {
1208
- const ratio = block.style.fontSize / baseFontSize;
1209
- if (ratio >= HEADING_RATIO_H1) level = 1;
1210
- else if (ratio >= HEADING_RATIO_H2) level = 2;
1211
- else if (ratio >= HEADING_RATIO_H3) level = 3;
1212
- }
1213
- const compactText = text.replace(/\s+/g, "");
1214
- if (/^제\d+[조장절편]/.test(compactText) && text.length <= 50) {
1215
- if (level === 0) level = 3;
1216
- }
1217
- if (level > 0) {
1218
- block.type = "heading";
1219
- block.level = level;
1220
- }
1221
- }
1321
+ function isInDeletedRange(ctx) {
1322
+ return (ctx?.shared.track.deleteDepth ?? 0) > 0;
1222
1323
  }
1223
- function buildTableWithCellMeta(state) {
1224
- const table = buildTable(state.rows);
1225
- if (state.caption) table.caption = state.caption;
1226
- const anchors = [];
1227
- {
1228
- const covered = /* @__PURE__ */ new Set();
1229
- for (let r = 0; r < table.rows; r++) {
1230
- for (let c = 0; c < table.cols; c++) {
1231
- if (covered.has(`${r},${c}`)) continue;
1232
- const cell = table.cells[r]?.[c];
1233
- if (!cell) continue;
1234
- for (let dr = 0; dr < cell.rowSpan; dr++) {
1235
- for (let dc = 0; dc < cell.colSpan; dc++) {
1236
- if (dr === 0 && dc === 0) continue;
1237
- if (r + dr < table.rows && c + dc < table.cols) covered.add(`${r + dr},${c + dc}`);
1324
+ function extractParagraphInfo(para, styleMap, ctx) {
1325
+ let text = "";
1326
+ let href;
1327
+ let footnote;
1328
+ let charPrId;
1329
+ const handleCtrl = (ctrlEl) => {
1330
+ const kids2 = ctrlEl.childNodes;
1331
+ if (!kids2) return;
1332
+ for (let j = 0; j < kids2.length; j++) {
1333
+ const k = kids2[j];
1334
+ if (k.nodeType !== 1) continue;
1335
+ const ktag = (k.tagName || k.localName || "").replace(/^[^:]+:/, "");
1336
+ switch (ktag) {
1337
+ // 머리말/꼬리말 문서당 1회 수집, 본문 앞/뒤 배치
1338
+ case "header":
1339
+ case "footer": {
1340
+ if (!ctx) break;
1341
+ const t = collectSubListText(k, ctx);
1342
+ if (t) {
1343
+ const bucket = ktag === "header" ? ctx.shared.pageText.headers : ctx.shared.pageText.footers;
1344
+ if (!bucket.includes(t)) bucket.push(t);
1345
+ }
1346
+ break;
1347
+ }
1348
+ // 각주/미주 — 해당 문단의 footnote로 인라인 보존
1349
+ case "footNote":
1350
+ case "endNote": {
1351
+ const noteText = extractTextFromNode(k);
1352
+ if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
1353
+ break;
1354
+ }
1355
+ // 하이퍼링크 — fieldBegin type=HYPERLINK의 Path 파라미터
1356
+ case "fieldBegin": {
1357
+ const url = extractHyperlinkHref(k);
1358
+ if (url && !href) href = url;
1359
+ break;
1360
+ }
1361
+ case "fieldEnd":
1362
+ break;
1363
+ // 변경추적 — 삭제 구간(deleteBegin~End)의 텍스트는 출력 제외 (최종본 상태 재현)
1364
+ case "deleteBegin":
1365
+ if (ctx) ctx.shared.track.deleteDepth++;
1366
+ break;
1367
+ case "deleteEnd":
1368
+ if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
1369
+ break;
1370
+ case "insertBegin":
1371
+ case "insertEnd":
1372
+ break;
1373
+ // 삽입분은 최종본에 포함
1374
+ // 숨은 설명 — 본문 혼입 차단
1375
+ case "hiddenComment": {
1376
+ if (ctx?.warnings && extractTextFromNode(k)) {
1377
+ ctx.warnings.push({ page: ctx.sectionNum, message: "\uC228\uC740 \uC124\uBA85 \uD14D\uC2A4\uD2B8 \uC81C\uC678: hiddenComment", code: "HIDDEN_TEXT_FILTERED" });
1378
+ }
1379
+ break;
1380
+ }
1381
+ // 콘텐츠 없는 제어 요소 — 스킵
1382
+ case "bookmark":
1383
+ case "pageNum":
1384
+ case "pageNumCtrl":
1385
+ case "pageHiding":
1386
+ case "newNum":
1387
+ case "autoNum":
1388
+ case "indexmark":
1389
+ case "colPr":
1390
+ break;
1391
+ // 미지원 요소 — 텍스트를 가졌으면 무음 손실 대신 경고
1392
+ default: {
1393
+ if (ctx?.warnings && extractTextFromNode(k)) {
1394
+ ctx.warnings.push({ page: ctx.sectionNum, message: `\uBBF8\uC9C0\uC6D0 \uC81C\uC5B4 \uC694\uC18C\uC758 \uD14D\uC2A4\uD2B8 \uC190\uC2E4: ${ktag}`, code: "UNSUPPORTED_ELEMENT" });
1238
1395
  }
1239
1396
  }
1240
- anchors.push(cell);
1241
- c += cell.colSpan - 1;
1242
1397
  }
1243
1398
  }
1244
- }
1245
- const srcCount = state.rows.reduce((s, r) => s + r.length, 0);
1246
- const ordinalReliable = anchors.length === srcCount;
1247
- const claimed = /* @__PURE__ */ new Set();
1248
- let flatIdx = -1;
1249
- for (const row of state.rows) {
1250
- for (const src of row) {
1251
- flatIdx++;
1252
- const needsBlocks = src.hasStructure && src.blocks && src.blocks.length > 0;
1253
- if (!needsBlocks && !src.isHeader) continue;
1254
- let target;
1255
- const trimmed = src.text.trim();
1256
- if (src.rowAddr !== void 0 && src.colAddr !== void 0) {
1257
- const cand = table.cells[src.rowAddr]?.[src.colAddr];
1258
- if (cand && cand.text === trimmed && !claimed.has(cand)) target = cand;
1259
- }
1260
- if (!target) {
1261
- outer: for (const irRow of table.cells) {
1262
- for (const cand of irRow) {
1263
- if (!claimed.has(cand) && cand.text === trimmed && cand.colSpan === src.colSpan && cand.rowSpan === src.rowSpan) {
1264
- target = cand;
1265
- break outer;
1266
- }
1399
+ };
1400
+ const walk = (node) => {
1401
+ const children = node.childNodes;
1402
+ if (!children) return;
1403
+ for (let i = 0; i < children.length; i++) {
1404
+ const child = children[i];
1405
+ if (child.nodeType === 3) {
1406
+ const t = child.textContent || "";
1407
+ if (isInDeletedRange(ctx)) {
1408
+ if (t && ctx && !ctx.shared.track.warned) {
1409
+ ctx.shared.track.warned = true;
1410
+ ctx.warnings?.push({ page: ctx.sectionNum, message: "\uBCC0\uACBD\uCD94\uC801 \uC0AD\uC81C \uD14D\uC2A4\uD2B8 \uCD9C\uB825 \uC81C\uC678", code: "HIDDEN_TEXT_FILTERED" });
1267
1411
  }
1412
+ } else {
1413
+ text += t;
1268
1414
  }
1415
+ continue;
1269
1416
  }
1270
- if (!target && ordinalReliable) {
1271
- const cand = anchors[flatIdx];
1272
- if (cand && !claimed.has(cand)) target = cand;
1273
- }
1274
- if (!target) continue;
1275
- claimed.add(target);
1276
- if (needsBlocks) target.blocks = src.blocks;
1277
- if (src.isHeader) target.isHeader = true;
1278
- }
1279
- }
1280
- return table;
1281
- }
1282
- function completeTable(newTable, tableStack, blocks, ctx) {
1283
- const parentTable = tableStack.length > 0 ? tableStack.pop() : null;
1284
- if (newTable.rows.length === 0) {
1285
- if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
1286
- return parentTable;
1287
- }
1288
- const ir = buildTableWithCellMeta(newTable);
1289
- const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
1290
- if (parentTable?.cell) {
1291
- const cell = parentTable.cell;
1292
- (cell.blocks ??= []).push(block);
1293
- cell.hasStructure = true;
1294
- let flat = convertTableToText(newTable.rows);
1295
- if (newTable.caption) flat = newTable.caption + (flat ? "\n" + flat : "");
1296
- if (flat) cell.text += (cell.text ? "\n" : "") + flat;
1297
- } else {
1298
- blocks.push(block);
1299
- }
1300
- return parentTable;
1301
- }
1302
- function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
1303
- const parser = createXmlParser(warnings);
1304
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1305
- if (!doc.documentElement) return [];
1306
- const ctx = { styleMap, warnings, sectionNum, shared: shared ?? createSectionShared() };
1307
- ctx.shared.track.deleteDepth = 0;
1308
- for (const tagName of ["hp:secPr", "secPr"]) {
1309
- const els = doc.getElementsByTagName(tagName);
1310
- if (els.length > 0) {
1311
- const v = els[0].getAttribute("outlineShapeIDRef");
1312
- if (v) ctx.outlineNumId = v;
1313
- break;
1314
- }
1315
- }
1316
- const blocks = [];
1317
- walkSection(doc.documentElement, blocks, null, [], ctx);
1318
- return blocks;
1319
- }
1320
- function extractImageRef(el) {
1321
- const children = el.childNodes;
1322
- if (!children) return null;
1323
- for (let i = 0; i < children.length; i++) {
1324
- const child = children[i];
1325
- if (child.nodeType !== 1) continue;
1326
- const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1327
- if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
1328
- const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
1329
- if (ref) return ref;
1330
- }
1331
- const nested = extractImageRef(child);
1332
- if (nested) return nested;
1333
- }
1334
- const directRef = el.getAttribute("binaryItemIDRef") || "";
1335
- if (directRef) return directRef;
1336
- return null;
1337
- }
1338
- function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1339
- if (depth > MAX_XML_DEPTH) return;
1340
- const children = node.childNodes;
1341
- if (!children) return;
1342
- for (let i = 0; i < children.length; i++) {
1343
- const el = children[i];
1344
- if (el.nodeType !== 1) continue;
1345
- const tag = el.tagName || el.localName || "";
1346
- const localTag = tag.replace(/^[^:]+:/, "");
1347
- switch (localTag) {
1348
- case "tbl": {
1349
- if (tableCtx) tableStack.push(tableCtx);
1350
- const newTable = { rows: [], currentRow: [], cell: null };
1351
- walkSection(el, blocks, newTable, tableStack, ctx, depth + 1);
1352
- tableCtx = completeTable(newTable, tableStack, blocks, ctx);
1353
- break;
1354
- }
1355
- // 표/도표 캡션 — IRTable.caption으로 보존 (v3.0, 기존 무음 드롭 수정)
1356
- case "caption":
1357
- if (tableCtx) {
1358
- const capText = collectSubListText(el, ctx);
1359
- if (capText) tableCtx.caption = (tableCtx.caption ? tableCtx.caption + "\n" : "") + capText;
1360
- }
1361
- break;
1362
- case "tr":
1363
- if (tableCtx) {
1364
- tableCtx.currentRow = [];
1365
- walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1366
- if (tableCtx.currentRow.length > 0) tableCtx.rows.push(tableCtx.currentRow);
1367
- tableCtx.currentRow = [];
1368
- }
1369
- break;
1370
- case "tc":
1371
- if (tableCtx) {
1372
- tableCtx.cell = { text: "", colSpan: 1, rowSpan: 1 };
1373
- if (el.getAttribute("header") === "1" || el.getAttribute("header") === "true") tableCtx.cell.isHeader = true;
1374
- walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1375
- if (tableCtx.cell) {
1376
- tableCtx.currentRow.push(tableCtx.cell);
1377
- tableCtx.cell = null;
1417
+ if (child.nodeType !== 1) continue;
1418
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1419
+ switch (tag) {
1420
+ case "t":
1421
+ walk(child);
1422
+ break;
1423
+ // 자식 순회 (tab 하위 요소 처리)
1424
+ case "tab": {
1425
+ const leader = child.getAttribute("leader");
1426
+ if (leader && leader !== "0") {
1427
+ text += "";
1428
+ } else {
1429
+ text += " ";
1378
1430
  }
1431
+ break;
1379
1432
  }
1380
- break;
1381
- case "cellAddr":
1382
- if (tableCtx?.cell) {
1383
- const ca = parseInt(el.getAttribute("colAddr") || "", 10);
1384
- const ra = parseInt(el.getAttribute("rowAddr") || "", 10);
1385
- if (!isNaN(ca)) tableCtx.cell.colAddr = ca;
1386
- if (!isNaN(ra)) tableCtx.cell.rowAddr = ra;
1433
+ case "br":
1434
+ if ((child.getAttribute("type") || "line") === "line") text += "\n";
1435
+ break;
1436
+ case "lineBreak":
1437
+ text += "\n";
1438
+ break;
1439
+ // 강제 줄바꿈 ref 추출기·소스맵 스캐너와 동일 모델
1440
+ case "fwSpace":
1441
+ case "hwSpace":
1442
+ text += " ";
1443
+ break;
1444
+ case "tbl":
1445
+ break;
1446
+ // 테이블은 walkSection에서 처리
1447
+ // 하이퍼링크
1448
+ case "hyperlink": {
1449
+ const url = child.getAttribute("url") || child.getAttribute("href") || "";
1450
+ if (url) {
1451
+ const safe = sanitizeHref(url);
1452
+ if (safe) href = safe;
1453
+ }
1454
+ walk(child);
1455
+ break;
1387
1456
  }
1388
- break;
1389
- case "cellSpan":
1390
- if (tableCtx?.cell) {
1391
- const rawCs = parseInt(el.getAttribute("colSpan") || "1", 10);
1392
- const cs = isNaN(rawCs) ? 1 : rawCs;
1393
- const rawRs = parseInt(el.getAttribute("rowSpan") || "1", 10);
1394
- const rs = isNaN(rawRs) ? 1 : rawRs;
1395
- tableCtx.cell.colSpan = clampSpan(cs, MAX_COLS);
1396
- tableCtx.cell.rowSpan = clampSpan(rs, MAX_ROWS);
1457
+ // 각주/미주
1458
+ case "footNote":
1459
+ case "endNote":
1460
+ case "fn":
1461
+ case "en": {
1462
+ const noteText = extractTextFromNode(child);
1463
+ if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
1464
+ break;
1397
1465
  }
1398
- break;
1399
- case "p": {
1400
- const { text: rawText, href, footnote, style } = extractParagraphInfo(el, ctx.styleMap, ctx);
1401
- let text = rawText;
1402
- let headingLevel;
1403
- if (text) {
1404
- const ph = resolveParaHeading(el, ctx);
1405
- if (ph?.prefix) text = ph.prefix + " " + text;
1406
- headingLevel = ph?.headingLevel;
1466
+ // 제어 요소 — 선별 순회 (머리말/꼬리말/각주/하이퍼링크/변경추적, v3.0)
1467
+ case "ctrl":
1468
+ handleCtrl(child);
1469
+ break;
1470
+ // run 직계 fieldBegin (비표준 경로) — 하이퍼링크 URL만 추출
1471
+ case "fieldBegin": {
1472
+ const url = extractHyperlinkHref(child);
1473
+ if (url && !href) href = url;
1474
+ break;
1407
1475
  }
1408
- if (text) {
1409
- if (tableCtx?.cell) {
1410
- const cell = tableCtx.cell;
1411
- if (footnote) text += ` (\uC8FC: ${footnote})`;
1412
- cell.text += (cell.text ? "\n" : "") + text;
1413
- (cell.blocks ??= []).push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
1414
- } else if (!tableCtx) {
1415
- const block = { type: headingLevel ? "heading" : "paragraph", text, pageNumber: ctx.sectionNum };
1416
- if (headingLevel) block.level = headingLevel;
1417
- if (style) block.style = style;
1418
- if (href) block.href = href;
1419
- if (footnote) block.footnoteText = footnote;
1420
- blocks.push(block);
1421
- } else {
1422
- blocks.push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
1476
+ // run 직계 변경추적 마커 (비표준 경로)
1477
+ case "deleteBegin":
1478
+ if (ctx) ctx.shared.track.deleteDepth++;
1479
+ break;
1480
+ case "deleteEnd":
1481
+ if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
1482
+ break;
1483
+ case "insertBegin":
1484
+ case "insertEnd":
1485
+ break;
1486
+ case "fieldEnd":
1487
+ case "parameters":
1488
+ case "stringParam":
1489
+ case "integerParam":
1490
+ case "boolParam":
1491
+ case "floatParam":
1492
+ case "secPr":
1493
+ // 섹션 속성 (페이지 설정 등)
1494
+ case "colPr":
1495
+ // 다단 속성
1496
+ case "linesegarray":
1497
+ case "lineseg":
1498
+ // 레이아웃 정보
1499
+ // 도형/이미지 요소 — 대체텍스트("사각형입니다." 등) 누출 방지 (walkParagraphChildren에서 처리)
1500
+ case "pic":
1501
+ case "shape":
1502
+ case "drawingObject":
1503
+ case "shapeComment":
1504
+ case "drawText":
1505
+ break;
1506
+ // 수식: <hp:equation> 내부의 <hp:script> 에 HULK-style equation
1507
+ // 스크립트가 담겨 있음. hml-equation-parser 로 LaTeX 변환 후 `$...$`
1508
+ // 로 래핑. 실패/빈 스크립트면 무시 (대체 텍스트 누출 방지).
1509
+ case "equation": {
1510
+ const script = findChildByLocalName(child, "script");
1511
+ const raw = script ? extractTextFromNode(script) : "";
1512
+ if (raw.trim()) {
1513
+ try {
1514
+ const latex = hmlToLatex(raw).trim();
1515
+ if (latex) text += " $" + latex + "$ ";
1516
+ } catch {
1517
+ }
1423
1518
  }
1519
+ break;
1424
1520
  }
1425
- tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1426
- break;
1427
- }
1428
- // 이미지/그림/글상자 이미지·텍스트·캡션 병행 추출
1429
- case "pic":
1430
- case "shape":
1431
- case "drawingObject": {
1432
- if (tableCtx?.cell) {
1433
- const sink = [];
1434
- handleShape(el, sink, ctx);
1435
- mergeBlocksIntoCell(tableCtx.cell, sink);
1436
- } else {
1437
- handleShape(el, blocks, ctx);
1438
- }
1439
- break;
1440
- }
1441
- // 메모 — 본문 혼입 차단 (v3.0)
1442
- case "memogroup":
1443
- case "memo": {
1444
- if (ctx.warnings && extractTextFromNode(el)) {
1445
- ctx.warnings.push({ page: ctx.sectionNum, message: "\uBA54\uBAA8 \uD14D\uC2A4\uD2B8 \uBCF8\uBB38 \uC81C\uC678: memogroup", code: "HIDDEN_TEXT_FILTERED" });
1521
+ // run 요소에서 charPrIDRef 추출
1522
+ case "r": {
1523
+ const runCharPr = child.getAttribute("charPrIDRef");
1524
+ if (runCharPr && !charPrId) charPrId = runCharPr;
1525
+ walk(child);
1526
+ break;
1446
1527
  }
1447
- break;
1528
+ default:
1529
+ walk(child);
1530
+ break;
1448
1531
  }
1449
- default:
1450
- walkSection(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1451
- break;
1532
+ }
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;
1452
1551
  }
1453
1552
  }
1553
+ return { text: cleanText, href, footnote, style };
1454
1554
  }
1455
- function handleShape(el, sink, ctx) {
1456
- const imgRef = extractImageRef(el);
1457
- const drawTextChild = findDescendant(el, "drawText");
1458
- if (imgRef) {
1459
- const block = { type: "image", text: imgRef, pageNumber: ctx.sectionNum };
1460
- const alt = userShapeComment(el);
1461
- if (alt) block.footnoteText = alt;
1462
- sink.push(block);
1463
- }
1464
- if (drawTextChild) {
1465
- extractDrawTextBlocks(drawTextChild, sink, ctx);
1466
- }
1467
- const capEl = findChildByLocalName(el, "caption");
1468
- if (capEl) {
1469
- const capText = collectSubListText(capEl, ctx);
1470
- if (capText) sink.push({ type: "paragraph", text: capText, pageNumber: ctx.sectionNum });
1471
- }
1472
- if (!imgRef && !drawTextChild && ctx.warnings && ctx.sectionNum) {
1473
- const localTag = (el.tagName || el.localName || "").replace(/^[^:]+:/, "");
1474
- ctx.warnings.push({ page: ctx.sectionNum, message: `\uC2A4\uD0B5\uB41C \uC694\uC18C: ${localTag}`, code: "SKIPPED_IMAGE" });
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";
1475
1579
  }
1476
1580
  }
1477
- function userShapeComment(el) {
1478
- const commentEl = findChildByLocalName(el, "shapeComment");
1479
- if (!commentEl) return void 0;
1480
- const text = extractTextFromNode(commentEl);
1481
- if (!text) return void 0;
1482
- if (/^그림입니다/.test(text)) return void 0;
1483
- if (/^(?:모서리가 둥근 |둥근 )?[^\n]{1,20}입니다\.?$/.test(text)) return void 0;
1484
- return text;
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";
1485
1591
  }
1486
- function mergeBlocksIntoCell(cell, sink) {
1487
- for (const b of sink) {
1488
- if ((b.type === "paragraph" || b.type === "heading") && b.text) {
1489
- cell.text += (cell.text ? "\n" : "") + b.text;
1490
- (cell.blocks ??= []).push(b);
1491
- } else if (b.type === "image" || b.type === "table") {
1492
- if (b.type === "image" && b.text) {
1493
- cell.text += (cell.text ? "\n" : "") + `![image](${b.text})`;
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
+ }
1494
1602
  }
1495
- ;
1496
- (cell.blocks ??= []).push(b);
1497
- cell.hasStructure = true;
1498
- }
1499
- }
1500
- }
1501
- function collectSubListText(el, ctx, depth = 0) {
1502
- if (depth > 10) return "";
1503
- const parts = [];
1504
- const children = el.childNodes;
1505
- if (!children) return "";
1506
- for (let i = 0; i < children.length; i++) {
1507
- const ch = children[i];
1508
- if (ch.nodeType !== 1) continue;
1509
- const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1510
- if (tag === "p" || tag === "para") {
1511
- const t = extractParagraphInfo(ch, ctx.styleMap, ctx).text;
1512
- if (t) parts.push(t);
1513
- } else if (tag === "tbl") {
1514
- continue;
1515
- } else {
1516
- const t = collectSubListText(ch, ctx, depth + 1);
1517
- if (t) parts.push(t);
1518
1603
  }
1519
1604
  }
1520
- return parts.join("\n").trim();
1521
1605
  }
1522
- function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1523
- if (depth > MAX_XML_DEPTH) return tableCtx;
1524
- const children = node.childNodes;
1525
- if (!children) return tableCtx;
1526
- const walkChildren = (parent, d) => {
1527
- if (d > MAX_XML_DEPTH) return;
1528
- const kids2 = parent.childNodes;
1529
- if (!kids2) return;
1530
- for (let i = 0; i < kids2.length; i++) {
1531
- const el = kids2[i];
1532
- if (el.nodeType !== 1) continue;
1533
- const tag = el.tagName || el.localName || "";
1534
- const localTag = tag.replace(/^[^:]+:/, "");
1535
- if (localTag === "tbl") {
1536
- if (tableCtx) tableStack.push(tableCtx);
1537
- const newTable = { rows: [], currentRow: [], cell: null };
1538
- walkSection(el, blocks, newTable, tableStack, ctx, d + 1);
1539
- tableCtx = completeTable(newTable, tableStack, blocks, ctx);
1540
- } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
1541
- if (tableCtx?.cell) {
1542
- const sink = [];
1543
- handleShape(el, sink, ctx);
1544
- mergeBlocksIntoCell(tableCtx.cell, sink);
1545
- } else {
1546
- handleShape(el, blocks, ctx);
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
+ }
1547
1633
  }
1548
- } else if (localTag === "drawText") {
1549
- if (tableCtx?.cell) {
1550
- const sink = [];
1551
- extractDrawTextBlocks(el, sink, ctx);
1552
- mergeBlocksIntoCell(tableCtx.cell, sink);
1553
- } else {
1554
- extractDrawTextBlocks(el, blocks, ctx);
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;
1555
1653
  }
1556
- } else if (localTag === "r" || localTag === "run" || localTag === "ctrl" || localTag === "rect" || localTag === "ellipse" || localTag === "polygon" || localTag === "line" || localTag === "arc" || localTag === "curve" || localTag === "connectLine" || localTag === "container") {
1557
- walkChildren(el, d + 1);
1558
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);
1559
1657
  }
1560
- };
1561
- walkChildren(node, depth);
1562
- return tableCtx;
1563
- }
1564
- function findDescendant(node, targetTag, depth = 0) {
1565
- if (depth > 5) return null;
1566
- const children = node.childNodes;
1567
- if (!children) return null;
1568
- for (let i = 0; i < children.length; i++) {
1569
- const child = children[i];
1570
- if (child.nodeType !== 1) continue;
1571
- const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1572
- if (tag === targetTag) return child;
1573
- const found = findDescendant(child, targetTag, depth + 1);
1574
- if (found) return found;
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})`);
1575
1667
  }
1576
- return null;
1668
+ return images;
1577
1669
  }
1578
- function extractDrawTextBlocks(drawTextNode, blocks, ctx) {
1579
- const children = drawTextNode.childNodes;
1580
- if (!children) return;
1581
- for (let i = 0; i < children.length; i++) {
1582
- const child = children[i];
1583
- if (child.nodeType !== 1) continue;
1584
- const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1585
- if (tag === "subList" || tag === "p" || tag === "para") {
1586
- if (tag === "subList") {
1587
- extractDrawTextBlocks(child, blocks, ctx);
1588
- } else {
1589
- const info = extractParagraphInfo(child, ctx.styleMap, ctx);
1590
- let text = info.text.trim();
1591
- if (text) {
1592
- const ph = resolveParaHeading(child, ctx);
1593
- if (ph?.prefix) text = ph.prefix + " " + text;
1594
- const block = { type: "paragraph", text, style: info.style ?? void 0, pageNumber: ctx.sectionNum };
1595
- if (info.href) block.href = info.href;
1596
- if (info.footnote) block.footnoteText = info.footnote;
1597
- blocks.push(block);
1598
- }
1599
- walkParagraphChildren(child, blocks, null, [], ctx);
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++;
1600
1694
  }
1695
+ continue;
1601
1696
  }
1602
- }
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 };
1603
1743
  }
1604
- function extractHyperlinkHref(fieldBegin) {
1605
- if ((fieldBegin.getAttribute("type") || "").toUpperCase() !== "HYPERLINK") return void 0;
1606
- const params = findChildByLocalName(fieldBegin, "parameters");
1607
- if (!params) return void 0;
1608
- const children = params.childNodes;
1609
- if (!children) return void 0;
1610
- for (let i = 0; i < children.length; i++) {
1611
- const ch = children[i];
1612
- if (ch.nodeType !== 1) continue;
1613
- const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1614
- if (tag !== "stringParam" || ch.getAttribute("name") !== "Path") continue;
1615
- let url = (ch.textContent || "").trim();
1616
- if (!url) continue;
1617
- url = url.replace(/^https?:\/\/(?=https?:\/\/)/i, "");
1618
- const safe = sanitizeHref(url);
1619
- if (safe) return safe;
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;
1620
1753
  }
1621
- return void 0;
1754
+ const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/);
1755
+ return sectionFiles.map((f) => f.name).sort(compareSectionPaths);
1622
1756
  }
1623
- function isInDeletedRange(ctx) {
1624
- return (ctx?.shared.track.deleteDepth ?? 0) > 0;
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);
1774
+ }
1775
+ if (ordered.length > 0) return ordered;
1776
+ }
1777
+ return Array.from(idToHref.values()).sort(compareSectionPaths);
1625
1778
  }
1626
- function extractParagraphInfo(para, styleMap, ctx) {
1627
- let text = "";
1628
- let href;
1629
- let footnote;
1630
- let charPrId;
1631
- const handleCtrl = (ctrlEl) => {
1632
- const kids2 = ctrlEl.childNodes;
1633
- if (!kids2) return;
1634
- for (let j = 0; j < kids2.length; j++) {
1635
- const k = kids2[j];
1636
- if (k.nodeType !== 1) continue;
1637
- const ktag = (k.tagName || k.localName || "").replace(/^[^:]+:/, "");
1638
- switch (ktag) {
1639
- // 머리말/꼬리말 — 문서당 1회 수집, 본문 앞/뒤 배치
1640
- case "header":
1641
- case "footer": {
1642
- if (!ctx) break;
1643
- const t = collectSubListText(k, ctx);
1644
- if (t) {
1645
- const bucket = ktag === "header" ? ctx.shared.pageText.headers : ctx.shared.pageText.footers;
1646
- if (!bucket.includes(t)) bucket.push(t);
1647
- }
1648
- break;
1649
- }
1650
- // 각주/미주 — 해당 문단의 footnote로 인라인 보존
1651
- case "footNote":
1652
- case "endNote": {
1653
- const noteText = extractTextFromNode(k);
1654
- if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
1655
- break;
1656
- }
1657
- // 하이퍼링크 — fieldBegin type=HYPERLINK의 Path 파라미터
1658
- case "fieldBegin": {
1659
- const url = extractHyperlinkHref(k);
1660
- if (url && !href) href = url;
1661
- break;
1662
- }
1663
- case "fieldEnd":
1664
- break;
1665
- // 변경추적 — 삭제 구간(deleteBegin~End)의 텍스트는 출력 제외 (최종본 상태 재현)
1666
- case "deleteBegin":
1667
- if (ctx) ctx.shared.track.deleteDepth++;
1668
- break;
1669
- case "deleteEnd":
1670
- if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
1671
- break;
1672
- case "insertBegin":
1673
- case "insertEnd":
1674
- break;
1675
- // 삽입분은 최종본에 포함
1676
- // 숨은 설명 — 본문 혼입 차단
1677
- case "hiddenComment": {
1678
- if (ctx?.warnings && extractTextFromNode(k)) {
1679
- ctx.warnings.push({ page: ctx.sectionNum, message: "\uC228\uC740 \uC124\uBA85 \uD14D\uC2A4\uD2B8 \uC81C\uC678: hiddenComment", code: "HIDDEN_TEXT_FILTERED" });
1680
- }
1681
- break;
1682
- }
1683
- // 콘텐츠 없는 제어 요소 — 스킵
1684
- case "bookmark":
1685
- case "pageNum":
1686
- case "pageNumCtrl":
1687
- case "pageHiding":
1688
- case "newNum":
1689
- case "autoNum":
1690
- case "indexmark":
1691
- case "colPr":
1692
- break;
1693
- // 미지원 요소 — 텍스트를 가졌으면 무음 손실 대신 경고
1694
- default: {
1695
- if (ctx?.warnings && extractTextFromNode(k)) {
1696
- ctx.warnings.push({ page: ctx.sectionNum, message: `\uBBF8\uC9C0\uC6D0 \uC81C\uC5B4 \uC694\uC18C\uC758 \uD14D\uC2A4\uD2B8 \uC190\uC2E4: ${ktag}`, code: "UNSUPPORTED_ELEMENT" });
1697
- }
1698
- }
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)");
1699
1791
  }
1792
+ parseDublinCoreMetadata(xml, metadata);
1793
+ if (metadata.title || metadata.author) return;
1700
1794
  }
1701
- };
1702
- const walk = (node) => {
1703
- const children = node.childNodes;
1704
- if (!children) return;
1705
- for (let i = 0; i < children.length; i++) {
1706
- const child = children[i];
1707
- if (child.nodeType === 3) {
1708
- const t = child.textContent || "";
1709
- if (isInDeletedRange(ctx)) {
1710
- if (t && ctx && !ctx.shared.track.warned) {
1711
- ctx.shared.track.warned = true;
1712
- ctx.warnings?.push({ page: ctx.sectionNum, message: "\uBCC0\uACBD\uCD94\uC801 \uC0AD\uC81C \uD14D\uC2A4\uD2B8 \uCD9C\uB825 \uC81C\uC678", code: "HIDDEN_TEXT_FILTERED" });
1713
- }
1714
- } else {
1715
- text += t;
1716
- }
1717
- continue;
1718
- }
1719
- if (child.nodeType !== 1) continue;
1720
- const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1721
- switch (tag) {
1722
- case "t":
1723
- walk(child);
1724
- break;
1725
- // 자식 순회 (tab 등 하위 요소 처리)
1726
- case "tab": {
1727
- const leader = child.getAttribute("leader");
1728
- if (leader && leader !== "0") {
1729
- text += "";
1730
- } else {
1731
- text += " ";
1732
- }
1733
- break;
1734
- }
1735
- case "br":
1736
- if ((child.getAttribute("type") || "line") === "line") text += "\n";
1737
- break;
1738
- case "lineBreak":
1739
- text += "\n";
1740
- break;
1741
- // 강제 줄바꿈 — ref 추출기·소스맵 스캐너와 동일 모델
1742
- case "fwSpace":
1743
- case "hwSpace":
1744
- text += " ";
1745
- break;
1746
- case "tbl":
1747
- break;
1748
- // 테이블은 walkSection에서 처리
1749
- // 하이퍼링크
1750
- case "hyperlink": {
1751
- const url = child.getAttribute("url") || child.getAttribute("href") || "";
1752
- if (url) {
1753
- const safe = sanitizeHref(url);
1754
- if (safe) href = safe;
1755
- }
1756
- walk(child);
1757
- break;
1758
- }
1759
- // 각주/미주
1760
- case "footNote":
1761
- case "endNote":
1762
- case "fn":
1763
- case "en": {
1764
- const noteText = extractTextFromNode(child);
1765
- if (noteText) footnote = (footnote ? footnote + "; " : "") + noteText;
1766
- break;
1767
- }
1768
- // 제어 요소 — 선별 순회 (머리말/꼬리말/각주/하이퍼링크/변경추적, v3.0)
1769
- case "ctrl":
1770
- handleCtrl(child);
1771
- break;
1772
- // run 직계 fieldBegin (비표준 경로) — 하이퍼링크 URL만 추출
1773
- case "fieldBegin": {
1774
- const url = extractHyperlinkHref(child);
1775
- if (url && !href) href = url;
1776
- break;
1777
- }
1778
- // run 직계 변경추적 마커 (비표준 경로)
1779
- case "deleteBegin":
1780
- if (ctx) ctx.shared.track.deleteDepth++;
1781
- break;
1782
- case "deleteEnd":
1783
- if (ctx && ctx.shared.track.deleteDepth > 0) ctx.shared.track.deleteDepth--;
1784
- break;
1785
- case "insertBegin":
1786
- case "insertEnd":
1787
- break;
1788
- case "fieldEnd":
1789
- case "parameters":
1790
- case "stringParam":
1791
- case "integerParam":
1792
- case "boolParam":
1793
- case "floatParam":
1794
- case "secPr":
1795
- // 섹션 속성 (페이지 설정 등)
1796
- case "colPr":
1797
- // 다단 속성
1798
- case "linesegarray":
1799
- case "lineseg":
1800
- // 레이아웃 정보
1801
- // 도형/이미지 요소 — 대체텍스트("사각형입니다." 등) 누출 방지 (walkParagraphChildren에서 처리)
1802
- case "pic":
1803
- case "shape":
1804
- case "drawingObject":
1805
- case "shapeComment":
1806
- case "drawText":
1807
- break;
1808
- // 수식: <hp:equation> 내부의 <hp:script> 에 HULK-style equation
1809
- // 스크립트가 담겨 있음. hml-equation-parser 로 LaTeX 변환 후 `$...$`
1810
- // 로 래핑. 실패/빈 스크립트면 무시 (대체 텍스트 누출 방지).
1811
- case "equation": {
1812
- const script = findChildByLocalName(child, "script");
1813
- const raw = script ? extractTextFromNode(script) : "";
1814
- if (raw.trim()) {
1815
- try {
1816
- const latex = hmlToLatex(raw).trim();
1817
- if (latex) text += " $" + latex + "$ ";
1818
- } catch {
1819
- }
1820
- }
1821
- break;
1822
- }
1823
- // run 요소에서 charPrIDRef 추출
1824
- case "r": {
1825
- const runCharPr = child.getAttribute("charPrIDRef");
1826
- if (runCharPr && !charPrId) charPrId = runCharPr;
1827
- walk(child);
1828
- break;
1829
- }
1830
- default:
1831
- walk(child);
1832
- break;
1795
+ } catch {
1796
+ }
1797
+ }
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;
1833
1808
  }
1834
1809
  }
1810
+ return void 0;
1835
1811
  };
1836
- walk(para);
1837
- const leaderIdx = text.indexOf("");
1838
- if (leaderIdx >= 0) text = text.substring(0, leaderIdx);
1839
- let cleanText = text.replace(/[ \t]+/g, " ").trim();
1840
- if (/^그림입니다\.?\s*원본\s*그림의\s*(이름|크기)/.test(cleanText)) cleanText = "";
1841
- cleanText = cleanText.replace(/그림입니다\.?\s*원본\s*그림의\s*(이름|크기)[^\n]*(\n[^\n]*원본\s*그림의\s*(이름|크기)[^\n]*)*/g, "").trim();
1842
- cleanText = cleanText.replace(/(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|선|직선|곡선|화살표|오각형|육각형|팔각형|별|십자|구름|마름모|도넛|평행사변형|사다리꼴|개체|그리기\s?개체|묶음\s?개체|글상자|표|그림|OLE\s?개체)\s?입니다\.?/g, "").trim();
1843
- let style;
1844
- if (styleMap && charPrId) {
1845
- const charProp = styleMap.charProperties.get(charPrId);
1846
- if (charProp) {
1847
- style = {};
1848
- if (charProp.fontSize) style.fontSize = charProp.fontSize;
1849
- if (charProp.bold) style.bold = true;
1850
- if (charProp.italic) style.italic = true;
1851
- if (charProp.fontName) style.fontName = charProp.fontName;
1852
- if (!style.fontSize && !style.bold && !style.italic) style = void 0;
1853
- }
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);
1854
1820
  }
1855
- return { text: cleanText, href, footnote, style };
1856
1821
  }
1857
- function findChildByLocalName(parent, name) {
1858
- const children = parent.childNodes;
1859
- if (!children) return null;
1860
- for (let i = 0; i < children.length; i++) {
1861
- const ch = children[i];
1862
- if (ch.nodeType !== 1) continue;
1863
- const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1864
- if (tag === name) return ch;
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");
1865
1828
  }
1866
- return null;
1829
+ const metadata = {};
1830
+ await extractHwpxMetadata(zip, metadata);
1831
+ const sectionPaths = await resolveSectionPaths(zip);
1832
+ metadata.pageCount = sectionPaths.length;
1833
+ return metadata;
1867
1834
  }
1868
- function extractTextFromNode(node) {
1869
- let result = "";
1870
- const children = node.childNodes;
1871
- if (!children) return result;
1872
- for (let i = 0; i < children.length; i++) {
1873
- const child = children[i];
1874
- if (child.nodeType === 3) result += child.textContent || "";
1875
- else if (child.nodeType === 1) result += extractTextFromNode(child);
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);
1876
1844
  }
1877
- return result.trim();
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 };
1878
1897
  }
1879
1898
 
1880
1899
  // src/hwp5/record.ts
@@ -16530,7 +16549,7 @@ function isDistributionSentinel(markdown) {
16530
16549
  }
16531
16550
 
16532
16551
  // src/xlsx/parser.ts
16533
- import JSZip2 from "jszip";
16552
+ import JSZip3 from "jszip";
16534
16553
  import { DOMParser as DOMParser2 } from "@xmldom/xmldom";
16535
16554
  var MAX_SHEETS = 100;
16536
16555
  var MAX_DECOMPRESS_SIZE3 = 100 * 1024 * 1024;
@@ -16721,7 +16740,7 @@ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex) {
16721
16740
  }
16722
16741
  async function parseXlsxDocument(buffer, options) {
16723
16742
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE3);
16724
- const zip = await JSZip2.loadAsync(buffer);
16743
+ const zip = await JSZip3.loadAsync(buffer);
16725
16744
  const warnings = [];
16726
16745
  const workbookFile = zip.file("xl/workbook.xml");
16727
16746
  if (!workbookFile) {
@@ -17427,7 +17446,7 @@ async function parseXlsDocument(buffer, options) {
17427
17446
  }
17428
17447
 
17429
17448
  // src/docx/parser.ts
17430
- import JSZip3 from "jszip";
17449
+ import JSZip4 from "jszip";
17431
17450
  import { DOMParser as DOMParser3 } from "@xmldom/xmldom";
17432
17451
 
17433
17452
  // src/docx/equation.ts
@@ -18181,7 +18200,7 @@ async function extractImages(zip, rels, doc, warnings) {
18181
18200
  }
18182
18201
  async function parseDocxDocument(buffer, options) {
18183
18202
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
18184
- const zip = await JSZip3.loadAsync(buffer);
18203
+ const zip = await JSZip4.loadAsync(buffer);
18185
18204
  const warnings = [];
18186
18205
  const docFile = zip.file("word/document.xml");
18187
18206
  if (!docFile) {
@@ -19099,7 +19118,7 @@ function fillInlineFields(text, values, filled, matchedLabels) {
19099
19118
  }
19100
19119
 
19101
19120
  // src/form/filler-hwpx.ts
19102
- import JSZip4 from "jszip";
19121
+ import JSZip5 from "jszip";
19103
19122
 
19104
19123
  // src/roundtrip/source-map.ts
19105
19124
  function escapeXmlText(text) {
@@ -19773,7 +19792,7 @@ function patchZipEntries(original, replacements) {
19773
19792
  // src/form/filler-hwpx.ts
19774
19793
  async function fillHwpx(hwpxBuffer, values) {
19775
19794
  const u8 = new Uint8Array(hwpxBuffer);
19776
- const zip = await JSZip4.loadAsync(hwpxBuffer);
19795
+ const zip = await JSZip5.loadAsync(hwpxBuffer);
19777
19796
  const sectionPaths = Object.keys(zip.files).filter((name) => /[Ss]ection\d+\.xml$/i.test(name)).sort();
19778
19797
  if (sectionPaths.length === 0) {
19779
19798
  throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -20014,7 +20033,7 @@ async function fillHwpx(hwpxBuffer, values) {
20014
20033
  }
20015
20034
 
20016
20035
  // src/hwpx/generator.ts
20017
- import JSZip5 from "jszip";
20036
+ import JSZip6 from "jszip";
20018
20037
 
20019
20038
  // src/hwpx/text-metrics.ts
20020
20039
  var ASCII_W = [
@@ -20385,53 +20404,112 @@ function mmToHwpunit(mm) {
20385
20404
  return Math.round(mm * 7200 / 25.4);
20386
20405
  }
20387
20406
 
20388
- // src/diff/text-diff.ts
20389
- function similarity(a, b) {
20390
- if (a === b) return 1;
20391
- if (!a || !b) return 0;
20392
- const maxLen = Math.max(a.length, b.length);
20393
- if (maxLen === 0) return 1;
20394
- return 1 - levenshtein(a, b) / maxLen;
20407
+ // src/hwpx/gen-ids.ts
20408
+ var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
20409
+ var NS_PARA = "http://www.hancom.co.kr/hwpml/2011/paragraph";
20410
+ var NS_HEAD = "http://www.hancom.co.kr/hwpml/2011/head";
20411
+ var NS_CORE = "http://www.hancom.co.kr/hwpml/2011/core";
20412
+ var NS_OPF = "http://www.idpf.org/2007/opf/";
20413
+ var NS_HPF = "http://www.hancom.co.kr/schema/2011/hpf";
20414
+ var NS_OCF = "urn:oasis:names:tc:opendocument:xmlns:container";
20415
+ var CHAR_NORMAL = 0;
20416
+ var CHAR_BOLD = 1;
20417
+ var CHAR_ITALIC = 2;
20418
+ var CHAR_BOLD_ITALIC = 3;
20419
+ var CHAR_CODE = 4;
20420
+ var CHAR_H1 = 5;
20421
+ var CHAR_H2 = 6;
20422
+ var CHAR_H3 = 7;
20423
+ var CHAR_H4 = 8;
20424
+ var CHAR_TABLE_HEADER = 9;
20425
+ var CHAR_QUOTE = 10;
20426
+ var PARA_NORMAL = 0;
20427
+ var PARA_H1 = 1;
20428
+ var PARA_H2 = 2;
20429
+ var PARA_H3 = 3;
20430
+ var PARA_H4 = 4;
20431
+ var PARA_CODE = 5;
20432
+ var PARA_QUOTE = 6;
20433
+ var PARA_LIST = 7;
20434
+ var DEFAULT_TEXT_COLOR = "#000000";
20435
+ function resolveTheme(theme) {
20436
+ return {
20437
+ h1: theme?.headingColors?.[1] ?? DEFAULT_TEXT_COLOR,
20438
+ h2: theme?.headingColors?.[2] ?? DEFAULT_TEXT_COLOR,
20439
+ h3: theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
20440
+ h4: theme?.headingColors?.[4] ?? theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
20441
+ body: theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
20442
+ quote: theme?.quoteColor ?? DEFAULT_TEXT_COLOR,
20443
+ /** quoteColor가 명시되었는지 — blockquote charPr 분기에 사용 (baseline 호환) */
20444
+ hasQuoteOption: theme?.quoteColor !== void 0,
20445
+ tableHeader: theme?.tableHeaderColor ?? theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
20446
+ tableHeaderBold: !!theme?.tableHeaderBold
20447
+ };
20395
20448
  }
20396
- function normalizedSimilarity(a, b) {
20397
- return similarity(normalize(a), normalize(b));
20449
+ function escapeXml(text) {
20450
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20398
20451
  }
20399
- function normalize(s) {
20400
- return s.replace(/\s+/g, " ").trim();
20452
+ function headingParaPrId(level) {
20453
+ if (level === 1) return PARA_H1;
20454
+ if (level === 2) return PARA_H2;
20455
+ if (level === 3) return PARA_H3;
20456
+ return PARA_H4;
20401
20457
  }
20402
- var MAX_LEVENSHTEIN_LEN = 1e4;
20403
- function levenshtein(a, b) {
20404
- if (a.length + b.length > MAX_LEVENSHTEIN_LEN) {
20405
- const sampleLen = Math.min(500, a.length, b.length);
20406
- let diffs = 0;
20407
- for (let i = 0; i < sampleLen; i++) if (a[i] !== b[i]) diffs++;
20408
- const sampleRate = sampleLen > 0 ? diffs / sampleLen : 1;
20409
- return Math.abs(a.length - b.length) + Math.round(Math.min(a.length, b.length) * sampleRate);
20410
- }
20411
- if (a.length > b.length) [a, b] = [b, a];
20412
- const m = a.length;
20413
- const n = b.length;
20414
- let prev = Array.from({ length: m + 1 }, (_, i) => i);
20415
- let curr = new Array(m + 1);
20416
- for (let j = 1; j <= n; j++) {
20417
- curr[0] = j;
20418
- for (let i = 1; i <= m; i++) {
20419
- if (a[i - 1] === b[j - 1]) {
20420
- curr[i] = prev[i - 1];
20421
- } else {
20422
- curr[i] = 1 + Math.min(prev[i - 1], prev[i], curr[i - 1]);
20423
- }
20424
- }
20425
- ;
20426
- [prev, curr] = [curr, prev];
20458
+ function headingCharPrId(level) {
20459
+ if (level === 1) return CHAR_H1;
20460
+ if (level === 2) return CHAR_H2;
20461
+ if (level === 3) return CHAR_H3;
20462
+ return CHAR_H4;
20463
+ }
20464
+ function charPr(id, height, bold, italic, fontId = 0, textColor = DEFAULT_TEXT_COLOR, ratioPct = 100) {
20465
+ const boldAttr = bold ? ` bold="1"` : "";
20466
+ const italicAttr = italic ? ` italic="1"` : "";
20467
+ const effFont = bold ? 2 : fontId;
20468
+ return ` <hh:charPr id="${id}" height="${height}" textColor="${textColor}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
20469
+ <hh:fontRef hangul="${effFont}" latin="${effFont}" hanja="${effFont}" japanese="${effFont}" other="${effFont}" symbol="${effFont}" user="${effFont}"/>
20470
+ <hh:ratio hangul="${ratioPct}" latin="${ratioPct}" hanja="${ratioPct}" japanese="100" other="100" symbol="100" user="100"/>
20471
+ <hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
20472
+ <hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
20473
+ <hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
20474
+ </hh:charPr>`;
20475
+ }
20476
+ function paraPr(id, opts = {}) {
20477
+ const { align = "JUSTIFY", spaceBefore = 0, spaceAfter = 0, lineSpacing = 160, indent = 0, left = 0, keepWord = false } = opts;
20478
+ const breakNonLatin = keepWord ? "KEEP_WORD" : "BREAK_WORD";
20479
+ const snapGrid = keepWord ? "0" : "1";
20480
+ return ` <hh:paraPr id="${id}" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="${snapGrid}" suppressLineNumbers="0" checked="0" textDir="AUTO">
20481
+ <hh:align horizontal="${align}" vertical="BASELINE"/>
20482
+ <hh:heading type="NONE" idRef="0" level="0"/>
20483
+ <hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="${breakNonLatin}" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>
20484
+ <hh:autoSpacing eAsianEng="0" eAsianNum="0"/>
20485
+ <hh:margin><hc:intent value="${indent}" unit="HWPUNIT"/><hc:left value="${left}" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="${spaceBefore}" unit="HWPUNIT"/><hc:next value="${spaceAfter}" unit="HWPUNIT"/></hh:margin>
20486
+ <hh:lineSpacing type="PERCENT" value="${lineSpacing}"/>
20487
+ <hh:border borderFillIDRef="1" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>
20488
+ </hh:paraPr>`;
20489
+ }
20490
+ var GONGMUN_LIST_BASE = 8;
20491
+ var GONGMUN_LIST_LEVELS = 8;
20492
+ var GONGMUN_CENTER = GONGMUN_LIST_BASE + GONGMUN_LIST_LEVELS;
20493
+ var CHAR_VARIANT_BASE = 11;
20494
+ var GONGMUN_BODY_RATIO = 95;
20495
+
20496
+ // src/hwpx/md-runs.ts
20497
+ function buildPrvText(blocks) {
20498
+ const lines = [];
20499
+ let bytes = 0;
20500
+ for (const b of blocks) {
20501
+ let text = b.text || (b.rows ? b.rows.map((r) => r.join(" ")).join("\n") : "");
20502
+ if (b.type === "html_table") text = text.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
20503
+ if (!text) continue;
20504
+ lines.push(text);
20505
+ bytes += text.length * 3;
20506
+ if (bytes > 1024) break;
20427
20507
  }
20428
- return prev[m];
20508
+ return lines.join("\n").slice(0, 1024);
20429
20509
  }
20430
-
20431
- // src/roundtrip/markdown-units.ts
20432
- function splitMarkdownUnits(md2) {
20510
+ function parseMarkdownToBlocks(md2) {
20433
20511
  const lines = md2.split("\n");
20434
- const units = [];
20512
+ const blocks = [];
20435
20513
  let i = 0;
20436
20514
  while (i < lines.length) {
20437
20515
  const line = lines[i];
@@ -20439,442 +20517,451 @@ function splitMarkdownUnits(md2) {
20439
20517
  i++;
20440
20518
  continue;
20441
20519
  }
20442
- if (line.trim().startsWith("<table>")) {
20443
- const collected2 = [];
20520
+ const fenceMatch = line.match(/^(`{3,}|~{3,})(.*)$/);
20521
+ if (fenceMatch) {
20522
+ const fence = fenceMatch[1];
20523
+ const lang = fenceMatch[2].trim();
20524
+ const codeLines = [];
20525
+ i++;
20526
+ while (i < lines.length && !lines[i].startsWith(fence)) {
20527
+ codeLines.push(lines[i]);
20528
+ i++;
20529
+ }
20530
+ if (i < lines.length) i++;
20531
+ blocks.push({ type: "code_block", text: codeLines.join("\n"), lang });
20532
+ continue;
20533
+ }
20534
+ if (/^(\*{3,}|-{3,}|_{3,})\s*$/.test(line.trim())) {
20535
+ blocks.push({ type: "hr" });
20536
+ i++;
20537
+ continue;
20538
+ }
20539
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
20540
+ if (headingMatch) {
20541
+ blocks.push({ type: "heading", text: headingMatch[2].trim(), level: headingMatch[1].length });
20542
+ i++;
20543
+ continue;
20544
+ }
20545
+ if (/^<table[\s>]/i.test(line.trimStart())) {
20546
+ const htmlLines = [];
20444
20547
  let depth = 0;
20445
20548
  while (i < lines.length) {
20446
20549
  const l = lines[i];
20447
- collected2.push(l);
20448
- depth += (l.match(/<table>/g) || []).length;
20449
- depth -= (l.match(/<\/table>/g) || []).length;
20550
+ htmlLines.push(l);
20551
+ depth += (l.match(/<table[\s>]/gi) ?? []).length;
20552
+ depth -= (l.match(/<\/table>/gi) ?? []).length;
20450
20553
  i++;
20451
20554
  if (depth <= 0) break;
20452
20555
  }
20453
- units.push({ kind: "html-table", raw: collected2.join("\n"), lines: collected2 });
20556
+ blocks.push({ type: "html_table", text: htmlLines.join("\n") });
20454
20557
  continue;
20455
20558
  }
20456
20559
  if (line.trimStart().startsWith("|")) {
20457
- const collected2 = [];
20560
+ const tableRows = [];
20458
20561
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
20459
- collected2.push(lines[i]);
20562
+ const row = lines[i];
20563
+ if (/^[\s|:\-]+$/.test(row)) {
20564
+ i++;
20565
+ continue;
20566
+ }
20567
+ const cells = row.split("|").slice(1, -1).map((c) => c.trim());
20568
+ if (cells.length > 0) tableRows.push(cells);
20460
20569
  i++;
20461
20570
  }
20462
- units.push({ kind: "gfm-table", raw: collected2.join("\n"), lines: collected2 });
20571
+ if (tableRows.length > 0) blocks.push({ type: "table", rows: tableRows });
20463
20572
  continue;
20464
20573
  }
20465
- if (/^-{3,}\s*$/.test(line.trim())) {
20466
- units.push({ kind: "separator", raw: line.trim(), lines: [line.trim()] });
20467
- i++;
20574
+ if (line.trimStart().startsWith("> ")) {
20575
+ const quoteLines = [];
20576
+ while (i < lines.length && (lines[i].trimStart().startsWith("> ") || lines[i].trimStart().startsWith(">"))) {
20577
+ quoteLines.push(lines[i].replace(/^>\s?/, ""));
20578
+ i++;
20579
+ }
20580
+ for (const ql of quoteLines) {
20581
+ blocks.push({ type: "blockquote", text: ql.trim() || "" });
20582
+ }
20468
20583
  continue;
20469
20584
  }
20470
- if (/^!\[image\]\([^)]*\)\s*$/.test(line.trim())) {
20471
- units.push({ kind: "image", raw: line.trim(), lines: [line.trim()] });
20585
+ const listMatch = line.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
20586
+ if (listMatch) {
20587
+ const indent = Math.floor(listMatch[1].length / 2);
20588
+ const ordered = /\d/.test(listMatch[2]);
20589
+ blocks.push({ type: "list_item", text: listMatch[3].trim(), ordered, indent });
20472
20590
  i++;
20473
20591
  continue;
20474
20592
  }
20475
- const collected = [];
20476
- while (i < lines.length && lines[i].trim() && !lines[i].trimStart().startsWith("|") && !lines[i].trim().startsWith("<table>")) {
20477
- collected.push(lines[i].trim());
20478
- i++;
20479
- }
20480
- units.push({ kind: "text", raw: collected.join("\n"), lines: collected });
20593
+ blocks.push({ type: "paragraph", text: line.trim() });
20594
+ i++;
20481
20595
  }
20482
- return units;
20596
+ return blocks;
20483
20597
  }
20484
- function alignUnits(a, b) {
20485
- const m = a.length, n = b.length;
20486
- if (m * n > 4e6) {
20487
- const result2 = [];
20488
- let pre = 0;
20489
- while (pre < m && pre < n && a[pre] === b[pre]) {
20490
- result2.push([pre, pre]);
20491
- pre++;
20598
+ function parseInlineMarkdown(text) {
20599
+ text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
20600
+ text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_, t, u) => t || u);
20601
+ text = text.replace(/~~([^~]+)~~/g, "$1");
20602
+ const spans = [];
20603
+ const regex = /(`[^`]+`|\*{3}[^*]+\*{3}|\*{2}[^*]+\*{2}|\*[^*]+\*|_{2}[^_]+_{2}|_[^_]+_)/g;
20604
+ let lastIdx = 0;
20605
+ for (const match of text.matchAll(regex)) {
20606
+ const idx = match.index;
20607
+ if (idx > lastIdx) {
20608
+ spans.push({ text: text.slice(lastIdx, idx), bold: false, italic: false, code: false });
20492
20609
  }
20493
- let suf = 0;
20494
- while (suf < m - pre && suf < n - pre && a[m - 1 - suf] === b[n - 1 - suf]) suf++;
20495
- const aMid = m - pre - suf, bMid = n - pre - suf;
20496
- if (aMid === bMid) {
20497
- for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, pre + i2]);
20610
+ const raw = match[0];
20611
+ if (raw.startsWith("`")) {
20612
+ spans.push({ text: raw.slice(1, -1), bold: false, italic: false, code: true });
20613
+ } else if (raw.startsWith("***") || raw.startsWith("___")) {
20614
+ spans.push({ text: raw.slice(3, -3), bold: true, italic: true, code: false });
20615
+ } else if (raw.startsWith("**") || raw.startsWith("__")) {
20616
+ spans.push({ text: raw.slice(2, -2), bold: true, italic: false, code: false });
20498
20617
  } else {
20499
- for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, null]);
20500
- for (let j2 = 0; j2 < bMid; j2++) result2.push([null, pre + j2]);
20501
- }
20502
- for (let s = suf - 1; s >= 0; s--) result2.push([m - 1 - s, n - 1 - s]);
20503
- return result2;
20504
- }
20505
- const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1));
20506
- for (let i2 = 1; i2 <= m; i2++) {
20507
- for (let j2 = 1; j2 <= n; j2++) {
20508
- dp[i2][j2] = a[i2 - 1] === b[j2 - 1] ? dp[i2 - 1][j2 - 1] + 1 : Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
20509
- }
20510
- }
20511
- const matches = [];
20512
- let i = m, j = n;
20513
- while (i > 0 && j > 0) {
20514
- if (a[i - 1] === b[j - 1] && dp[i][j] === dp[i - 1][j - 1] + 1) {
20515
- matches.push([i - 1, j - 1]);
20516
- i--;
20517
- j--;
20518
- } else if (dp[i - 1][j] >= dp[i][j - 1]) i--;
20519
- else j--;
20520
- }
20521
- matches.reverse();
20522
- const result = [];
20523
- let ai = 0, bi = 0;
20524
- const flushGap = (aEnd, bEnd) => {
20525
- if (aEnd - ai === bEnd - bi) {
20526
- while (ai < aEnd) result.push([ai++, bi++]);
20527
- return;
20528
- }
20529
- while (ai < aEnd && bi < bEnd) {
20530
- const sim = normalizedSimilarity(a[ai], b[bi]);
20531
- if (sim >= 0.4) {
20532
- if (aEnd - ai > bEnd - bi && bestSimInRange(a, ai + 1, ai + (aEnd - ai) - (bEnd - bi), b[bi]) > sim) {
20533
- result.push([ai++, null]);
20534
- } else if (bEnd - bi > aEnd - ai && bestSimInRange(b, bi + 1, bi + (bEnd - bi) - (aEnd - ai), a[ai]) > sim) {
20535
- result.push([null, bi++]);
20536
- } else {
20537
- result.push([ai++, bi++]);
20538
- }
20539
- } else if (aEnd - ai >= bEnd - bi) result.push([ai++, null]);
20540
- else result.push([null, bi++]);
20541
- }
20542
- while (ai < aEnd) result.push([ai++, null]);
20543
- while (bi < bEnd) result.push([null, bi++]);
20544
- };
20545
- for (const [pi, pj] of matches) {
20546
- flushGap(pi, pj);
20547
- result.push([ai++, bi++]);
20548
- }
20549
- flushGap(m, n);
20550
- return result;
20551
- }
20552
- function bestSimInRange(arr, from, to, target) {
20553
- let best = 0;
20554
- for (let k = from; k <= to && k < arr.length; k++) {
20555
- const s = normalizedSimilarity(arr[k], target);
20556
- if (s > best) best = s;
20557
- }
20558
- return best;
20559
- }
20560
- function escapeGfm(text) {
20561
- return text.replace(/~/g, "\\~");
20562
- }
20563
- var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
20564
- function sanitizeText(text) {
20565
- let result = mapPuaText(text).replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
20566
- if (result.length <= 30 && result.includes(" ")) {
20567
- const tokens = result.split(" ");
20568
- const koreanSingleCharCount = tokens.filter((t) => t.length === 1 && /[가-힯ㄱ-ㆎ]/.test(t)).length;
20569
- if (tokens.length >= 3 && koreanSingleCharCount / tokens.length >= 0.7) {
20570
- result = tokens.join("");
20618
+ spans.push({ text: raw.slice(1, -1), bold: false, italic: true, code: false });
20571
20619
  }
20620
+ lastIdx = idx + raw.length;
20572
20621
  }
20573
- return result;
20574
- }
20575
- function normForMatch(text) {
20576
- return sanitizeText(text).replace(/\s+/g, " ").trim();
20577
- }
20578
- function unescapeGfm(text) {
20579
- return text.replace(/\\~/g, "~");
20580
- }
20581
- function summarize(text) {
20582
- const t = text.replace(/\s+/g, " ").trim();
20583
- return t.length > 80 ? t.slice(0, 77) + "..." : t;
20584
- }
20585
- function replicateGfmTable(table) {
20586
- const { cells, rows: numRows, cols: numCols } = table;
20587
- if (numRows === 0 || numCols === 0) return null;
20588
- if (numRows === 1 && numCols === 1) return null;
20589
- if (numCols === 1) return null;
20590
- const display = Array.from({ length: numRows }, (_, r) => Array.from({ length: numCols }, (_2, c) => ({ text: "", gridR: r, gridC: c })));
20591
- const skip = /* @__PURE__ */ new Set();
20592
- for (let r = 0; r < numRows; r++) {
20593
- for (let c = 0; c < numCols; c++) {
20594
- if (skip.has(`${r},${c}`)) continue;
20595
- const cell = cells[r]?.[c];
20596
- if (!cell) continue;
20597
- display[r][c] = {
20598
- text: escapeGfm(sanitizeText(cell.text)).replace(/\|/g, "\\|").replace(/\n/g, "<br>"),
20599
- gridR: r,
20600
- gridC: c
20601
- };
20602
- for (let dr = 0; dr < cell.rowSpan; dr++) {
20603
- for (let dc = 0; dc < cell.colSpan; dc++) {
20604
- if (dr === 0 && dc === 0) continue;
20605
- if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
20606
- }
20607
- }
20608
- c += cell.colSpan - 1;
20609
- }
20622
+ if (lastIdx < text.length) {
20623
+ spans.push({ text: text.slice(lastIdx), bold: false, italic: false, code: false });
20610
20624
  }
20611
- const uniqueRows = [];
20612
- let pendingLabelRow = null;
20613
- for (let r = 0; r < display.length; r++) {
20614
- const row = display[r];
20615
- if (row.every((cell) => cell.text === "")) continue;
20616
- const nonEmptyCols = row.filter((cell) => cell.text !== "");
20617
- const hasSkipInRow = row.some((_, c) => skip.has(`${r},${c}`));
20618
- if (!hasSkipInRow && nonEmptyCols.length === 1 && row[0].text !== "" && row.slice(1).every((c) => c.text === "")) {
20619
- if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
20620
- pendingLabelRow = row;
20621
- continue;
20622
- }
20623
- if (pendingLabelRow) {
20624
- if (row[0].text === "") row[0] = pendingLabelRow[0];
20625
- else uniqueRows.push(pendingLabelRow);
20626
- pendingLabelRow = null;
20627
- }
20628
- uniqueRows.push(row);
20625
+ if (spans.length === 0) {
20626
+ spans.push({ text, bold: false, italic: false, code: false });
20629
20627
  }
20630
- if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
20631
- return uniqueRows.length > 0 ? uniqueRows : null;
20628
+ return spans;
20632
20629
  }
20633
- function parseGfmTable(lines) {
20634
- const rows = [];
20635
- for (const line of lines) {
20636
- const trimmed = line.trim();
20637
- if (!trimmed.startsWith("|")) continue;
20638
- const cells = trimmed.split(/(?<!\\)\|/).slice(1, -1).map((c) => c.trim());
20639
- if (cells.length === 0) continue;
20640
- if (cells.every((c) => /^:?-{3,}:?$/.test(c))) continue;
20641
- rows.push(cells);
20642
- }
20643
- return rows;
20630
+ function spanToCharPrId(span) {
20631
+ if (span.code) return CHAR_CODE;
20632
+ if (span.bold && span.italic) return CHAR_BOLD_ITALIC;
20633
+ if (span.bold) return CHAR_BOLD;
20634
+ if (span.italic) return CHAR_ITALIC;
20635
+ return CHAR_NORMAL;
20644
20636
  }
20645
- function unescapeGfmCell(text) {
20646
- return text.replace(/<br\s*\/?>/gi, "\n").replace(/\\\|/g, "|").replace(/\\~/g, "~");
20637
+ function generateRuns(text, defaultCharPr = CHAR_NORMAL, mapCharId) {
20638
+ const spans = parseInlineMarkdown(text);
20639
+ return spans.map((span) => {
20640
+ let charId = span.code || span.bold || span.italic ? spanToCharPrId(span) : defaultCharPr;
20641
+ if (mapCharId) charId = mapCharId(charId);
20642
+ return `<hp:run charPrIDRef="${charId}"><hp:t>${escapeXml(span.text)}</hp:t></hp:run>`;
20643
+ }).join("");
20647
20644
  }
20648
- function replicateCellInnerHtml(cell) {
20649
- if (cell.blocks?.length) {
20650
- return cell.blocks.map((b) => {
20651
- if (b.type === "table" && b.table) {
20652
- const cap = b.table.caption ? sanitizeText(b.table.caption) : "";
20653
- return (cap ? cap + "<br>" : "") + replicateTableToHtml(b.table);
20654
- }
20655
- if (b.type === "image" && b.text) return `<img src="${b.text}" alt="image">`;
20656
- const t = sanitizeText(b.text ?? "");
20657
- return t ? t.replace(/\n/g, "<br>") : "";
20658
- }).filter(Boolean).join("<br>");
20645
+ function generateParagraph(text, paraPrId = PARA_NORMAL, charPrId = CHAR_NORMAL, mapCharId) {
20646
+ if (paraPrId === PARA_CODE) {
20647
+ return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_CODE}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`;
20659
20648
  }
20660
- return sanitizeText(cell.text).replace(/\n/g, "<br>");
20649
+ const runs = generateRuns(text, charPrId, mapCharId);
20650
+ return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0">${runs}</hp:p>`;
20661
20651
  }
20662
- function replicateTableToHtml(table) {
20663
- const rows = replicateHtmlTable(table);
20664
- const lines = ["<table>"];
20665
- for (let r = 0; r < rows.length; r++) {
20666
- const tag = rows[r].tag;
20667
- const rowHtml = rows[r].cells.map((cell) => {
20668
- const attrs = [];
20669
- if (cell.colSpan > 1) attrs.push(`colspan="${cell.colSpan}"`);
20670
- if (cell.rowSpan > 1) attrs.push(`rowspan="${cell.rowSpan}"`);
20671
- const attrStr = attrs.length ? " " + attrs.join(" ") : "";
20672
- return `<${tag}${attrStr}>${cell.inner}</${tag}>`;
20673
- });
20674
- if (rowHtml.length) lines.push(`<tr>${rowHtml.join("")}</tr>`);
20652
+
20653
+ // src/hwpx/gen-header.ts
20654
+ function generateContainerXml() {
20655
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
20656
+ <ocf:container xmlns:ocf="${NS_OCF}" xmlns:hpf="${NS_HPF}">
20657
+ <ocf:rootfiles>
20658
+ <ocf:rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/>
20659
+ </ocf:rootfiles>
20660
+ </ocf:container>`;
20661
+ }
20662
+ function generateManifest() {
20663
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
20664
+ <opf:package xmlns:opf="${NS_OPF}" xmlns:hpf="${NS_HPF}" xmlns:hh="${NS_HEAD}">
20665
+ <opf:manifest>
20666
+ <opf:item id="header" href="Contents/header.xml" media-type="application/xml"/>
20667
+ <opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/>
20668
+ </opf:manifest>
20669
+ <opf:spine>
20670
+ <opf:itemref idref="header" linear="no"/>
20671
+ <opf:itemref idref="section0" linear="yes"/>
20672
+ </opf:spine>
20673
+ </opf:package>`;
20674
+ }
20675
+ function buildCharProperties(theme, gongmun, ratioVariants = []) {
20676
+ let body = 1e3, code = 900, h1 = 1800, h2 = 1400, h3 = 1200, h4 = 1100;
20677
+ if (gongmun) {
20678
+ body = gongmun.bodyHeight;
20679
+ code = Math.max(body - 200, 900);
20680
+ h1 = gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700;
20681
+ h2 = 1600;
20682
+ h3 = body;
20683
+ h4 = Math.max(body - 100, 1300);
20675
20684
  }
20676
- lines.push("</table>");
20677
- return lines.join("\n");
20685
+ const bodyRatio = gongmun ? GONGMUN_BODY_RATIO : 100;
20686
+ const rows = [
20687
+ charPr(0, body, false, false, 0, theme.body, bodyRatio),
20688
+ charPr(1, body, true, false, 0, theme.body, bodyRatio),
20689
+ charPr(2, body, false, true, 0, theme.body, bodyRatio),
20690
+ charPr(3, body, true, true, 0, theme.body, bodyRatio),
20691
+ charPr(4, code, false, false, 1),
20692
+ charPr(5, h1, true, false, 1, theme.h1),
20693
+ charPr(6, h2, true, false, 1, theme.h2),
20694
+ charPr(7, h3, true, false, 1, theme.h3),
20695
+ charPr(8, h4, true, false, 1, theme.h4),
20696
+ charPr(CHAR_TABLE_HEADER, body, theme.tableHeaderBold, false, 0, theme.tableHeader),
20697
+ charPr(CHAR_QUOTE, body, false, true, 0, theme.quote)
20698
+ ];
20699
+ for (const r of ratioVariants) {
20700
+ rows.push(
20701
+ charPr(rows.length, body, false, false, 0, theme.body, r),
20702
+ charPr(rows.length + 1, body, true, false, 0, theme.body, r),
20703
+ charPr(rows.length + 2, body, false, true, 0, theme.body, r),
20704
+ charPr(rows.length + 3, body, true, true, 0, theme.body, r)
20705
+ );
20706
+ }
20707
+ return `<hh:charProperties itemCnt="${rows.length}">
20708
+ ${rows.join("\n")}
20709
+ </hh:charProperties>`;
20678
20710
  }
20679
- function replicateHtmlTable(table) {
20680
- const { cells, rows: numRows, cols: numCols } = table;
20681
- const skip = /* @__PURE__ */ new Set();
20682
- const result = [];
20683
- for (let r = 0; r < numRows; r++) {
20684
- const tag = r === 0 ? "th" : "td";
20685
- const rowCells = [];
20686
- for (let c = 0; c < numCols; c++) {
20687
- if (skip.has(`${r},${c}`)) continue;
20688
- const cell = cells[r]?.[c];
20689
- if (!cell) continue;
20690
- for (let dr = 0; dr < cell.rowSpan; dr++) {
20691
- for (let dc = 0; dc < cell.colSpan; dc++) {
20692
- if (dr === 0 && dc === 0) continue;
20693
- if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
20694
- }
20695
- }
20696
- rowCells.push({
20697
- inner: replicateCellInnerHtml(cell),
20698
- colSpan: cell.colSpan,
20699
- rowSpan: cell.rowSpan,
20700
- gridR: r,
20701
- gridC: c
20702
- });
20703
- }
20704
- if (rowCells.length) result.push({ tag, cells: rowCells });
20711
+ function buildParaProperties(gongmun) {
20712
+ if (!gongmun) {
20713
+ const base2 = [
20714
+ paraPr(0),
20715
+ paraPr(1, { align: "LEFT", spaceBefore: 800, spaceAfter: 200, lineSpacing: 180 }),
20716
+ paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: 170 }),
20717
+ paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: 160 }),
20718
+ paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: 160 }),
20719
+ paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400 }),
20720
+ paraPr(6, { align: "LEFT", lineSpacing: 150, indent: 600 }),
20721
+ paraPr(7, { align: "LEFT", lineSpacing: 160, indent: 600 })
20722
+ ];
20723
+ return `<hh:paraProperties itemCnt="${base2.length}">
20724
+ ${base2.join("\n")}
20725
+ </hh:paraProperties>`;
20705
20726
  }
20706
- return result;
20727
+ const ls = gongmun.lineSpacing;
20728
+ const titleAlign = gongmun.centerTitle ? "CENTER" : "LEFT";
20729
+ const base = [
20730
+ paraPr(0, { lineSpacing: ls, keepWord: true }),
20731
+ paraPr(1, { align: titleAlign, spaceBefore: 400, spaceAfter: 400, lineSpacing: ls, keepWord: true }),
20732
+ paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: ls, keepWord: true }),
20733
+ paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
20734
+ paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
20735
+ paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400, keepWord: true }),
20736
+ paraPr(6, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true }),
20737
+ paraPr(7, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true })
20738
+ ];
20739
+ for (let d = 0; d < GONGMUN_LIST_LEVELS; d++) {
20740
+ const { left, indent } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering);
20741
+ const sectionGap = gongmun.numbering === "report" && d === 0 ? Math.round(gongmun.bodyHeight * 0.5) : 0;
20742
+ base.push(paraPr(GONGMUN_LIST_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent, spaceBefore: sectionGap, keepWord: true }));
20743
+ }
20744
+ base.push(paraPr(GONGMUN_CENTER, { align: "CENTER", lineSpacing: ls, keepWord: true }));
20745
+ return `<hh:paraProperties itemCnt="${base.length}">
20746
+ ${base.join("\n")}
20747
+ </hh:paraProperties>`;
20748
+ }
20749
+ function generateHeaderXml(theme, gongmun, ratioVariants = []) {
20750
+ const bodyFace = gongmun?.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD568\uCD08\uB86C\uBC14\uD0D5";
20751
+ const charPropsXml = buildCharProperties(theme, gongmun, ratioVariants);
20752
+ const paraPropsXml = buildParaProperties(gongmun);
20753
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
20754
+ <hh:head xmlns:hh="${NS_HEAD}" xmlns:hp="${NS_PARA}" xmlns:hc="${NS_CORE}" version="1.4" secCnt="1">
20755
+ <hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
20756
+ <hh:refList>
20757
+ <hh:fontfaces itemCnt="7">
20758
+ <hh:fontface lang="HANGUL" fontCnt="3">
20759
+ <hh:font id="0" face="${bodyFace}" type="TTF" isEmbedded="0">
20760
+ <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20761
+ </hh:font>
20762
+ <hh:font id="1" face="\uD568\uCD08\uB86C\uB3CB\uC6C0" type="TTF" isEmbedded="0">
20763
+ <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20764
+ </hh:font>
20765
+ <hh:font id="2" face="HY\uACAC\uACE0\uB515" type="TTF" isEmbedded="0">
20766
+ <hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20767
+ </hh:font>
20768
+ </hh:fontface>
20769
+ <hh:fontface lang="LATIN" fontCnt="3">
20770
+ <hh:font id="0" face="Times New Roman" type="TTF" isEmbedded="0">
20771
+ <hh:typeInfo familyType="FCAT_OLDSTYLE" weight="5" proportion="4" contrast="2" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="4"/>
20772
+ </hh:font>
20773
+ <hh:font id="1" face="Consolas" type="TTF" isEmbedded="0">
20774
+ <hh:typeInfo familyType="FCAT_MODERN" weight="5" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
20775
+ </hh:font>
20776
+ <hh:font id="2" face="Arial Black" type="TTF" isEmbedded="0">
20777
+ <hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
20778
+ </hh:font>
20779
+ </hh:fontface>
20780
+ <hh:fontface lang="HANJA" fontCnt="1">
20781
+ <hh:font id="0" face="\uD568\uCD08\uB86C\uBC14\uD0D5" type="TTF" isEmbedded="0">
20782
+ <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20783
+ </hh:font>
20784
+ </hh:fontface>
20785
+ <hh:fontface lang="JAPANESE" fontCnt="1">
20786
+ <hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
20787
+ <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20788
+ </hh:font>
20789
+ </hh:fontface>
20790
+ <hh:fontface lang="OTHER" fontCnt="1">
20791
+ <hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
20792
+ <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20793
+ </hh:font>
20794
+ </hh:fontface>
20795
+ <hh:fontface lang="SYMBOL" fontCnt="1">
20796
+ <hh:font id="0" face="Symbol" type="TTF" isEmbedded="0">
20797
+ <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20798
+ </hh:font>
20799
+ </hh:fontface>
20800
+ <hh:fontface lang="USER" fontCnt="1">
20801
+ <hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
20802
+ <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20803
+ </hh:font>
20804
+ </hh:fontface>
20805
+ </hh:fontfaces>
20806
+ <hh:borderFills itemCnt="2">
20807
+ <hh:borderFill id="1" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
20808
+ <hh:slash type="NONE" Crooked="0" isCounter="0"/>
20809
+ <hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
20810
+ <hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>
20811
+ <hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>
20812
+ <hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>
20813
+ <hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>
20814
+ </hh:borderFill>
20815
+ <hh:borderFill id="2" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
20816
+ <hh:slash type="NONE" Crooked="0" isCounter="0"/>
20817
+ <hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
20818
+ <hh:leftBorder type="SOLID" width="0.12 mm" color="#000000"/>
20819
+ <hh:rightBorder type="SOLID" width="0.12 mm" color="#000000"/>
20820
+ <hh:topBorder type="SOLID" width="0.12 mm" color="#000000"/>
20821
+ <hh:bottomBorder type="SOLID" width="0.12 mm" color="#000000"/>
20822
+ </hh:borderFill>
20823
+ </hh:borderFills>
20824
+ ${charPropsXml}
20825
+ <hh:tabProperties itemCnt="0"/>
20826
+ <hh:numberings itemCnt="0"/>
20827
+ <hh:bullets itemCnt="0"/>
20828
+ ${paraPropsXml}
20829
+ <hh:styles itemCnt="1">
20830
+ <hh:style id="0" type="PARA" name="\uBC14\uD0D5\uAE00" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0" langIDRef="1042" lockForm="0"/>
20831
+ </hh:styles>
20832
+ </hh:refList>
20833
+ <hh:compatibleDocument targetProgram="HWP2018"><hh:layoutCompatibility/></hh:compatibleDocument>
20834
+ </hh:head>`;
20707
20835
  }
20708
- function parseHtmlTable(raw) {
20709
- const re = /<(\/?)(table|tr|td|th)((?:"[^"]*"|'[^']*'|[^>"'])*?)>/gi;
20710
- let depth = 0;
20711
- let currentRow = null;
20712
- let cellStart = -1;
20713
- let cellInfo = null;
20714
- const rows = [];
20715
- let m;
20716
- while ((m = re.exec(raw)) !== null) {
20717
- const isClose = m[1] === "/";
20718
- const tag = m[2].toLowerCase();
20719
- const attrs = m[3] || "";
20720
- if (tag === "table") {
20721
- depth += isClose ? -1 : 1;
20722
- if (depth < 0) return null;
20723
- continue;
20724
- }
20725
- if (depth !== 1) continue;
20726
- if (tag === "tr") {
20727
- if (!isClose) currentRow = [];
20728
- else if (currentRow) {
20729
- rows.push({ tag: rows.length === 0 ? "th" : "td", cells: currentRow });
20730
- currentRow = null;
20731
- }
20836
+
20837
+ // src/hwpx/gen-gongmun-fit.ts
20838
+ function plainRenderText(text) {
20839
+ return parseInlineMarkdown(text).map((s) => s.text).join("");
20840
+ }
20841
+ function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
20842
+ const minRatio = gongmun.autoFitMinRatio;
20843
+ if (minRatio === null || minRatio >= GONGMUN_BODY_RATIO) return null;
20844
+ const pageW = 59528 - mmToHwpunit(gongmun.margins.left) - mmToHwpunit(gongmun.margins.right);
20845
+ const ratioByBlock = /* @__PURE__ */ new Map();
20846
+ const variants = [];
20847
+ for (let i = 0; i < blocks.length; i++) {
20848
+ const block = blocks[i];
20849
+ let text;
20850
+ let firstW;
20851
+ let contW;
20852
+ if (block.type === "list_item" && gongmunList.has(i)) {
20853
+ const { marker, depth } = gongmunList.get(i);
20854
+ const content = plainRenderText(block.text || "");
20855
+ text = marker ? `${marker} ${content}` : content;
20856
+ const { left, indent } = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering);
20857
+ firstW = pageW - left - Math.max(indent, 0);
20858
+ contW = pageW - left - Math.max(-indent, 0);
20859
+ } else if (block.type === "paragraph") {
20860
+ const raw = (block.text || "").trim();
20861
+ if (/^<center>[\s\S]*<\/center>$/i.test(raw)) continue;
20862
+ text = plainRenderText(raw);
20863
+ firstW = contW = pageW;
20732
20864
  } else {
20733
- if (!isClose) {
20734
- const cs = parseInt(attrs.match(/colspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
20735
- const rs = parseInt(attrs.match(/rowspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
20736
- cellStart = m.index + m[0].length;
20737
- cellInfo = { colSpan: isNaN(cs) ? 1 : cs, rowSpan: isNaN(rs) ? 1 : rs };
20738
- } else if (cellStart >= 0 && cellInfo && currentRow) {
20739
- currentRow.push({ inner: raw.slice(cellStart, m.index), colSpan: cellInfo.colSpan, rowSpan: cellInfo.rowSpan });
20740
- cellStart = -1;
20741
- cellInfo = null;
20742
- }
20865
+ continue;
20743
20866
  }
20867
+ if (!text) continue;
20868
+ const r = fitRatioForFewerLines(text, firstW, contW, gongmun.bodyHeight, GONGMUN_BODY_RATIO, minRatio);
20869
+ if (r === null) continue;
20870
+ ratioByBlock.set(i, r);
20871
+ if (!variants.includes(r)) variants.push(r);
20744
20872
  }
20745
- if (depth !== 0) return null;
20746
- return rows;
20873
+ return ratioByBlock.size > 0 ? { ratioByBlock, variants } : null;
20747
20874
  }
20748
- var AUTONUM_PREFIX_RE = /^(?:[0-90-9a-zA-Z가-힣]{1,6}[.)\]:]|[([][0-90-9a-zA-Z가-힣]{1,6}[)\]][.:]?|[ⅰ-ⅹⅠ-Ⅹ①-⑮][.)\]:]?)$/u;
20749
- function htmlCellInnerToLines(inner) {
20750
- let hadNonText = false;
20751
- let work = inner;
20752
- if (/<table[\s>]/i.test(work)) {
20753
- hadNonText = true;
20754
- work = removeNestedTables(work);
20755
- }
20756
- if (/<img\s/i.test(work)) {
20757
- hadNonText = true;
20758
- work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, "");
20759
- }
20760
- const lines = work.split(/<br\s*\/?>/gi).map((s) => s.trim()).filter((s) => s.length > 0);
20761
- return { lines, hadNonText };
20875
+ function variantMapper(fit, blockIdx) {
20876
+ const r = fit.ratioByBlock.get(blockIdx);
20877
+ if (r === void 0) return void 0;
20878
+ const vi = fit.variants.indexOf(r);
20879
+ return (id) => id >= 0 && id <= 3 ? CHAR_VARIANT_BASE + vi * 4 + id : id;
20762
20880
  }
20763
- function extractTopLevelTables(html) {
20764
- const result = [];
20765
- let depth = 0;
20766
- let start = -1;
20767
- const re = /<(\/?)table(?:[\s>]|>)/gi;
20768
- let m;
20769
- while ((m = re.exec(html)) !== null) {
20770
- if (m[1] !== "/") {
20771
- if (depth === 0) start = m.index;
20772
- depth++;
20773
- } else {
20774
- depth--;
20775
- if (depth === 0 && start >= 0) {
20776
- result.push(html.slice(start, m.index + m[0].length));
20777
- start = -1;
20778
- }
20779
- if (depth < 0) depth = 0;
20881
+ function precomputeGongmunList(blocks, gongmun) {
20882
+ const result = /* @__PURE__ */ new Map();
20883
+ let i = 0;
20884
+ while (i < blocks.length) {
20885
+ if (blocks[i].type !== "list_item") {
20886
+ i++;
20887
+ continue;
20780
20888
  }
20781
- }
20782
- return result;
20783
- }
20784
- function removeNestedTables(html) {
20785
- let result = "";
20786
- let depth = 0;
20787
- const re = /<(\/?)table(?:[\s>]|>)/gi;
20788
- let last = 0;
20789
- let m;
20790
- while ((m = re.exec(html)) !== null) {
20791
- if (m[1] !== "/") {
20792
- if (depth === 0) result += html.slice(last, m.index);
20793
- depth++;
20794
- } else {
20795
- depth--;
20796
- if (depth === 0) last = m.index + m[0].length;
20797
- if (depth < 0) depth = 0;
20889
+ const run = [];
20890
+ while (i < blocks.length) {
20891
+ const t = blocks[i].type;
20892
+ if (t === "list_item") {
20893
+ run.push(i);
20894
+ i++;
20895
+ continue;
20896
+ }
20897
+ if (t === "table" || t === "html_table") {
20898
+ let j = i + 1;
20899
+ while (j < blocks.length && (blocks[j].type === "table" || blocks[j].type === "html_table")) j++;
20900
+ if (j < blocks.length && blocks[j].type === "list_item") {
20901
+ i = j;
20902
+ continue;
20903
+ }
20904
+ }
20905
+ break;
20798
20906
  }
20907
+ const depths = run.map((bi) => Math.min(Math.max(blocks[bi].indent || 0, 0), GONGMUN_LIST_LEVELS - 1));
20908
+ const suppress = gongmun.numbering === "standard" ? computeSuppression(depths) : depths.map(() => false);
20909
+ const numberer = new GongmunNumberer(gongmun.numbering);
20910
+ run.forEach((bi, k) => {
20911
+ const marker = numberer.next(depths[k], suppress[k]);
20912
+ result.set(bi, { marker, depth: depths[k] });
20913
+ });
20799
20914
  }
20800
- if (depth === 0) result += html.slice(last);
20801
20915
  return result;
20802
20916
  }
20803
20917
 
20804
- // src/hwpx/generator.ts
20805
- var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
20806
- var NS_PARA = "http://www.hancom.co.kr/hwpml/2011/paragraph";
20807
- var NS_HEAD = "http://www.hancom.co.kr/hwpml/2011/head";
20808
- var NS_CORE = "http://www.hancom.co.kr/hwpml/2011/core";
20809
- var NS_OPF = "http://www.idpf.org/2007/opf/";
20810
- var NS_HPF = "http://www.hancom.co.kr/schema/2011/hpf";
20811
- var NS_OCF = "urn:oasis:names:tc:opendocument:xmlns:container";
20812
- var CHAR_NORMAL = 0;
20813
- var CHAR_BOLD = 1;
20814
- var CHAR_ITALIC = 2;
20815
- var CHAR_BOLD_ITALIC = 3;
20816
- var CHAR_CODE = 4;
20817
- var CHAR_H1 = 5;
20818
- var CHAR_H2 = 6;
20819
- var CHAR_H3 = 7;
20820
- var CHAR_H4 = 8;
20821
- var CHAR_TABLE_HEADER = 9;
20822
- var CHAR_QUOTE = 10;
20823
- var PARA_NORMAL = 0;
20824
- var PARA_H1 = 1;
20825
- var PARA_H2 = 2;
20826
- var PARA_H3 = 3;
20827
- var PARA_H4 = 4;
20828
- var PARA_CODE = 5;
20829
- var PARA_QUOTE = 6;
20830
- var PARA_LIST = 7;
20831
- var DEFAULT_TEXT_COLOR = "#000000";
20832
- function resolveTheme(theme) {
20833
- return {
20834
- h1: theme?.headingColors?.[1] ?? DEFAULT_TEXT_COLOR,
20835
- h2: theme?.headingColors?.[2] ?? DEFAULT_TEXT_COLOR,
20836
- h3: theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
20837
- h4: theme?.headingColors?.[4] ?? theme?.headingColors?.[3] ?? DEFAULT_TEXT_COLOR,
20838
- body: theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
20839
- quote: theme?.quoteColor ?? DEFAULT_TEXT_COLOR,
20840
- /** quoteColor가 명시되었는지 — blockquote charPr 분기에 사용 (baseline 호환) */
20841
- hasQuoteOption: theme?.quoteColor !== void 0,
20842
- tableHeader: theme?.tableHeaderColor ?? theme?.bodyColor ?? DEFAULT_TEXT_COLOR,
20843
- tableHeaderBold: !!theme?.tableHeaderBold
20844
- };
20918
+ // src/diff/text-diff.ts
20919
+ function similarity(a, b) {
20920
+ if (a === b) return 1;
20921
+ if (!a || !b) return 0;
20922
+ const maxLen = Math.max(a.length, b.length);
20923
+ if (maxLen === 0) return 1;
20924
+ return 1 - levenshtein(a, b) / maxLen;
20845
20925
  }
20846
- async function markdownToHwpx(markdown, options) {
20847
- const theme = resolveTheme(options?.theme);
20848
- const gongmun = options?.gongmun ? resolveGongmun(options.gongmun) : null;
20849
- const blocks = parseMarkdownToBlocks(markdown);
20850
- const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
20851
- const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
20852
- const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit);
20853
- const zip = new JSZip5();
20854
- zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
20855
- zip.file("META-INF/container.xml", generateContainerXml());
20856
- zip.file("Contents/content.hpf", generateManifest());
20857
- zip.file("Contents/header.xml", generateHeaderXml(theme, gongmun, fit?.variants ?? []));
20858
- zip.file("Contents/section0.xml", sectionXml);
20859
- zip.file("Preview/PrvText.txt", buildPrvText(blocks));
20860
- return await zip.generateAsync({ type: "arraybuffer" });
20926
+ function normalizedSimilarity(a, b) {
20927
+ return similarity(normalize(a), normalize(b));
20861
20928
  }
20862
- function buildPrvText(blocks) {
20863
- const lines = [];
20864
- let bytes = 0;
20865
- for (const b of blocks) {
20866
- let text = b.text || (b.rows ? b.rows.map((r) => r.join(" ")).join("\n") : "");
20867
- if (b.type === "html_table") text = text.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
20868
- if (!text) continue;
20869
- lines.push(text);
20870
- bytes += text.length * 3;
20871
- if (bytes > 1024) break;
20929
+ function normalize(s) {
20930
+ return s.replace(/\s+/g, " ").trim();
20931
+ }
20932
+ var MAX_LEVENSHTEIN_LEN = 1e4;
20933
+ function levenshtein(a, b) {
20934
+ if (a.length + b.length > MAX_LEVENSHTEIN_LEN) {
20935
+ const sampleLen = Math.min(500, a.length, b.length);
20936
+ let diffs = 0;
20937
+ for (let i = 0; i < sampleLen; i++) if (a[i] !== b[i]) diffs++;
20938
+ const sampleRate = sampleLen > 0 ? diffs / sampleLen : 1;
20939
+ return Math.abs(a.length - b.length) + Math.round(Math.min(a.length, b.length) * sampleRate);
20940
+ }
20941
+ if (a.length > b.length) [a, b] = [b, a];
20942
+ const m = a.length;
20943
+ const n = b.length;
20944
+ let prev = Array.from({ length: m + 1 }, (_, i) => i);
20945
+ let curr = new Array(m + 1);
20946
+ for (let j = 1; j <= n; j++) {
20947
+ curr[0] = j;
20948
+ for (let i = 1; i <= m; i++) {
20949
+ if (a[i - 1] === b[j - 1]) {
20950
+ curr[i] = prev[i - 1];
20951
+ } else {
20952
+ curr[i] = 1 + Math.min(prev[i - 1], prev[i], curr[i - 1]);
20953
+ }
20954
+ }
20955
+ ;
20956
+ [prev, curr] = [curr, prev];
20872
20957
  }
20873
- return lines.join("\n").slice(0, 1024);
20958
+ return prev[m];
20874
20959
  }
20875
- function parseMarkdownToBlocks(md2) {
20960
+
20961
+ // src/roundtrip/markdown-units.ts
20962
+ function splitMarkdownUnits(md2) {
20876
20963
  const lines = md2.split("\n");
20877
- const blocks = [];
20964
+ const units = [];
20878
20965
  let i = 0;
20879
20966
  while (i < lines.length) {
20880
20967
  const line = lines[i];
@@ -20882,420 +20969,369 @@ function parseMarkdownToBlocks(md2) {
20882
20969
  i++;
20883
20970
  continue;
20884
20971
  }
20885
- const fenceMatch = line.match(/^(`{3,}|~{3,})(.*)$/);
20886
- if (fenceMatch) {
20887
- const fence = fenceMatch[1];
20888
- const lang = fenceMatch[2].trim();
20889
- const codeLines = [];
20890
- i++;
20891
- while (i < lines.length && !lines[i].startsWith(fence)) {
20892
- codeLines.push(lines[i]);
20893
- i++;
20894
- }
20895
- if (i < lines.length) i++;
20896
- blocks.push({ type: "code_block", text: codeLines.join("\n"), lang });
20897
- continue;
20898
- }
20899
- if (/^(\*{3,}|-{3,}|_{3,})\s*$/.test(line.trim())) {
20900
- blocks.push({ type: "hr" });
20901
- i++;
20902
- continue;
20903
- }
20904
- const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
20905
- if (headingMatch) {
20906
- blocks.push({ type: "heading", text: headingMatch[2].trim(), level: headingMatch[1].length });
20907
- i++;
20908
- continue;
20909
- }
20910
- if (/^<table[\s>]/i.test(line.trimStart())) {
20911
- const htmlLines = [];
20972
+ if (line.trim().startsWith("<table>")) {
20973
+ const collected2 = [];
20912
20974
  let depth = 0;
20913
20975
  while (i < lines.length) {
20914
20976
  const l = lines[i];
20915
- htmlLines.push(l);
20916
- depth += (l.match(/<table[\s>]/gi) ?? []).length;
20917
- depth -= (l.match(/<\/table>/gi) ?? []).length;
20977
+ collected2.push(l);
20978
+ depth += (l.match(/<table>/g) || []).length;
20979
+ depth -= (l.match(/<\/table>/g) || []).length;
20918
20980
  i++;
20919
20981
  if (depth <= 0) break;
20920
20982
  }
20921
- blocks.push({ type: "html_table", text: htmlLines.join("\n") });
20983
+ units.push({ kind: "html-table", raw: collected2.join("\n"), lines: collected2 });
20922
20984
  continue;
20923
20985
  }
20924
20986
  if (line.trimStart().startsWith("|")) {
20925
- const tableRows = [];
20987
+ const collected2 = [];
20926
20988
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
20927
- const row = lines[i];
20928
- if (/^[\s|:\-]+$/.test(row)) {
20929
- i++;
20930
- continue;
20931
- }
20932
- const cells = row.split("|").slice(1, -1).map((c) => c.trim());
20933
- if (cells.length > 0) tableRows.push(cells);
20989
+ collected2.push(lines[i]);
20934
20990
  i++;
20935
20991
  }
20936
- if (tableRows.length > 0) blocks.push({ type: "table", rows: tableRows });
20992
+ units.push({ kind: "gfm-table", raw: collected2.join("\n"), lines: collected2 });
20937
20993
  continue;
20938
20994
  }
20939
- if (line.trimStart().startsWith("> ")) {
20940
- const quoteLines = [];
20941
- while (i < lines.length && (lines[i].trimStart().startsWith("> ") || lines[i].trimStart().startsWith(">"))) {
20942
- quoteLines.push(lines[i].replace(/^>\s?/, ""));
20943
- i++;
20944
- }
20945
- for (const ql of quoteLines) {
20946
- blocks.push({ type: "blockquote", text: ql.trim() || "" });
20947
- }
20995
+ if (/^-{3,}\s*$/.test(line.trim())) {
20996
+ units.push({ kind: "separator", raw: line.trim(), lines: [line.trim()] });
20997
+ i++;
20948
20998
  continue;
20949
20999
  }
20950
- const listMatch = line.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
20951
- if (listMatch) {
20952
- const indent = Math.floor(listMatch[1].length / 2);
20953
- const ordered = /\d/.test(listMatch[2]);
20954
- blocks.push({ type: "list_item", text: listMatch[3].trim(), ordered, indent });
21000
+ if (/^!\[image\]\([^)]*\)\s*$/.test(line.trim())) {
21001
+ units.push({ kind: "image", raw: line.trim(), lines: [line.trim()] });
20955
21002
  i++;
20956
21003
  continue;
20957
21004
  }
20958
- blocks.push({ type: "paragraph", text: line.trim() });
20959
- i++;
21005
+ const collected = [];
21006
+ while (i < lines.length && lines[i].trim() && !lines[i].trimStart().startsWith("|") && !lines[i].trim().startsWith("<table>")) {
21007
+ collected.push(lines[i].trim());
21008
+ i++;
21009
+ }
21010
+ units.push({ kind: "text", raw: collected.join("\n"), lines: collected });
20960
21011
  }
20961
- return blocks;
21012
+ return units;
20962
21013
  }
20963
- function parseInlineMarkdown(text) {
20964
- text = text.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
20965
- text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_, t, u) => t || u);
20966
- text = text.replace(/~~([^~]+)~~/g, "$1");
20967
- const spans = [];
20968
- const regex = /(`[^`]+`|\*{3}[^*]+\*{3}|\*{2}[^*]+\*{2}|\*[^*]+\*|_{2}[^_]+_{2}|_[^_]+_)/g;
20969
- let lastIdx = 0;
20970
- for (const match of text.matchAll(regex)) {
20971
- const idx = match.index;
20972
- if (idx > lastIdx) {
20973
- spans.push({ text: text.slice(lastIdx, idx), bold: false, italic: false, code: false });
21014
+ function alignUnits(a, b) {
21015
+ const m = a.length, n = b.length;
21016
+ if (m * n > 4e6) {
21017
+ const result2 = [];
21018
+ let pre = 0;
21019
+ while (pre < m && pre < n && a[pre] === b[pre]) {
21020
+ result2.push([pre, pre]);
21021
+ pre++;
20974
21022
  }
20975
- const raw = match[0];
20976
- if (raw.startsWith("`")) {
20977
- spans.push({ text: raw.slice(1, -1), bold: false, italic: false, code: true });
20978
- } else if (raw.startsWith("***") || raw.startsWith("___")) {
20979
- spans.push({ text: raw.slice(3, -3), bold: true, italic: true, code: false });
20980
- } else if (raw.startsWith("**") || raw.startsWith("__")) {
20981
- spans.push({ text: raw.slice(2, -2), bold: true, italic: false, code: false });
21023
+ let suf = 0;
21024
+ while (suf < m - pre && suf < n - pre && a[m - 1 - suf] === b[n - 1 - suf]) suf++;
21025
+ const aMid = m - pre - suf, bMid = n - pre - suf;
21026
+ if (aMid === bMid) {
21027
+ for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, pre + i2]);
20982
21028
  } else {
20983
- spans.push({ text: raw.slice(1, -1), bold: false, italic: true, code: false });
21029
+ for (let i2 = 0; i2 < aMid; i2++) result2.push([pre + i2, null]);
21030
+ for (let j2 = 0; j2 < bMid; j2++) result2.push([null, pre + j2]);
20984
21031
  }
20985
- lastIdx = idx + raw.length;
21032
+ for (let s = suf - 1; s >= 0; s--) result2.push([m - 1 - s, n - 1 - s]);
21033
+ return result2;
20986
21034
  }
20987
- if (lastIdx < text.length) {
20988
- spans.push({ text: text.slice(lastIdx), bold: false, italic: false, code: false });
21035
+ const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1));
21036
+ for (let i2 = 1; i2 <= m; i2++) {
21037
+ for (let j2 = 1; j2 <= n; j2++) {
21038
+ dp[i2][j2] = a[i2 - 1] === b[j2 - 1] ? dp[i2 - 1][j2 - 1] + 1 : Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
21039
+ }
20989
21040
  }
20990
- if (spans.length === 0) {
20991
- spans.push({ text, bold: false, italic: false, code: false });
21041
+ const matches = [];
21042
+ let i = m, j = n;
21043
+ while (i > 0 && j > 0) {
21044
+ if (a[i - 1] === b[j - 1] && dp[i][j] === dp[i - 1][j - 1] + 1) {
21045
+ matches.push([i - 1, j - 1]);
21046
+ i--;
21047
+ j--;
21048
+ } else if (dp[i - 1][j] >= dp[i][j - 1]) i--;
21049
+ else j--;
20992
21050
  }
20993
- return spans;
20994
- }
20995
- function spanToCharPrId(span) {
20996
- if (span.code) return CHAR_CODE;
20997
- if (span.bold && span.italic) return CHAR_BOLD_ITALIC;
20998
- if (span.bold) return CHAR_BOLD;
20999
- if (span.italic) return CHAR_ITALIC;
21000
- return CHAR_NORMAL;
21001
- }
21002
- function escapeXml(text) {
21003
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
21004
- }
21005
- function generateRuns(text, defaultCharPr = CHAR_NORMAL, mapCharId) {
21006
- const spans = parseInlineMarkdown(text);
21007
- return spans.map((span) => {
21008
- let charId = span.code || span.bold || span.italic ? spanToCharPrId(span) : defaultCharPr;
21009
- if (mapCharId) charId = mapCharId(charId);
21010
- return `<hp:run charPrIDRef="${charId}"><hp:t>${escapeXml(span.text)}</hp:t></hp:run>`;
21011
- }).join("");
21012
- }
21013
- function generateParagraph(text, paraPrId = PARA_NORMAL, charPrId = CHAR_NORMAL, mapCharId) {
21014
- if (paraPrId === PARA_CODE) {
21015
- return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_CODE}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`;
21051
+ matches.reverse();
21052
+ const result = [];
21053
+ let ai = 0, bi = 0;
21054
+ const flushGap = (aEnd, bEnd) => {
21055
+ if (aEnd - ai === bEnd - bi) {
21056
+ while (ai < aEnd) result.push([ai++, bi++]);
21057
+ return;
21058
+ }
21059
+ while (ai < aEnd && bi < bEnd) {
21060
+ const sim = normalizedSimilarity(a[ai], b[bi]);
21061
+ if (sim >= 0.4) {
21062
+ if (aEnd - ai > bEnd - bi && bestSimInRange(a, ai + 1, ai + (aEnd - ai) - (bEnd - bi), b[bi]) > sim) {
21063
+ result.push([ai++, null]);
21064
+ } else if (bEnd - bi > aEnd - ai && bestSimInRange(b, bi + 1, bi + (bEnd - bi) - (aEnd - ai), a[ai]) > sim) {
21065
+ result.push([null, bi++]);
21066
+ } else {
21067
+ result.push([ai++, bi++]);
21068
+ }
21069
+ } else if (aEnd - ai >= bEnd - bi) result.push([ai++, null]);
21070
+ else result.push([null, bi++]);
21071
+ }
21072
+ while (ai < aEnd) result.push([ai++, null]);
21073
+ while (bi < bEnd) result.push([null, bi++]);
21074
+ };
21075
+ for (const [pi, pj] of matches) {
21076
+ flushGap(pi, pj);
21077
+ result.push([ai++, bi++]);
21016
21078
  }
21017
- const runs = generateRuns(text, charPrId, mapCharId);
21018
- return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0">${runs}</hp:p>`;
21019
- }
21020
- function headingParaPrId(level) {
21021
- if (level === 1) return PARA_H1;
21022
- if (level === 2) return PARA_H2;
21023
- if (level === 3) return PARA_H3;
21024
- return PARA_H4;
21079
+ flushGap(m, n);
21080
+ return result;
21025
21081
  }
21026
- function headingCharPrId(level) {
21027
- if (level === 1) return CHAR_H1;
21028
- if (level === 2) return CHAR_H2;
21029
- if (level === 3) return CHAR_H3;
21030
- return CHAR_H4;
21082
+ function bestSimInRange(arr, from, to, target) {
21083
+ let best = 0;
21084
+ for (let k = from; k <= to && k < arr.length; k++) {
21085
+ const s = normalizedSimilarity(arr[k], target);
21086
+ if (s > best) best = s;
21087
+ }
21088
+ return best;
21031
21089
  }
21032
- function generateContainerXml() {
21033
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
21034
- <ocf:container xmlns:ocf="${NS_OCF}" xmlns:hpf="${NS_HPF}">
21035
- <ocf:rootfiles>
21036
- <ocf:rootfile full-path="Contents/content.hpf" media-type="application/hwpml-package+xml"/>
21037
- </ocf:rootfiles>
21038
- </ocf:container>`;
21090
+ function escapeGfm(text) {
21091
+ return text.replace(/~/g, "\\~");
21039
21092
  }
21040
- function generateManifest() {
21041
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
21042
- <opf:package xmlns:opf="${NS_OPF}" xmlns:hpf="${NS_HPF}" xmlns:hh="${NS_HEAD}">
21043
- <opf:manifest>
21044
- <opf:item id="header" href="Contents/header.xml" media-type="application/xml"/>
21045
- <opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/>
21046
- </opf:manifest>
21047
- <opf:spine>
21048
- <opf:itemref idref="header" linear="no"/>
21049
- <opf:itemref idref="section0" linear="yes"/>
21050
- </opf:spine>
21051
- </opf:package>`;
21093
+ var HWP_SHAPE_ALT_TEXT_RE = /(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\s?개체|묶음\s?개체|글상자|수식\s?개체|OLE\s?개체)\s?입니다\.?/g;
21094
+ function sanitizeText(text) {
21095
+ let result = mapPuaText(text).replace(/[\u{F0000}-\u{FFFFD}]/gu, "").replace(HWP_SHAPE_ALT_TEXT_RE, "").replace(/ +/g, " ").trim();
21096
+ if (result.length <= 30 && result.includes(" ")) {
21097
+ const tokens = result.split(" ");
21098
+ const koreanSingleCharCount = tokens.filter((t) => t.length === 1 && /[가-힯ㄱ-ㆎ]/.test(t)).length;
21099
+ if (tokens.length >= 3 && koreanSingleCharCount / tokens.length >= 0.7) {
21100
+ result = tokens.join("");
21101
+ }
21102
+ }
21103
+ return result;
21052
21104
  }
21053
- function charPr(id, height, bold, italic, fontId = 0, textColor = DEFAULT_TEXT_COLOR, ratioPct = 100) {
21054
- const boldAttr = bold ? ` bold="1"` : "";
21055
- const italicAttr = italic ? ` italic="1"` : "";
21056
- const effFont = bold ? 2 : fontId;
21057
- return ` <hh:charPr id="${id}" height="${height}" textColor="${textColor}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
21058
- <hh:fontRef hangul="${effFont}" latin="${effFont}" hanja="${effFont}" japanese="${effFont}" other="${effFont}" symbol="${effFont}" user="${effFont}"/>
21059
- <hh:ratio hangul="${ratioPct}" latin="${ratioPct}" hanja="${ratioPct}" japanese="100" other="100" symbol="100" user="100"/>
21060
- <hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
21061
- <hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
21062
- <hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
21063
- </hh:charPr>`;
21105
+ function normForMatch(text) {
21106
+ return sanitizeText(text).replace(/\s+/g, " ").trim();
21064
21107
  }
21065
- function paraPr(id, opts = {}) {
21066
- const { align = "JUSTIFY", spaceBefore = 0, spaceAfter = 0, lineSpacing = 160, indent = 0, left = 0, keepWord = false } = opts;
21067
- const breakNonLatin = keepWord ? "KEEP_WORD" : "BREAK_WORD";
21068
- const snapGrid = keepWord ? "0" : "1";
21069
- return ` <hh:paraPr id="${id}" tabPrIDRef="0" condense="0" fontLineHeight="0" snapToGrid="${snapGrid}" suppressLineNumbers="0" checked="0" textDir="AUTO">
21070
- <hh:align horizontal="${align}" vertical="BASELINE"/>
21071
- <hh:heading type="NONE" idRef="0" level="0"/>
21072
- <hh:breakSetting breakLatinWord="KEEP_WORD" breakNonLatinWord="${breakNonLatin}" widowOrphan="0" keepWithNext="0" keepLines="0" pageBreakBefore="0" lineWrap="BREAK"/>
21073
- <hh:autoSpacing eAsianEng="0" eAsianNum="0"/>
21074
- <hh:margin><hc:intent value="${indent}" unit="HWPUNIT"/><hc:left value="${left}" unit="HWPUNIT"/><hc:right value="0" unit="HWPUNIT"/><hc:prev value="${spaceBefore}" unit="HWPUNIT"/><hc:next value="${spaceAfter}" unit="HWPUNIT"/></hh:margin>
21075
- <hh:lineSpacing type="PERCENT" value="${lineSpacing}"/>
21076
- <hh:border borderFillIDRef="1" offsetLeft="0" offsetRight="0" offsetTop="0" offsetBottom="0" connect="0" ignoreMargin="0"/>
21077
- </hh:paraPr>`;
21108
+ function unescapeGfm(text) {
21109
+ return text.replace(/\\~/g, "~");
21078
21110
  }
21079
- var GONGMUN_LIST_BASE = 8;
21080
- var GONGMUN_LIST_LEVELS = 8;
21081
- var GONGMUN_CENTER = GONGMUN_LIST_BASE + GONGMUN_LIST_LEVELS;
21082
- var CHAR_VARIANT_BASE = 11;
21083
- var GONGMUN_BODY_RATIO = 95;
21084
- function plainRenderText(text) {
21085
- return parseInlineMarkdown(text).map((s) => s.text).join("");
21111
+ function summarize(text) {
21112
+ const t = text.replace(/\s+/g, " ").trim();
21113
+ return t.length > 80 ? t.slice(0, 77) + "..." : t;
21086
21114
  }
21087
- function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
21088
- const minRatio = gongmun.autoFitMinRatio;
21089
- if (minRatio === null || minRatio >= GONGMUN_BODY_RATIO) return null;
21090
- const pageW = 59528 - mmToHwpunit(gongmun.margins.left) - mmToHwpunit(gongmun.margins.right);
21091
- const ratioByBlock = /* @__PURE__ */ new Map();
21092
- const variants = [];
21093
- for (let i = 0; i < blocks.length; i++) {
21094
- const block = blocks[i];
21095
- let text;
21096
- let firstW;
21097
- let contW;
21098
- if (block.type === "list_item" && gongmunList.has(i)) {
21099
- const { marker, depth } = gongmunList.get(i);
21100
- const content = plainRenderText(block.text || "");
21101
- text = marker ? `${marker} ${content}` : content;
21102
- const { left, indent } = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering);
21103
- firstW = pageW - left - Math.max(indent, 0);
21104
- contW = pageW - left - Math.max(-indent, 0);
21105
- } else if (block.type === "paragraph") {
21106
- const raw = (block.text || "").trim();
21107
- if (/^<center>[\s\S]*<\/center>$/i.test(raw)) continue;
21108
- text = plainRenderText(raw);
21109
- firstW = contW = pageW;
21110
- } else {
21115
+ function replicateGfmTable(table) {
21116
+ const { cells, rows: numRows, cols: numCols } = table;
21117
+ if (numRows === 0 || numCols === 0) return null;
21118
+ if (numRows === 1 && numCols === 1) return null;
21119
+ if (numCols === 1) return null;
21120
+ const display = Array.from({ length: numRows }, (_, r) => Array.from({ length: numCols }, (_2, c) => ({ text: "", gridR: r, gridC: c })));
21121
+ const skip = /* @__PURE__ */ new Set();
21122
+ for (let r = 0; r < numRows; r++) {
21123
+ for (let c = 0; c < numCols; c++) {
21124
+ if (skip.has(`${r},${c}`)) continue;
21125
+ const cell = cells[r]?.[c];
21126
+ if (!cell) continue;
21127
+ display[r][c] = {
21128
+ text: escapeGfm(sanitizeText(cell.text)).replace(/\|/g, "\\|").replace(/\n/g, "<br>"),
21129
+ gridR: r,
21130
+ gridC: c
21131
+ };
21132
+ for (let dr = 0; dr < cell.rowSpan; dr++) {
21133
+ for (let dc = 0; dc < cell.colSpan; dc++) {
21134
+ if (dr === 0 && dc === 0) continue;
21135
+ if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
21136
+ }
21137
+ }
21138
+ c += cell.colSpan - 1;
21139
+ }
21140
+ }
21141
+ const uniqueRows = [];
21142
+ let pendingLabelRow = null;
21143
+ for (let r = 0; r < display.length; r++) {
21144
+ const row = display[r];
21145
+ if (row.every((cell) => cell.text === "")) continue;
21146
+ const nonEmptyCols = row.filter((cell) => cell.text !== "");
21147
+ const hasSkipInRow = row.some((_, c) => skip.has(`${r},${c}`));
21148
+ if (!hasSkipInRow && nonEmptyCols.length === 1 && row[0].text !== "" && row.slice(1).every((c) => c.text === "")) {
21149
+ if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
21150
+ pendingLabelRow = row;
21111
21151
  continue;
21112
21152
  }
21113
- if (!text) continue;
21114
- const r = fitRatioForFewerLines(text, firstW, contW, gongmun.bodyHeight, GONGMUN_BODY_RATIO, minRatio);
21115
- if (r === null) continue;
21116
- ratioByBlock.set(i, r);
21117
- if (!variants.includes(r)) variants.push(r);
21153
+ if (pendingLabelRow) {
21154
+ if (row[0].text === "") row[0] = pendingLabelRow[0];
21155
+ else uniqueRows.push(pendingLabelRow);
21156
+ pendingLabelRow = null;
21157
+ }
21158
+ uniqueRows.push(row);
21118
21159
  }
21119
- return ratioByBlock.size > 0 ? { ratioByBlock, variants } : null;
21160
+ if (pendingLabelRow) uniqueRows.push(pendingLabelRow);
21161
+ return uniqueRows.length > 0 ? uniqueRows : null;
21120
21162
  }
21121
- function variantMapper(fit, blockIdx) {
21122
- const r = fit.ratioByBlock.get(blockIdx);
21123
- if (r === void 0) return void 0;
21124
- const vi = fit.variants.indexOf(r);
21125
- return (id) => id >= 0 && id <= 3 ? CHAR_VARIANT_BASE + vi * 4 + id : id;
21163
+ function parseGfmTable(lines) {
21164
+ const rows = [];
21165
+ for (const line of lines) {
21166
+ const trimmed = line.trim();
21167
+ if (!trimmed.startsWith("|")) continue;
21168
+ const cells = trimmed.split(/(?<!\\)\|/).slice(1, -1).map((c) => c.trim());
21169
+ if (cells.length === 0) continue;
21170
+ if (cells.every((c) => /^:?-{3,}:?$/.test(c))) continue;
21171
+ rows.push(cells);
21172
+ }
21173
+ return rows;
21126
21174
  }
21127
- function buildCharProperties(theme, gongmun, ratioVariants = []) {
21128
- let body = 1e3, code = 900, h1 = 1800, h2 = 1400, h3 = 1200, h4 = 1100;
21129
- if (gongmun) {
21130
- body = gongmun.bodyHeight;
21131
- code = Math.max(body - 200, 900);
21132
- h1 = gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700;
21133
- h2 = 1600;
21134
- h3 = body;
21135
- h4 = Math.max(body - 100, 1300);
21175
+ function unescapeGfmCell(text) {
21176
+ return text.replace(/<br\s*\/?>/gi, "\n").replace(/\\\|/g, "|").replace(/\\~/g, "~");
21177
+ }
21178
+ function replicateCellInnerHtml(cell) {
21179
+ if (cell.blocks?.length) {
21180
+ return cell.blocks.map((b) => {
21181
+ if (b.type === "table" && b.table) {
21182
+ const cap = b.table.caption ? sanitizeText(b.table.caption) : "";
21183
+ return (cap ? cap + "<br>" : "") + replicateTableToHtml(b.table);
21184
+ }
21185
+ if (b.type === "image" && b.text) return `<img src="${b.text}" alt="image">`;
21186
+ const t = sanitizeText(b.text ?? "");
21187
+ return t ? t.replace(/\n/g, "<br>") : "";
21188
+ }).filter(Boolean).join("<br>");
21136
21189
  }
21137
- const bodyRatio = gongmun ? GONGMUN_BODY_RATIO : 100;
21138
- const rows = [
21139
- charPr(0, body, false, false, 0, theme.body, bodyRatio),
21140
- charPr(1, body, true, false, 0, theme.body, bodyRatio),
21141
- charPr(2, body, false, true, 0, theme.body, bodyRatio),
21142
- charPr(3, body, true, true, 0, theme.body, bodyRatio),
21143
- charPr(4, code, false, false, 1),
21144
- charPr(5, h1, true, false, 1, theme.h1),
21145
- charPr(6, h2, true, false, 1, theme.h2),
21146
- charPr(7, h3, true, false, 1, theme.h3),
21147
- charPr(8, h4, true, false, 1, theme.h4),
21148
- charPr(CHAR_TABLE_HEADER, body, theme.tableHeaderBold, false, 0, theme.tableHeader),
21149
- charPr(CHAR_QUOTE, body, false, true, 0, theme.quote)
21150
- ];
21151
- for (const r of ratioVariants) {
21152
- rows.push(
21153
- charPr(rows.length, body, false, false, 0, theme.body, r),
21154
- charPr(rows.length + 1, body, true, false, 0, theme.body, r),
21155
- charPr(rows.length + 2, body, false, true, 0, theme.body, r),
21156
- charPr(rows.length + 3, body, true, true, 0, theme.body, r)
21157
- );
21190
+ return sanitizeText(cell.text).replace(/\n/g, "<br>");
21191
+ }
21192
+ function replicateTableToHtml(table) {
21193
+ const rows = replicateHtmlTable(table);
21194
+ const lines = ["<table>"];
21195
+ for (let r = 0; r < rows.length; r++) {
21196
+ const tag = rows[r].tag;
21197
+ const rowHtml = rows[r].cells.map((cell) => {
21198
+ const attrs = [];
21199
+ if (cell.colSpan > 1) attrs.push(`colspan="${cell.colSpan}"`);
21200
+ if (cell.rowSpan > 1) attrs.push(`rowspan="${cell.rowSpan}"`);
21201
+ const attrStr = attrs.length ? " " + attrs.join(" ") : "";
21202
+ return `<${tag}${attrStr}>${cell.inner}</${tag}>`;
21203
+ });
21204
+ if (rowHtml.length) lines.push(`<tr>${rowHtml.join("")}</tr>`);
21205
+ }
21206
+ lines.push("</table>");
21207
+ return lines.join("\n");
21208
+ }
21209
+ function replicateHtmlTable(table) {
21210
+ const { cells, rows: numRows, cols: numCols } = table;
21211
+ const skip = /* @__PURE__ */ new Set();
21212
+ const result = [];
21213
+ for (let r = 0; r < numRows; r++) {
21214
+ const tag = r === 0 ? "th" : "td";
21215
+ const rowCells = [];
21216
+ for (let c = 0; c < numCols; c++) {
21217
+ if (skip.has(`${r},${c}`)) continue;
21218
+ const cell = cells[r]?.[c];
21219
+ if (!cell) continue;
21220
+ for (let dr = 0; dr < cell.rowSpan; dr++) {
21221
+ for (let dc = 0; dc < cell.colSpan; dc++) {
21222
+ if (dr === 0 && dc === 0) continue;
21223
+ if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`);
21224
+ }
21225
+ }
21226
+ rowCells.push({
21227
+ inner: replicateCellInnerHtml(cell),
21228
+ colSpan: cell.colSpan,
21229
+ rowSpan: cell.rowSpan,
21230
+ gridR: r,
21231
+ gridC: c
21232
+ });
21233
+ }
21234
+ if (rowCells.length) result.push({ tag, cells: rowCells });
21235
+ }
21236
+ return result;
21237
+ }
21238
+ function parseHtmlTable(raw) {
21239
+ const re = /<(\/?)(table|tr|td|th)((?:"[^"]*"|'[^']*'|[^>"'])*?)>/gi;
21240
+ let depth = 0;
21241
+ let currentRow = null;
21242
+ let cellStart = -1;
21243
+ let cellInfo = null;
21244
+ const rows = [];
21245
+ let m;
21246
+ while ((m = re.exec(raw)) !== null) {
21247
+ const isClose = m[1] === "/";
21248
+ const tag = m[2].toLowerCase();
21249
+ const attrs = m[3] || "";
21250
+ if (tag === "table") {
21251
+ depth += isClose ? -1 : 1;
21252
+ if (depth < 0) return null;
21253
+ continue;
21254
+ }
21255
+ if (depth !== 1) continue;
21256
+ if (tag === "tr") {
21257
+ if (!isClose) currentRow = [];
21258
+ else if (currentRow) {
21259
+ rows.push({ tag: rows.length === 0 ? "th" : "td", cells: currentRow });
21260
+ currentRow = null;
21261
+ }
21262
+ } else {
21263
+ if (!isClose) {
21264
+ const cs = parseInt(attrs.match(/colspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
21265
+ const rs = parseInt(attrs.match(/rowspan\s*=\s*"(\d+)"/i)?.[1] || "1", 10);
21266
+ cellStart = m.index + m[0].length;
21267
+ cellInfo = { colSpan: isNaN(cs) ? 1 : cs, rowSpan: isNaN(rs) ? 1 : rs };
21268
+ } else if (cellStart >= 0 && cellInfo && currentRow) {
21269
+ currentRow.push({ inner: raw.slice(cellStart, m.index), colSpan: cellInfo.colSpan, rowSpan: cellInfo.rowSpan });
21270
+ cellStart = -1;
21271
+ cellInfo = null;
21272
+ }
21273
+ }
21158
21274
  }
21159
- return `<hh:charProperties itemCnt="${rows.length}">
21160
- ${rows.join("\n")}
21161
- </hh:charProperties>`;
21275
+ if (depth !== 0) return null;
21276
+ return rows;
21162
21277
  }
21163
- function buildParaProperties(gongmun) {
21164
- if (!gongmun) {
21165
- const base2 = [
21166
- paraPr(0),
21167
- paraPr(1, { align: "LEFT", spaceBefore: 800, spaceAfter: 200, lineSpacing: 180 }),
21168
- paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: 170 }),
21169
- paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: 160 }),
21170
- paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: 160 }),
21171
- paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400 }),
21172
- paraPr(6, { align: "LEFT", lineSpacing: 150, indent: 600 }),
21173
- paraPr(7, { align: "LEFT", lineSpacing: 160, indent: 600 })
21174
- ];
21175
- return `<hh:paraProperties itemCnt="${base2.length}">
21176
- ${base2.join("\n")}
21177
- </hh:paraProperties>`;
21278
+ var AUTONUM_PREFIX_RE = /^(?:[0-90-9a-zA-Z가-힣]{1,6}[.)\]:]|[([][0-90-9a-zA-Z가-힣]{1,6}[)\]][.:]?|[ⅰ-ⅹⅠ-Ⅹ①-⑮][.)\]:]?)$/u;
21279
+ function htmlCellInnerToLines(inner) {
21280
+ let hadNonText = false;
21281
+ let work = inner;
21282
+ if (/<table[\s>]/i.test(work)) {
21283
+ hadNonText = true;
21284
+ work = removeNestedTables(work);
21178
21285
  }
21179
- const ls = gongmun.lineSpacing;
21180
- const titleAlign = gongmun.centerTitle ? "CENTER" : "LEFT";
21181
- const base = [
21182
- paraPr(0, { lineSpacing: ls, keepWord: true }),
21183
- paraPr(1, { align: titleAlign, spaceBefore: 400, spaceAfter: 400, lineSpacing: ls, keepWord: true }),
21184
- paraPr(2, { align: "LEFT", spaceBefore: 600, spaceAfter: 150, lineSpacing: ls, keepWord: true }),
21185
- paraPr(3, { align: "LEFT", spaceBefore: 400, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
21186
- paraPr(4, { align: "LEFT", spaceBefore: 300, spaceAfter: 100, lineSpacing: ls, keepWord: true }),
21187
- paraPr(5, { align: "LEFT", lineSpacing: 130, indent: 400, keepWord: true }),
21188
- paraPr(6, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true }),
21189
- paraPr(7, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true })
21190
- ];
21191
- for (let d = 0; d < GONGMUN_LIST_LEVELS; d++) {
21192
- const { left, indent } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering);
21193
- const sectionGap = gongmun.numbering === "report" && d === 0 ? Math.round(gongmun.bodyHeight * 0.5) : 0;
21194
- base.push(paraPr(GONGMUN_LIST_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent, spaceBefore: sectionGap, keepWord: true }));
21286
+ if (/<img\s/i.test(work)) {
21287
+ hadNonText = true;
21288
+ work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, "");
21195
21289
  }
21196
- base.push(paraPr(GONGMUN_CENTER, { align: "CENTER", lineSpacing: ls, keepWord: true }));
21197
- return `<hh:paraProperties itemCnt="${base.length}">
21198
- ${base.join("\n")}
21199
- </hh:paraProperties>`;
21290
+ const lines = work.split(/<br\s*\/?>/gi).map((s) => s.trim()).filter((s) => s.length > 0);
21291
+ return { lines, hadNonText };
21200
21292
  }
21201
- function generateHeaderXml(theme, gongmun, ratioVariants = []) {
21202
- const bodyFace = gongmun?.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD568\uCD08\uB86C\uBC14\uD0D5";
21203
- const charPropsXml = buildCharProperties(theme, gongmun, ratioVariants);
21204
- const paraPropsXml = buildParaProperties(gongmun);
21205
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
21206
- <hh:head xmlns:hh="${NS_HEAD}" xmlns:hp="${NS_PARA}" xmlns:hc="${NS_CORE}" version="1.4" secCnt="1">
21207
- <hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
21208
- <hh:refList>
21209
- <hh:fontfaces itemCnt="7">
21210
- <hh:fontface lang="HANGUL" fontCnt="3">
21211
- <hh:font id="0" face="${bodyFace}" type="TTF" isEmbedded="0">
21212
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21213
- </hh:font>
21214
- <hh:font id="1" face="\uD568\uCD08\uB86C\uB3CB\uC6C0" type="TTF" isEmbedded="0">
21215
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21216
- </hh:font>
21217
- <hh:font id="2" face="HY\uACAC\uACE0\uB515" type="TTF" isEmbedded="0">
21218
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21219
- </hh:font>
21220
- </hh:fontface>
21221
- <hh:fontface lang="LATIN" fontCnt="3">
21222
- <hh:font id="0" face="Times New Roman" type="TTF" isEmbedded="0">
21223
- <hh:typeInfo familyType="FCAT_OLDSTYLE" weight="5" proportion="4" contrast="2" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="4"/>
21224
- </hh:font>
21225
- <hh:font id="1" face="Consolas" type="TTF" isEmbedded="0">
21226
- <hh:typeInfo familyType="FCAT_MODERN" weight="5" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
21227
- </hh:font>
21228
- <hh:font id="2" face="Arial Black" type="TTF" isEmbedded="0">
21229
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
21230
- </hh:font>
21231
- </hh:fontface>
21232
- <hh:fontface lang="HANJA" fontCnt="1">
21233
- <hh:font id="0" face="\uD568\uCD08\uB86C\uBC14\uD0D5" type="TTF" isEmbedded="0">
21234
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21235
- </hh:font>
21236
- </hh:fontface>
21237
- <hh:fontface lang="JAPANESE" fontCnt="1">
21238
- <hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
21239
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21240
- </hh:font>
21241
- </hh:fontface>
21242
- <hh:fontface lang="OTHER" fontCnt="1">
21243
- <hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
21244
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21245
- </hh:font>
21246
- </hh:fontface>
21247
- <hh:fontface lang="SYMBOL" fontCnt="1">
21248
- <hh:font id="0" face="Symbol" type="TTF" isEmbedded="0">
21249
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21250
- </hh:font>
21251
- </hh:fontface>
21252
- <hh:fontface lang="USER" fontCnt="1">
21253
- <hh:font id="0" face="\uAD74\uB9BC" type="TTF" isEmbedded="0">
21254
- <hh:typeInfo familyType="FCAT_GOTHIC" weight="6" proportion="0" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
21255
- </hh:font>
21256
- </hh:fontface>
21257
- </hh:fontfaces>
21258
- <hh:borderFills itemCnt="2">
21259
- <hh:borderFill id="1" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
21260
- <hh:slash type="NONE" Crooked="0" isCounter="0"/>
21261
- <hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
21262
- <hh:leftBorder type="NONE" width="0.1 mm" color="#000000"/>
21263
- <hh:rightBorder type="NONE" width="0.1 mm" color="#000000"/>
21264
- <hh:topBorder type="NONE" width="0.1 mm" color="#000000"/>
21265
- <hh:bottomBorder type="NONE" width="0.1 mm" color="#000000"/>
21266
- </hh:borderFill>
21267
- <hh:borderFill id="2" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
21268
- <hh:slash type="NONE" Crooked="0" isCounter="0"/>
21269
- <hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
21270
- <hh:leftBorder type="SOLID" width="0.12 mm" color="#000000"/>
21271
- <hh:rightBorder type="SOLID" width="0.12 mm" color="#000000"/>
21272
- <hh:topBorder type="SOLID" width="0.12 mm" color="#000000"/>
21273
- <hh:bottomBorder type="SOLID" width="0.12 mm" color="#000000"/>
21274
- </hh:borderFill>
21275
- </hh:borderFills>
21276
- ${charPropsXml}
21277
- <hh:tabProperties itemCnt="0"/>
21278
- <hh:numberings itemCnt="0"/>
21279
- <hh:bullets itemCnt="0"/>
21280
- ${paraPropsXml}
21281
- <hh:styles itemCnt="1">
21282
- <hh:style id="0" type="PARA" name="\uBC14\uD0D5\uAE00" engName="Normal" paraPrIDRef="0" charPrIDRef="0" nextStyleIDRef="0" langIDRef="1042" lockForm="0"/>
21283
- </hh:styles>
21284
- </hh:refList>
21285
- <hh:compatibleDocument targetProgram="HWP2018"><hh:layoutCompatibility/></hh:compatibleDocument>
21286
- </hh:head>`;
21293
+ function extractTopLevelTables(html) {
21294
+ const result = [];
21295
+ let depth = 0;
21296
+ let start = -1;
21297
+ const re = /<(\/?)table(?:[\s>]|>)/gi;
21298
+ let m;
21299
+ while ((m = re.exec(html)) !== null) {
21300
+ if (m[1] !== "/") {
21301
+ if (depth === 0) start = m.index;
21302
+ depth++;
21303
+ } else {
21304
+ depth--;
21305
+ if (depth === 0 && start >= 0) {
21306
+ result.push(html.slice(start, m.index + m[0].length));
21307
+ start = -1;
21308
+ }
21309
+ if (depth < 0) depth = 0;
21310
+ }
21311
+ }
21312
+ return result;
21287
21313
  }
21288
- function generateSecPr(gongmun) {
21289
- const m = gongmun ? {
21290
- top: mmToHwpunit(gongmun.margins.top),
21291
- bottom: mmToHwpunit(gongmun.margins.bottom),
21292
- left: mmToHwpunit(gongmun.margins.left),
21293
- right: mmToHwpunit(gongmun.margins.right),
21294
- header: 0,
21295
- footer: 0
21296
- } : { top: 8504, bottom: 4252, left: 5670, right: 4252, header: 2835, footer: 2835 };
21297
- return `<hp:secPr textDirection="HORIZONTAL" spaceColumns="1134" tabStop="8000" outlineShapeIDRef="0" memoShapeIDRef="0" textVerticalWidthHead="0" masterPageCnt="0"><hp:grid lineGrid="0" charGrid="0" wonggojiFormat="0"/><hp:startNum pageStartsOn="BOTH" page="0" pic="0" tbl="0" equation="0"/><hp:visibility hideFirstHeader="0" hideFirstFooter="0" hideFirstMasterPage="0" border="SHOW_ALL" fill="SHOW_ALL" hideFirstPageNum="0" hideFirstEmptyLine="0" showLineNumber="0"/><hp:pagePr landscape="WIDELY" width="59528" height="84188" gutterType="LEFT_ONLY"><hp:margin header="${m.header}" footer="${m.footer}" gutter="0" left="${m.left}" right="${m.right}" top="${m.top}" bottom="${m.bottom}"/></hp:pagePr><hp:footNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="-1" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="283" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="EACH_COLUMN" beneathText="0"/></hp:footNotePr><hp:endNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="14692344" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="0" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="END_OF_DOCUMENT" beneathText="0"/></hp:endNotePr></hp:secPr>`;
21314
+ function removeNestedTables(html) {
21315
+ let result = "";
21316
+ let depth = 0;
21317
+ const re = /<(\/?)table(?:[\s>]|>)/gi;
21318
+ let last = 0;
21319
+ let m;
21320
+ while ((m = re.exec(html)) !== null) {
21321
+ if (m[1] !== "/") {
21322
+ if (depth === 0) result += html.slice(last, m.index);
21323
+ depth++;
21324
+ } else {
21325
+ depth--;
21326
+ if (depth === 0) last = m.index + m[0].length;
21327
+ if (depth < 0) depth = 0;
21328
+ }
21329
+ }
21330
+ if (depth === 0) result += html.slice(last);
21331
+ return result;
21298
21332
  }
21333
+
21334
+ // src/hwpx/gen-table.ts
21299
21335
  var TABLE_ID_BASE = 1e3;
21300
21336
  var tableIdCounter = TABLE_ID_BASE;
21301
21337
  function nextTableId() {
@@ -21385,41 +21421,18 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3) {
21385
21421
  }
21386
21422
  return `<hp:tbl id="${tblId}" zOrder="0" numberingType="TABLE" pageBreak="CELL" repeatHeader="0" rowCnt="${rowCnt}" colCnt="${colCnt}" cellSpacing="0" borderFillIDRef="2" noShading="0"><hp:sz width="${tblW}" widthRelTo="ABSOLUTE" height="${cellH * rowCnt}" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="0" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="0" right="0" top="0" bottom="0"/><hp:inMargin left="510" right="510" top="141" bottom="141"/>` + trXmls.join("") + `</hp:tbl>`;
21387
21423
  }
21388
- function precomputeGongmunList(blocks, gongmun) {
21389
- const result = /* @__PURE__ */ new Map();
21390
- let i = 0;
21391
- while (i < blocks.length) {
21392
- if (blocks[i].type !== "list_item") {
21393
- i++;
21394
- continue;
21395
- }
21396
- const run = [];
21397
- while (i < blocks.length) {
21398
- const t = blocks[i].type;
21399
- if (t === "list_item") {
21400
- run.push(i);
21401
- i++;
21402
- continue;
21403
- }
21404
- if (t === "table" || t === "html_table") {
21405
- let j = i + 1;
21406
- while (j < blocks.length && (blocks[j].type === "table" || blocks[j].type === "html_table")) j++;
21407
- if (j < blocks.length && blocks[j].type === "list_item") {
21408
- i = j;
21409
- continue;
21410
- }
21411
- }
21412
- break;
21413
- }
21414
- const depths = run.map((bi) => Math.min(Math.max(blocks[bi].indent || 0, 0), GONGMUN_LIST_LEVELS - 1));
21415
- const suppress = gongmun.numbering === "standard" ? computeSuppression(depths) : depths.map(() => false);
21416
- const numberer = new GongmunNumberer(gongmun.numbering);
21417
- run.forEach((bi, k) => {
21418
- const marker = numberer.next(depths[k], suppress[k]);
21419
- result.set(bi, { marker, depth: depths[k] });
21420
- });
21421
- }
21422
- return result;
21424
+
21425
+ // src/hwpx/gen-section.ts
21426
+ function generateSecPr(gongmun) {
21427
+ const m = gongmun ? {
21428
+ top: mmToHwpunit(gongmun.margins.top),
21429
+ bottom: mmToHwpunit(gongmun.margins.bottom),
21430
+ left: mmToHwpunit(gongmun.margins.left),
21431
+ right: mmToHwpunit(gongmun.margins.right),
21432
+ header: 0,
21433
+ footer: 0
21434
+ } : { top: 8504, bottom: 4252, left: 5670, right: 4252, header: 2835, footer: 2835 };
21435
+ return `<hp:secPr textDirection="HORIZONTAL" spaceColumns="1134" tabStop="8000" outlineShapeIDRef="0" memoShapeIDRef="0" textVerticalWidthHead="0" masterPageCnt="0"><hp:grid lineGrid="0" charGrid="0" wonggojiFormat="0"/><hp:startNum pageStartsOn="BOTH" page="0" pic="0" tbl="0" equation="0"/><hp:visibility hideFirstHeader="0" hideFirstFooter="0" hideFirstMasterPage="0" border="SHOW_ALL" fill="SHOW_ALL" hideFirstPageNum="0" hideFirstEmptyLine="0" showLineNumber="0"/><hp:pagePr landscape="WIDELY" width="59528" height="84188" gutterType="LEFT_ONLY"><hp:margin header="${m.header}" footer="${m.footer}" gutter="0" left="${m.left}" right="${m.right}" top="${m.top}" bottom="${m.bottom}"/></hp:pagePr><hp:footNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="-1" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="283" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="EACH_COLUMN" beneathText="0"/></hp:footNotePr><hp:endNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="14692344" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="0" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="END_OF_DOCUMENT" beneathText="0"/></hp:endNotePr></hp:secPr>`;
21423
21436
  }
21424
21437
  function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null, fit = null) {
21425
21438
  const paraXmls = [];
@@ -21542,6 +21555,24 @@ function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? prec
21542
21555
  </hs:sec>`;
21543
21556
  }
21544
21557
 
21558
+ // src/hwpx/generator.ts
21559
+ async function markdownToHwpx(markdown, options) {
21560
+ const theme = resolveTheme(options?.theme);
21561
+ const gongmun = options?.gongmun ? resolveGongmun(options.gongmun) : null;
21562
+ const blocks = parseMarkdownToBlocks(markdown);
21563
+ const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
21564
+ const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
21565
+ const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit);
21566
+ const zip = new JSZip6();
21567
+ zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
21568
+ zip.file("META-INF/container.xml", generateContainerXml());
21569
+ zip.file("Contents/content.hpf", generateManifest());
21570
+ zip.file("Contents/header.xml", generateHeaderXml(theme, gongmun, fit?.variants ?? []));
21571
+ zip.file("Contents/section0.xml", sectionXml);
21572
+ zip.file("Preview/PrvText.txt", buildPrvText(blocks));
21573
+ return await zip.generateAsync({ type: "arraybuffer" });
21574
+ }
21575
+
21545
21576
  // src/diff/compare.ts
21546
21577
  var SIMILARITY_THRESHOLD = 0.4;
21547
21578
  async function compare(bufferA, bufferB, options) {
@@ -21677,7 +21708,7 @@ function diffTableCells(a, b) {
21677
21708
  }
21678
21709
 
21679
21710
  // src/roundtrip/patcher.ts
21680
- import JSZip6 from "jszip";
21711
+ import JSZip7 from "jszip";
21681
21712
 
21682
21713
  // src/roundtrip/table-rows.ts
21683
21714
  var ROW_OBJECT_RE = /<(?:[A-Za-z0-9_]+:)?(?:tbl|pic|equation|ole|container|shape|drawingObject|drawText|video|chart|fieldBegin|fieldEnd|ctrl)\b/;
@@ -22315,7 +22346,7 @@ async function patchHwpx(original, editedMarkdown, options) {
22315
22346
  }
22316
22347
  let zip;
22317
22348
  try {
22318
- zip = await JSZip6.loadAsync(original);
22349
+ zip = await JSZip7.loadAsync(original);
22319
22350
  } catch {
22320
22351
  return { success: false, applied: 0, skipped, error: "ZIP \uB85C\uB4DC \uC2E4\uD328" };
22321
22352
  }
@@ -23682,10 +23713,10 @@ function stageParaPatch(scan, para, newPlain, skip) {
23682
23713
  }
23683
23714
 
23684
23715
  // src/roundtrip/session.ts
23685
- import JSZip7 from "jszip";
23716
+ import JSZip8 from "jszip";
23686
23717
  async function buildState(bytes) {
23687
23718
  const parsed = await parseHwpxDocument(u8ToArrayBuffer2(bytes));
23688
- const zip = await JSZip7.loadAsync(bytes);
23719
+ const zip = await JSZip8.loadAsync(bytes);
23689
23720
  const sectionPaths = await resolveSectionEntryNames(zip);
23690
23721
  if (sectionPaths.length === 0) {
23691
23722
  throw new Error("HWPX \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -24266,7 +24297,7 @@ async function parseHwp(buffer, options) {
24266
24297
  async function parsePdf(buffer, options) {
24267
24298
  let parsePdfDocument;
24268
24299
  try {
24269
- const mod = await import("./parser-7G5F7PT2.js");
24300
+ const mod = await import("./parser-KNQDRLZQ.js");
24270
24301
  parsePdfDocument = mod.parsePdfDocument;
24271
24302
  } catch {
24272
24303
  return {
@@ -24401,4 +24432,4 @@ export {
24401
24432
  parseHwpml,
24402
24433
  fillForm
24403
24434
  };
24404
- //# sourceMappingURL=chunk-SLKF72QF.js.map
24435
+ //# sourceMappingURL=chunk-DP37KF2X.js.map