kordoc 4.2.9 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +922 -885
  2. package/dist/{-ECUCTQTR.js → -DYHT3HWH.js} +28 -20
  3. package/dist/{chunk-J2RZ444H.js → chunk-3FFPHTXD.js} +1 -1
  4. package/dist/chunk-3FFPHTXD.js.map +1 -0
  5. package/dist/{chunk-27Z4PUZZ.js → chunk-4DBCUF3C.js} +38 -7
  6. package/dist/chunk-4DBCUF3C.js.map +1 -0
  7. package/dist/{chunk-5RPYBC2Q.js → chunk-6GBLFQMA.js} +1 -1
  8. package/dist/chunk-6GBLFQMA.js.map +1 -0
  9. package/dist/{chunk-VEHC4EJL.js → chunk-6RAYTNQT.js} +5 -5
  10. package/dist/chunk-6RAYTNQT.js.map +1 -0
  11. package/dist/{chunk-GEPINOL6.js → chunk-6XPXIG57.js} +2 -2
  12. package/dist/chunk-6XPXIG57.js.map +1 -0
  13. package/dist/{chunk-7ZARSBQ3.js → chunk-7MFO4TKP.js} +4 -3
  14. package/dist/chunk-7MFO4TKP.js.map +1 -0
  15. package/dist/{chunk-PATPZTH7.cjs → chunk-AL34DGS5.cjs} +6 -6
  16. package/dist/chunk-AL34DGS5.cjs.map +1 -0
  17. package/dist/{chunk-LXE7OR6N.js → chunk-ARXNKC5D.js} +3 -3
  18. package/dist/chunk-ARXNKC5D.js.map +1 -0
  19. package/dist/{chunk-6GM2QLM3.cjs → chunk-AUOEZKJE.cjs} +21 -21
  20. package/dist/chunk-AUOEZKJE.cjs.map +1 -0
  21. package/dist/chunk-DCZVOIEO.cjs.map +1 -1
  22. package/dist/{chunk-3TSTZGCJ.js → chunk-DRQUAYNT.js} +4 -3
  23. package/dist/chunk-DRQUAYNT.js.map +1 -0
  24. package/dist/{chunk-G3DMDTZI.js → chunk-DRZUJ2RK.js} +1511 -1135
  25. package/dist/chunk-DRZUJ2RK.js.map +1 -0
  26. package/dist/{chunk-NIISBGSR.js → chunk-EFVAY5VE.js} +71 -29
  27. package/dist/chunk-EFVAY5VE.js.map +1 -0
  28. package/dist/{chunk-3PLF5UIS.js → chunk-EZHW3RNQ.js} +37 -6
  29. package/dist/chunk-EZHW3RNQ.js.map +1 -0
  30. package/dist/chunk-GS7T56RP.cjs.map +1 -1
  31. package/dist/{chunk-MHMVFI2N.js → chunk-GSMPTZC7.js} +4 -4
  32. package/dist/chunk-GSMPTZC7.js.map +1 -0
  33. package/dist/{chunk-GZ7W64QU.cjs → chunk-JFV7QT5T.cjs} +4 -3
  34. package/dist/chunk-JFV7QT5T.cjs.map +1 -0
  35. package/dist/{chunk-4ZWKFDUC.cjs → chunk-LPQWKQQQ.cjs} +44 -13
  36. package/dist/chunk-LPQWKQQQ.cjs.map +1 -0
  37. package/dist/{chunk-H7OWEYH2.cjs → chunk-MGGU3ZRK.cjs} +1 -1
  38. package/dist/chunk-MGGU3ZRK.cjs.map +1 -0
  39. package/dist/chunk-MOL7MDBG.js +0 -0
  40. package/dist/{chunk-XU4VWOKL.js → chunk-NMWXBJ5B.js} +4 -3
  41. package/dist/chunk-NMWXBJ5B.js.map +1 -0
  42. package/dist/{chunk-DZIXKL7E.js → chunk-O5EK52TG.js} +1 -1
  43. package/dist/chunk-O5EK52TG.js.map +1 -0
  44. package/dist/{chunk-KTPZYXN2.js → chunk-SPKBIFSK.js} +2 -2
  45. package/dist/chunk-SPKBIFSK.js.map +1 -0
  46. package/dist/{chunk-STYN45MS.js → chunk-US4WFSIQ.js} +4 -4
  47. package/dist/chunk-US4WFSIQ.js.map +1 -0
  48. package/dist/{chunk-WTT2KJOM.js → chunk-VXMLENCV.js} +14 -10
  49. package/dist/chunk-VXMLENCV.js.map +1 -0
  50. package/dist/{chunk-7FCNOKOR.js → chunk-W34QSJFF.js} +1 -1
  51. package/dist/chunk-W34QSJFF.js.map +1 -0
  52. package/dist/{chunk-YR4PF56B.js → chunk-WFOQFJI4.js} +2 -2
  53. package/dist/chunk-WFOQFJI4.js.map +1 -0
  54. package/dist/{chunk-NNDVFNMJ.js → chunk-YNJDNP44.js} +1 -1
  55. package/dist/chunk-YNJDNP44.js.map +1 -0
  56. package/dist/{chunk-LM7C6HI2.js → chunk-ZSL6MQVE.js} +1 -1
  57. package/dist/chunk-ZSL6MQVE.js.map +1 -0
  58. package/dist/chunks-Q7T4SQGW.js +10 -0
  59. package/dist/cli.js +75 -39
  60. package/dist/cli.js.map +1 -1
  61. package/dist/{formula-SXC6LRRC.js → formula-7ZPC54JO.js} +2 -2
  62. package/dist/formula-7ZPC54JO.js.map +1 -0
  63. package/dist/{formula-KSNAEBZ2.js → formula-EQRHSMYW.js} +2 -2
  64. package/dist/formula-EQRHSMYW.js.map +1 -0
  65. package/dist/{formula-SB3NHDUM.cjs → formula-IWV5IJ3Y.cjs} +8 -8
  66. package/dist/formula-IWV5IJ3Y.cjs.map +1 -0
  67. package/dist/{image-ocr-PS7XCV6K.js → image-ocr-G2LYTEUH.js} +8 -6
  68. package/dist/image-ocr-G2LYTEUH.js.map +1 -0
  69. package/dist/{image-ocr-KZTNJ62U.js → image-ocr-R44RYDBU.js} +10 -8
  70. package/dist/image-ocr-R44RYDBU.js.map +1 -0
  71. package/dist/{image-ocr-J5656BEE.cjs → image-ocr-XEJFXH6M.cjs} +13 -11
  72. package/dist/image-ocr-XEJFXH6M.cjs.map +1 -0
  73. package/dist/index.cjs +1993 -1845
  74. package/dist/index.cjs.map +1 -1
  75. package/dist/index.d.cts +75 -4
  76. package/dist/index.d.ts +75 -4
  77. package/dist/index.js +2128 -1980
  78. package/dist/index.js.map +1 -1
  79. package/dist/mcp.js +123 -78
  80. package/dist/mcp.js.map +1 -1
  81. package/dist/{models-KA4GLQK5.js → models-6VMAVZR6.js} +3 -3
  82. package/dist/page-range-737B4EZW.js +0 -0
  83. package/dist/page-range-P7SDW6LR.cjs.map +1 -1
  84. package/dist/{parser-A34NAYKA.js → parser-2G245MMN.js} +6 -6
  85. package/dist/parser-2G245MMN.js.map +1 -0
  86. package/dist/{parser-WNFWMBSL.js → parser-W3POOYQQ.js} +8 -8
  87. package/dist/parser-W3POOYQQ.js.map +1 -0
  88. package/dist/{parser-CSYU3CAW.cjs → parser-W3ZGQUCQ.cjs} +25 -25
  89. package/dist/parser-W3ZGQUCQ.cjs.map +1 -0
  90. package/dist/pdf-ocr-3GDTJSHF.js +15 -0
  91. package/dist/pdf-ocr-N7IU23UV.cjs +13 -0
  92. package/dist/pdf-ocr-N7IU23UV.cjs.map +1 -0
  93. package/dist/pdf-ocr-U7MEGNBZ.js +12 -0
  94. package/dist/{profile-io-43MF52H6.js → profile-io-72R3B7KO.js} +3 -3
  95. package/dist/{rasterize-OG7KXBJS.js → rasterize-GNJQVURE.js} +2 -2
  96. package/dist/rasterize-GNJQVURE.js.map +1 -0
  97. package/dist/{redact-7ILEAUTS.js → redact-W5K3ILQH.js} +2 -2
  98. package/dist/render-6OW6U4L4.js +10 -0
  99. package/dist/seal-GTNLVBJX.js +10 -0
  100. package/dist/{setup-GUQT5ELJ.js → setup-I4I75QJU.js} +1 -1
  101. package/dist/setup-I4I75QJU.js.map +1 -0
  102. package/dist/{watch-SOHQMZN2.js → watch-2ADAJLIJ.js} +12 -14
  103. package/dist/watch-2ADAJLIJ.js.map +1 -0
  104. package/package.json +101 -98
  105. package/templates//352/260/204/354/235/264/352/270/260/354/225/210/353/254/270_/354/204/234/354/213/235.hwpx +0 -0
  106. package/templates//352/260/204/354/235/264/352/270/260/354/225/210/353/254/270_/354/230/210/354/213/234/352/260/222.json +9 -0
  107. package/templates//354/235/274/353/260/230/352/270/260/354/225/210/353/254/270_/354/204/234/354/213/235.hwpx +0 -0
  108. package/templates//354/235/274/353/260/230/352/270/260/354/225/210/353/254/270_/354/230/210/354/213/234/352/260/222.json +25 -0
  109. package/dist/chunk-27Z4PUZZ.js.map +0 -1
  110. package/dist/chunk-3PLF5UIS.js.map +0 -1
  111. package/dist/chunk-3TSTZGCJ.js.map +0 -1
  112. package/dist/chunk-4ZWKFDUC.cjs.map +0 -1
  113. package/dist/chunk-5RPYBC2Q.js.map +0 -1
  114. package/dist/chunk-6GM2QLM3.cjs.map +0 -1
  115. package/dist/chunk-7FCNOKOR.js.map +0 -1
  116. package/dist/chunk-7ZARSBQ3.js.map +0 -1
  117. package/dist/chunk-DZIXKL7E.js.map +0 -1
  118. package/dist/chunk-G3DMDTZI.js.map +0 -1
  119. package/dist/chunk-GEPINOL6.js.map +0 -1
  120. package/dist/chunk-GZ7W64QU.cjs.map +0 -1
  121. package/dist/chunk-H7OWEYH2.cjs.map +0 -1
  122. package/dist/chunk-J2RZ444H.js.map +0 -1
  123. package/dist/chunk-KTPZYXN2.js.map +0 -1
  124. package/dist/chunk-LM7C6HI2.js.map +0 -1
  125. package/dist/chunk-LXE7OR6N.js.map +0 -1
  126. package/dist/chunk-MHMVFI2N.js.map +0 -1
  127. package/dist/chunk-NIISBGSR.js.map +0 -1
  128. package/dist/chunk-NNDVFNMJ.js.map +0 -1
  129. package/dist/chunk-PATPZTH7.cjs.map +0 -1
  130. package/dist/chunk-STYN45MS.js.map +0 -1
  131. package/dist/chunk-TCJRGFYY.js +0 -280
  132. package/dist/chunk-TCJRGFYY.js.map +0 -1
  133. package/dist/chunk-VEHC4EJL.js.map +0 -1
  134. package/dist/chunk-WTT2KJOM.js.map +0 -1
  135. package/dist/chunk-XU4VWOKL.js.map +0 -1
  136. package/dist/chunk-YR4PF56B.js.map +0 -1
  137. package/dist/chunks-VFIMYK36.js +0 -10
  138. package/dist/detect-FX2ZHKZP.js +0 -26
  139. package/dist/formula-KSNAEBZ2.js.map +0 -1
  140. package/dist/formula-SB3NHDUM.cjs.map +0 -1
  141. package/dist/formula-SXC6LRRC.js.map +0 -1
  142. package/dist/image-ocr-J5656BEE.cjs.map +0 -1
  143. package/dist/image-ocr-KZTNJ62U.js.map +0 -1
  144. package/dist/image-ocr-PS7XCV6K.js.map +0 -1
  145. package/dist/parser-A34NAYKA.js.map +0 -1
  146. package/dist/parser-CSYU3CAW.cjs.map +0 -1
  147. package/dist/parser-WNFWMBSL.js.map +0 -1
  148. package/dist/pdf-ocr-KBMWPG2L.js +0 -12
  149. package/dist/pdf-ocr-NPZG67FN.js +0 -15
  150. package/dist/pdf-ocr-O44DAGTY.cjs +0 -13
  151. package/dist/pdf-ocr-O44DAGTY.cjs.map +0 -1
  152. package/dist/rasterize-OG7KXBJS.js.map +0 -1
  153. package/dist/render-336CJHAS.js +0 -10
  154. package/dist/seal-TM2XCE2R.js +0 -10
  155. package/dist/seal-TM2XCE2R.js.map +0 -1
  156. package/dist/setup-GUQT5ELJ.js.map +0 -1
  157. package/dist/watch-SOHQMZN2.js.map +0 -1
  158. /package/dist/{-ECUCTQTR.js.map → -DYHT3HWH.js.map} +0 -0
  159. /package/dist/{chunks-VFIMYK36.js.map → chunks-Q7T4SQGW.js.map} +0 -0
  160. /package/dist/{detect-FX2ZHKZP.js.map → models-6VMAVZR6.js.map} +0 -0
  161. /package/dist/{models-KA4GLQK5.js.map → pdf-ocr-3GDTJSHF.js.map} +0 -0
  162. /package/dist/{pdf-ocr-KBMWPG2L.js.map → pdf-ocr-U7MEGNBZ.js.map} +0 -0
  163. /package/dist/{pdf-ocr-NPZG67FN.js.map → profile-io-72R3B7KO.js.map} +0 -0
  164. /package/dist/{profile-io-43MF52H6.js.map → redact-W5K3ILQH.js.map} +0 -0
  165. /package/dist/{redact-7ILEAUTS.js.map → render-6OW6U4L4.js.map} +0 -0
  166. /package/dist/{render-336CJHAS.js.map → seal-GTNLVBJX.js.map} +0 -0
@@ -1,18 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  inlineImagesIntoMarkdown
4
- } from "./chunk-7FCNOKOR.js";
4
+ } from "./chunk-W34QSJFF.js";
5
5
  import {
6
6
  HEADING_RATIO_H1,
7
7
  HEADING_RATIO_H2,
8
8
  HEADING_RATIO_H3
9
- } from "./chunk-LM7C6HI2.js";
10
- import {
11
- detectFormat,
12
- detectOle2Format,
13
- detectZipFormat,
14
- parseLenientCfb
15
- } from "./chunk-TCJRGFYY.js";
9
+ } from "./chunk-ZSL6MQVE.js";
16
10
  import {
17
11
  parsePageRange
18
12
  } from "./chunk-MOL7MDBG.js";
@@ -21,13 +15,14 @@ import {
21
15
  applySplices,
22
16
  buildParagraphSplices,
23
17
  buildRangeSplices,
18
+ decodeXmlEntities,
24
19
  escapeXmlText,
25
20
  findElementEnd,
26
21
  paraTText,
27
22
  paraTextPureT,
28
23
  patchZipEntries,
29
24
  scanSectionXml
30
- } from "./chunk-XU4VWOKL.js";
25
+ } from "./chunk-NMWXBJ5B.js";
31
26
  import {
32
27
  MAX_COLS,
33
28
  MAX_ROWS,
@@ -37,26 +32,30 @@ import {
37
32
  dedupeRunningHeaders,
38
33
  flattenLayoutTables,
39
34
  mapPuaText
40
- } from "./chunk-MHMVFI2N.js";
35
+ } from "./chunk-GSMPTZC7.js";
41
36
  import {
42
37
  SPACE_EM_FIXED,
43
38
  charWidthEm1000,
44
39
  fitRatioForFewerLines,
45
40
  measureTextWidth,
46
41
  simulateWrap
47
- } from "./chunk-WTT2KJOM.js";
42
+ } from "./chunk-VXMLENCV.js";
48
43
  import {
49
44
  MAX_DECOMPRESS_SIZE,
50
45
  MAX_XML_DEPTH,
51
46
  MAX_ZIP_ENTRIES,
52
47
  ZipBombError,
53
48
  applyPageText,
49
+ childrenByLocalName,
54
50
  clampSpan,
55
51
  createSectionShared,
56
52
  createXmlParser,
53
+ elementChildren,
57
54
  extractTextFromNode,
58
- findChildByLocalName
59
- } from "./chunk-NIISBGSR.js";
55
+ findChildByLocalName,
56
+ localName,
57
+ rawTextContent
58
+ } from "./chunk-EFVAY5VE.js";
60
59
  import {
61
60
  KordocError,
62
61
  classifyError,
@@ -68,13 +67,277 @@ import {
68
67
  sanitizeHref,
69
68
  stripDtd,
70
69
  toArrayBuffer
71
- } from "./chunk-3TSTZGCJ.js";
70
+ } from "./chunk-DRQUAYNT.js";
72
71
 
73
72
  // src/index.ts
74
73
  import { readFile } from "fs/promises";
75
74
 
75
+ // src/detect.ts
76
+ import JSZip from "jszip";
77
+
78
+ // src/hwp5/cfb-lenient.ts
79
+ var CFB_MAGIC = Buffer.from([208, 207, 17, 224, 161, 177, 26, 225]);
80
+ var END_OF_CHAIN = 4294967294;
81
+ var FREE_SECT = 4294967295;
82
+ var MAX_CHAIN_LENGTH = 1e6;
83
+ var MAX_DIR_ENTRIES = 1e5;
84
+ var MAX_STREAM_SIZE = 100 * 1024 * 1024;
85
+ function parseLenientCfb(data) {
86
+ if (data.length < 512) throw new Error("CFB \uD30C\uC77C\uC774 \uB108\uBB34 \uC9E7\uC2B5\uB2C8\uB2E4 (\uCD5C\uC18C 512\uBC14\uC774\uD2B8)");
87
+ if (!data.subarray(0, 8).equals(CFB_MAGIC)) throw new Error("CFB \uB9E4\uC9C1 \uBC14\uC774\uD2B8 \uBD88\uC77C\uCE58");
88
+ const sectorSizeShift = data.readUInt16LE(30);
89
+ if (sectorSizeShift < 7 || sectorSizeShift > 16) throw new Error("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC139\uD130 \uD06C\uAE30 \uC2DC\uD504\uD2B8: " + sectorSizeShift);
90
+ const sectorSize = 1 << sectorSizeShift;
91
+ const miniSectorSizeShift = data.readUInt16LE(32);
92
+ if (miniSectorSizeShift > 16) throw new Error("\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uBBF8\uB2C8 \uC139\uD130 \uD06C\uAE30 \uC2DC\uD504\uD2B8: " + miniSectorSizeShift);
93
+ const miniSectorSize = 1 << miniSectorSizeShift;
94
+ const headerFatSectorCount = data.readUInt32LE(44);
95
+ if (headerFatSectorCount > 1e4) throw new Error("FAT \uC139\uD130 \uC218\uAC00 \uB108\uBB34 \uB9CE\uC2B5\uB2C8\uB2E4: " + headerFatSectorCount);
96
+ const maxRealSectors = Math.ceil(Math.max(0, data.length - 512) / sectorSize);
97
+ const maxFatSectors = Math.max(1, Math.ceil(maxRealSectors / (sectorSize / 4)));
98
+ const fatSectorCount = Math.min(headerFatSectorCount, maxFatSectors);
99
+ const firstDirSector = data.readUInt32LE(48);
100
+ const miniStreamCutoff = data.readUInt32LE(56);
101
+ const firstMiniFatSector = data.readUInt32LE(60);
102
+ const miniFatSectorCount = data.readUInt32LE(64);
103
+ const firstDifatSector = data.readUInt32LE(68);
104
+ const difatSectorCount = data.readUInt32LE(72);
105
+ function sectorOffset(id) {
106
+ return 512 + id * sectorSize;
107
+ }
108
+ function readSectorData(id) {
109
+ const off = sectorOffset(id);
110
+ if (off + sectorSize > data.length) return Buffer.alloc(0);
111
+ return data.subarray(off, off + sectorSize);
112
+ }
113
+ const fatSectors = [];
114
+ for (let i = 0; i < 109 && fatSectors.length < fatSectorCount; i++) {
115
+ const sid = data.readUInt32LE(76 + i * 4);
116
+ if (sid === FREE_SECT || sid === END_OF_CHAIN) break;
117
+ fatSectors.push(sid);
118
+ }
119
+ let difatSector = firstDifatSector;
120
+ const visitedDifat = /* @__PURE__ */ new Set();
121
+ for (let d = 0; d < difatSectorCount && difatSector !== END_OF_CHAIN && difatSector !== FREE_SECT; d++) {
122
+ if (visitedDifat.has(difatSector)) break;
123
+ visitedDifat.add(difatSector);
124
+ const buf = readSectorData(difatSector);
125
+ if (buf.length < sectorSize) break;
126
+ const entriesPerSector = sectorSize / 4 - 1;
127
+ for (let i = 0; i < entriesPerSector && fatSectors.length < fatSectorCount; i++) {
128
+ const sid = buf.readUInt32LE(i * 4);
129
+ if (sid === FREE_SECT || sid === END_OF_CHAIN) continue;
130
+ fatSectors.push(sid);
131
+ }
132
+ difatSector = buf.readUInt32LE(entriesPerSector * 4);
133
+ }
134
+ const entriesPerFatSector = sectorSize / 4;
135
+ const fatTable = new Uint32Array(fatSectors.length * entriesPerFatSector);
136
+ for (let fi = 0; fi < fatSectors.length; fi++) {
137
+ const buf = readSectorData(fatSectors[fi]);
138
+ for (let i = 0; i < entriesPerFatSector; i++) {
139
+ fatTable[fi * entriesPerFatSector + i] = i * 4 + 3 < buf.length ? buf.readUInt32LE(i * 4) : FREE_SECT;
140
+ }
141
+ }
142
+ function readChain(startSector, maxBytes) {
143
+ if (startSector === END_OF_CHAIN || startSector === FREE_SECT) return Buffer.alloc(0);
144
+ if (maxBytes > MAX_STREAM_SIZE) throw new Error("\uC2A4\uD2B8\uB9BC\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4");
145
+ const chunks = [];
146
+ let current = startSector;
147
+ let totalRead = 0;
148
+ const visited = /* @__PURE__ */ new Set();
149
+ while (current !== END_OF_CHAIN && current !== FREE_SECT && totalRead < maxBytes) {
150
+ if (visited.has(current)) break;
151
+ if (visited.size > MAX_CHAIN_LENGTH) break;
152
+ visited.add(current);
153
+ const buf = readSectorData(current);
154
+ const remaining = maxBytes - totalRead;
155
+ chunks.push(remaining < sectorSize ? buf.subarray(0, remaining) : buf);
156
+ totalRead += Math.min(buf.length, remaining);
157
+ current = current < fatTable.length ? fatTable[current] : END_OF_CHAIN;
158
+ }
159
+ return Buffer.concat(chunks);
160
+ }
161
+ let miniFatTable = null;
162
+ function getMiniFatTable() {
163
+ if (miniFatTable) return miniFatTable;
164
+ if (miniFatSectorCount === 0 || firstMiniFatSector === END_OF_CHAIN) {
165
+ miniFatTable = new Uint32Array(0);
166
+ return miniFatTable;
167
+ }
168
+ const miniFatData = readChain(firstMiniFatSector, miniFatSectorCount * sectorSize);
169
+ const entries = miniFatData.length / 4;
170
+ miniFatTable = new Uint32Array(entries);
171
+ for (let i = 0; i < entries; i++) {
172
+ miniFatTable[i] = miniFatData.readUInt32LE(i * 4);
173
+ }
174
+ return miniFatTable;
175
+ }
176
+ const dirData = readChain(firstDirSector, MAX_DIR_ENTRIES * 128);
177
+ const dirEntries = [];
178
+ for (let offset = 0; offset + 128 <= dirData.length && dirEntries.length < MAX_DIR_ENTRIES; offset += 128) {
179
+ const nameLen = dirData.readUInt16LE(offset + 64);
180
+ if (nameLen <= 0 || nameLen > 64) {
181
+ dirEntries.push({ name: "", type: 0, startSector: 0, size: 0 });
182
+ continue;
183
+ }
184
+ const nameBytes = nameLen - 2;
185
+ const name = nameBytes > 0 ? dirData.subarray(offset, offset + nameBytes).toString("utf16le") : "";
186
+ const type = dirData[offset + 66];
187
+ const startSector = dirData.readUInt32LE(offset + 116);
188
+ const size = dirData.readUInt32LE(offset + 120);
189
+ dirEntries.push({ name, type, startSector, size });
190
+ }
191
+ let miniStreamData = null;
192
+ function getMiniStream() {
193
+ if (miniStreamData) return miniStreamData;
194
+ const root = dirEntries[0];
195
+ if (!root || root.type !== 5) {
196
+ miniStreamData = Buffer.alloc(0);
197
+ return miniStreamData;
198
+ }
199
+ miniStreamData = readChain(root.startSector, root.size || MAX_STREAM_SIZE);
200
+ return miniStreamData;
201
+ }
202
+ function readMiniStream(startSector, size) {
203
+ const mft = getMiniFatTable();
204
+ const ms = getMiniStream();
205
+ if (mft.length === 0 || ms.length === 0) return Buffer.alloc(0);
206
+ const chunks = [];
207
+ let current = startSector;
208
+ let totalRead = 0;
209
+ const visited = /* @__PURE__ */ new Set();
210
+ while (current !== END_OF_CHAIN && current !== FREE_SECT && totalRead < size) {
211
+ if (visited.has(current)) break;
212
+ if (visited.size > MAX_CHAIN_LENGTH) break;
213
+ visited.add(current);
214
+ const off = current * miniSectorSize;
215
+ const remaining = size - totalRead;
216
+ const chunkSize = Math.min(miniSectorSize, remaining);
217
+ if (off + chunkSize <= ms.length) {
218
+ chunks.push(ms.subarray(off, off + chunkSize));
219
+ }
220
+ totalRead += chunkSize;
221
+ current = current < mft.length ? mft[current] : END_OF_CHAIN;
222
+ }
223
+ return Buffer.concat(chunks);
224
+ }
225
+ function readStreamData(entry) {
226
+ if (entry.size === 0) return Buffer.alloc(0);
227
+ if (entry.size < miniStreamCutoff) {
228
+ const miniResult = readMiniStream(entry.startSector, entry.size);
229
+ if (miniResult.length > 0) return miniResult;
230
+ }
231
+ return readChain(entry.startSector, entry.size);
232
+ }
233
+ function findEntryByPath(path) {
234
+ const parts = path.replace(/^\//, "").split("/");
235
+ if (parts.length === 1) {
236
+ return dirEntries.find((e) => e.name === parts[0] && e.type === 2) ?? null;
237
+ }
238
+ const storageName = parts[0];
239
+ const streamName = parts.slice(1).join("/");
240
+ for (const e of dirEntries) {
241
+ if (e.type === 2 && e.name === streamName) {
242
+ return e;
243
+ }
244
+ }
245
+ const lastPart = parts[parts.length - 1];
246
+ return dirEntries.find((e) => e.type === 2 && e.name === lastPart) ?? null;
247
+ }
248
+ return {
249
+ findStream(path) {
250
+ const normalized = path.replace(/^\//, "");
251
+ const entry = findEntryByPath(normalized);
252
+ if (!entry || entry.type !== 2) return null;
253
+ const stream = readStreamData(entry);
254
+ return stream.length > 0 ? stream : null;
255
+ },
256
+ entries() {
257
+ return dirEntries.filter((e) => e.type === 2);
258
+ }
259
+ };
260
+ }
261
+
262
+ // src/detect.ts
263
+ function magicBytes(buffer) {
264
+ return new Uint8Array(buffer, 0, Math.min(4, buffer.byteLength));
265
+ }
266
+ function isZipFile(buffer) {
267
+ const b = magicBytes(buffer);
268
+ return b[0] === 80 && b[1] === 75 && b[2] === 3 && b[3] === 4;
269
+ }
270
+ function isHwpxFile(buffer) {
271
+ return isZipFile(buffer);
272
+ }
273
+ function isOldHwpFile(buffer) {
274
+ const b = magicBytes(buffer);
275
+ return b[0] === 208 && b[1] === 207 && b[2] === 17 && b[3] === 224;
276
+ }
277
+ var HWP3_PREFIX = new TextEncoder().encode("HWP Document File V3.00");
278
+ function isHwp3File(buffer) {
279
+ if (buffer.byteLength < HWP3_PREFIX.length) return false;
280
+ const head = new Uint8Array(buffer, 0, HWP3_PREFIX.length);
281
+ for (let i = 0; i < HWP3_PREFIX.length; i++) {
282
+ if (head[i] !== HWP3_PREFIX[i]) return false;
283
+ }
284
+ return true;
285
+ }
286
+ function isPdfFile(buffer) {
287
+ const b = magicBytes(buffer);
288
+ return b[0] === 37 && b[1] === 80 && b[2] === 68 && b[3] === 70;
289
+ }
290
+ function isImageFile(buffer) {
291
+ if (buffer.byteLength < 12) return false;
292
+ const b = new Uint8Array(buffer, 0, 12);
293
+ if (b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) return true;
294
+ if (b[0] === 255 && b[1] === 216 && b[2] === 255) return true;
295
+ if (b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80) return true;
296
+ return false;
297
+ }
298
+ function isHwpmlFile(buffer) {
299
+ const bytes = new Uint8Array(buffer, 0, Math.min(512, buffer.byteLength));
300
+ const head = new TextDecoder("utf-8", { fatal: false }).decode(bytes).replace(/^\uFEFF/, "");
301
+ return head.trimStart().startsWith("<?xml") && head.includes("<HWPML");
302
+ }
303
+ function detectFormat(buffer) {
304
+ if (buffer.byteLength < 4) return "unknown";
305
+ if (isHwp3File(buffer)) return "hwp3";
306
+ if (isZipFile(buffer)) return "hwpx";
307
+ if (isOldHwpFile(buffer)) return "hwp";
308
+ if (isPdfFile(buffer)) return "pdf";
309
+ if (isHwpmlFile(buffer)) return "hwpml";
310
+ if (isImageFile(buffer)) return "image";
311
+ return "unknown";
312
+ }
313
+ function detectOle2Format(buffer) {
314
+ try {
315
+ const cfb = parseLenientCfb(Buffer.from(buffer));
316
+ const names = cfb.entries().map((e) => e.name);
317
+ if (names.includes("Workbook") || names.includes("Book")) return "xls";
318
+ if (names.includes("FileHeader")) return "hwp";
319
+ if (names.some((n) => n === "DocInfo" || n.startsWith("Section"))) return "hwp";
320
+ return "unknown";
321
+ } catch {
322
+ return "unknown";
323
+ }
324
+ }
325
+ async function detectZipFormat(buffer) {
326
+ try {
327
+ const zip = await JSZip.loadAsync(buffer);
328
+ if (zip.file("xl/workbook.xml")) return "xlsx";
329
+ if (zip.file("word/document.xml")) return "docx";
330
+ if (zip.file("Contents/content.hpf") || zip.file("mimetype")) return "hwpx";
331
+ const hasSection = Object.keys(zip.files).some((f) => f.startsWith("Contents/"));
332
+ if (hasSection) return "hwpx";
333
+ return "unknown";
334
+ } catch {
335
+ return "unknown";
336
+ }
337
+ }
338
+
76
339
  // src/hwpx/parser.ts
77
- import JSZip2 from "jszip";
340
+ import JSZip3 from "jszip";
78
341
 
79
342
  // src/hwpx/com-fallback.ts
80
343
  import { execFileSync } from "child_process";
@@ -261,9 +524,9 @@ function parseCharProperties(doc, map) {
261
524
  if (bold === "true" || bold === "1") prop.bold = true;
262
525
  const italic = el.getAttribute("italic");
263
526
  if (italic === "true" || italic === "1") prop.italic = true;
264
- const kids2 = el.childNodes;
265
- for (let j = 0; j < (kids2?.length ?? 0); j++) {
266
- const k = kids2[j];
527
+ const kids = el.childNodes;
528
+ for (let j = 0; j < (kids?.length ?? 0); j++) {
529
+ const k = kids[j];
267
530
  if (k.nodeType !== 1) continue;
268
531
  const localTag = (k.tagName || "").replace(/^[^:]+:/, "");
269
532
  if (localTag === "bold") prop.bold = true;
@@ -895,615 +1158,239 @@ function hmlToLatex(hmlEqStr) {
895
1158
  return out;
896
1159
  }
897
1160
 
898
- // src/hwpx/geometry.ts
899
- var A4_W_HU = 59528;
900
- var A4_H_HU = 84188;
901
- var HU_PER_MM = 7200 / 25.4;
902
- function mmToHwpunit(mm) {
903
- return Math.round(mm * 7200 / 25.4);
904
- }
905
- var BODY_WIDTH_20MM = mmToHwpunit(170);
906
- var COVER_MEASURED_W = 48180;
907
- var DATA_TABLE_ID_BASE = 1e3;
908
- var CHART_TABLE_ID_BASE = 91e5;
909
- var GJ_TABLE_ID_BASE = 92e5;
910
- var EXTRA_TABLE_ID_BASE = 93e5;
911
- var PIC_ID_BASE = 94e5;
912
-
913
- // src/hwpx/gaejosik.ts
914
- var GAEJOSIK_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
915
- function gaejosikMarker(depth, bullet2 = "\u25CB") {
916
- if (depth === 1) return bullet2;
917
- return GAEJOSIK_BULLETS[Math.min(depth, GAEJOSIK_BULLETS.length - 1)];
918
- }
919
- function gaejosikSizes(bodyHeight, ov = {}) {
920
- const s = bodyHeight / 1500;
921
- const r = (v, pt) => pt ? Math.round(pt * 100) : Math.round(v * s);
922
- return {
923
- /** □ 대항목 — HY헤드라인M 16pt */
924
- dae: r(1600, ov.dae),
925
- /** ※ 참고 — 한양중고딕 13pt */
926
- cham: r(1300, ov.cham),
927
- /** 장 헤더(로마숫자·제목) 17pt */
928
- chapter: r(1700, ov.chapter),
929
- /** 표지 제목 30pt */
930
- coverTitle: r(3e3, ov.coverTitle),
931
- /** 표지 날짜·기관명 25pt */
932
- coverSub: r(2500, ov.coverSub),
933
- /** 목차 라벨 "목 차" 28pt */
934
- tocLabel: r(2800, ov.tocLabel),
935
- /** 목차 로마숫자 21pt(한양신명조) */
936
- tocRoman: r(2100, ov.tocRoman),
937
- /** 목차 항목 18pt */
938
- tocItem: r(1800, ov.tocItem),
939
- /** 표 셀 — 맑은 고딕 12pt (실측: 정부 양식 표) */
940
- table: r(1200, ov.table),
941
- /** 표지 장식 바 셀 빈 문단 — 6pt (실측: 바 높이 818 안에 줄높이 수납) */
942
- bar: r(600),
943
- /** 본문 첫 페이지 제목 박스 — HY헤드라인M 22pt (실측: GT3 표④) */
944
- bodyTitle: r(2200, ov.bodyTitle)
945
- };
946
- }
947
- function gaejosikSpaceBefore(depth, bodyHeight) {
948
- const s = bodyHeight / 1500;
949
- const ui = [3e3, 2e3, 1200, 600][Math.min(depth, 3)];
950
- return Math.round(ui * s);
951
- }
952
- function gaejosikLevelIndent(depth, bodyHeight, sizes = {}, bullet2 = "\u25CB") {
953
- const lefts = [0, 1, 1.5, 2];
954
- const left = depth <= 3 ? Math.round(lefts[depth] * bodyHeight) : Math.round((2 + (depth - 3) * 0.5) * bodyHeight);
955
- const h = depth === 0 ? gaejosikSizes(bodyHeight, sizes).dae : bodyHeight;
956
- return { left, indent: -markerWidth(gaejosikMarker(depth, bullet2), h) };
1161
+ // src/shared/numbering.ts
1162
+ var HANGUL_INITIALS = [0, 2, 3, 5, 6, 7, 9, 11, 12, 14, 15, 16, 17, 18];
1163
+ var HANGUL_MEDIALS = [0, 4, 8, 13, 18, 20];
1164
+ function hangulOrdinal(n) {
1165
+ const cols = HANGUL_INITIALS.length;
1166
+ const vowel = HANGUL_MEDIALS[Math.min(Math.floor(n / cols), HANGUL_MEDIALS.length - 1)];
1167
+ const init = HANGUL_INITIALS[n % cols];
1168
+ return String.fromCodePoint(44032 + init * 588 + vowel * 28);
957
1169
  }
958
- function gaejosikChamIndent(bodyHeight, sizes = {}) {
959
- const cham = gaejosikSizes(bodyHeight, sizes).cham;
960
- return { left: Math.round(2.5 * cham), indent: -markerWidth("\u203B", cham) };
1170
+ function circledNumber(n) {
1171
+ if (n < 20) return String.fromCodePoint(9312 + n);
1172
+ if (n < 35) return String.fromCodePoint(12881 + (n - 20));
1173
+ if (n < 50) return String.fromCodePoint(12977 + (n - 35));
1174
+ return `(${n + 1})`;
961
1175
  }
962
- function gaejosikTocItemIndent(bodyHeight, sizes = {}) {
963
- const sz = gaejosikSizes(bodyHeight, sizes);
964
- const hang = markerWidth("\u2167.", sz.tocRoman);
965
- return { left: 2e3 + hang, indent: -hang };
1176
+ function circledHangul(n) {
1177
+ return n < 14 ? String.fromCodePoint(12910 + n) : hangulOrdinal(n);
966
1178
  }
967
- function formatGaejosikDate(d) {
968
- return `${d.getFullYear()}. ${d.getMonth() + 1}. ${d.getDate()}.`;
1179
+ function romanNumeral(n, upper) {
1180
+ if (n <= 0 || n > 3999) return String(n);
1181
+ const values = [1e3, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
1182
+ const symbols = upper ? ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"] : ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"];
1183
+ let result = "";
1184
+ let num2 = n;
1185
+ for (let i = 0; i < values.length; i++) {
1186
+ while (num2 >= values[i]) {
1187
+ result += symbols[i];
1188
+ num2 -= values[i];
1189
+ }
1190
+ }
1191
+ return result;
969
1192
  }
970
- function stripChapterNumber(title) {
971
- return title.replace(/^\s*[□○ㅇㆍ-]\s+/u, "").replace(/^\s*(?:\d+(?:\.\d+)*|[Ⅰ-Ⅻ]+)\s*[.)]\s*/u, "").trim();
1193
+
1194
+ // src/hwpx/para-heading.ts
1195
+ var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
1196
+ function formatHeadNumber(n, numFormat) {
1197
+ if (n === 0 && numFormat === "DIGIT") return "0";
1198
+ if (n <= 0) n = 1;
1199
+ switch (numFormat) {
1200
+ case "DIGIT":
1201
+ return String(n);
1202
+ // 서수 시퀀스는 생성기(gongmun.ts)와 단일 소스 (v4.0.5) — 종전 mod-14 순환은
1203
+ // 15번째 형제를 다시 '가'로 만들어 한글 실렌더(가→하→거→너 단모음 연속)와 어긋났다
1204
+ case "CIRCLED_DIGIT":
1205
+ return n <= 50 ? circledNumber(n - 1) : `(${n})`;
1206
+ case "HANGUL_SYLLABLE":
1207
+ return hangulOrdinal(n - 1);
1208
+ case "CIRCLED_HANGUL_SYLLABLE":
1209
+ return n <= 14 ? circledHangul(n - 1) : hangulOrdinal(n - 1);
1210
+ case "HANGUL_JAMO":
1211
+ return HANGUL_JAMO_SEQ[(n - 1) % HANGUL_JAMO_SEQ.length];
1212
+ case "CIRCLED_HANGUL_JAMO":
1213
+ return n <= 14 ? String.fromCodePoint(12896 + n - 1) : HANGUL_JAMO_SEQ[(n - 1) % 14];
1214
+ case "LATIN_CAPITAL":
1215
+ return String.fromCharCode(65 + (n - 1) % 26);
1216
+ case "LATIN_SMALL":
1217
+ return String.fromCharCode(97 + (n - 1) % 26);
1218
+ case "CIRCLED_LATIN_CAPITAL":
1219
+ return n <= 26 ? String.fromCodePoint(9398 + n - 1) : String.fromCharCode(65 + (n - 1) % 26);
1220
+ case "CIRCLED_LATIN_SMALL":
1221
+ return n <= 26 ? String.fromCodePoint(9424 + n - 1) : String.fromCharCode(97 + (n - 1) % 26);
1222
+ case "ROMAN_CAPITAL":
1223
+ return romanNumeral(n, true);
1224
+ case "ROMAN_SMALL":
1225
+ return romanNumeral(n, false);
1226
+ default:
1227
+ return String(n);
1228
+ }
972
1229
  }
973
- var GAEJOSIK_COLORS = {
974
- /** 장헤더 로마숫자 셀·표지 진한 바 음영 */
975
- primary: "#193AAA",
976
- /** 장헤더 테두리 */
977
- border: "#006699",
978
- /** 표지 연한 */
979
- accent: "#A0B4E6",
980
- /** 장헤더 제목 음영 */
981
- titleFill: "#F2F2F2",
982
- /** 장헤더 제목 셀 상하선 */
983
- titleLine: "#A6A6A6",
984
- /** 목차 박스 테두리 */
985
- tocBorder: "#514BAC",
986
- /** 목차 배너 라벤더 스트라이프 (실측: GT3 표②) */
987
- tocStripe: "#E0E5FA"
988
- };
989
- var GAEJOSIK_BASE_WIDTH = BODY_WIDTH_20MM;
990
- var CHAPTER_GEOM = { numW: 3327, gapW: 848, titleW: 43513, rowH: 2832 };
991
- var COVER_GEOM = {
992
- totalW: COVER_MEASURED_W,
993
- topDarkW: 38219,
994
- topLightW: 9961,
995
- barH: 818,
996
- titleH: 11060,
997
- botDarkW: 10466,
998
- botLightW: 37714,
999
- botBarH: 812
1000
- };
1001
- var TOC_GEOM = { boxW: 46492, boxH: 57840 };
1002
- function chapterGeom(bodyHeight, ov = {}, bodyWidth = GAEJOSIK_BASE_WIDTH) {
1003
- const s = gaejosikSizes(bodyHeight, ov).chapter / 1700;
1004
- const ws = bodyWidth / GAEJOSIK_BASE_WIDTH;
1005
- const numW = Math.round(CHAPTER_GEOM.numW * ws);
1006
- const gapW = Math.round(CHAPTER_GEOM.gapW * ws);
1007
- const titleW = Math.round((CHAPTER_GEOM.numW + CHAPTER_GEOM.gapW + CHAPTER_GEOM.titleW) * ws) - numW - gapW;
1008
- return { numW, gapW, titleW, rowH: Math.round(CHAPTER_GEOM.rowH * s) };
1009
- }
1010
- function coverGeom(bodyHeight, ov = {}, bodyWidth = GAEJOSIK_BASE_WIDTH) {
1011
- const s = gaejosikSizes(bodyHeight, ov).coverTitle / 3e3;
1012
- const w = (v) => Math.round(v * bodyWidth / GAEJOSIK_BASE_WIDTH);
1013
- const totalW = w(COVER_GEOM.totalW);
1014
- return {
1015
- ...COVER_GEOM,
1016
- totalW,
1017
- topDarkW: w(COVER_GEOM.topDarkW),
1018
- topLightW: totalW - w(COVER_GEOM.topDarkW),
1019
- botDarkW: w(COVER_GEOM.botDarkW),
1020
- botLightW: totalW - w(COVER_GEOM.botDarkW),
1021
- titleH: Math.round(COVER_GEOM.titleH * s)
1022
- };
1023
- }
1024
- var TOC_BANNER_GEOM = {
1025
- colWs: [565, 565, 1414, 13191, 1414, 565, 565],
1026
- h: 3931
1027
- };
1028
- function tocBannerGeom(bodyHeight, ov = {}) {
1029
- const s = gaejosikSizes(bodyHeight, ov).tocLabel / 2800;
1030
- return { colWs: TOC_BANNER_GEOM.colWs, h: Math.round(TOC_BANNER_GEOM.h * s) };
1031
- }
1032
- var BODY_TITLE_GEOM = { barH: 600, titleH: 3566 };
1033
- function bodyTitleGeom(bodyHeight, ov = {}) {
1034
- const s = gaejosikSizes(bodyHeight, ov).bodyTitle / 2200;
1035
- return { barH: BODY_TITLE_GEOM.barH, titleH: Math.round(BODY_TITLE_GEOM.titleH * s) };
1230
+ function resolveParaHeading(paraEl, ctx) {
1231
+ const sm = ctx.styleMap;
1232
+ if (!sm) return null;
1233
+ const styleId = paraEl.getAttribute("styleIDRef");
1234
+ let styleHeading;
1235
+ if (styleId && styleId !== "0") {
1236
+ const st = sm.styles.get(styleId);
1237
+ const m = st && /^개요\s*([1-6])$/.exec(st.name);
1238
+ if (m) styleHeading = Math.min(parseInt(m[1], 10), 6);
1239
+ }
1240
+ const prId = paraEl.getAttribute("paraPrIDRef");
1241
+ if (!prId) return styleHeading ? { headingLevel: styleHeading } : null;
1242
+ const ref = sm.paraHeadings.get(prId);
1243
+ if (!ref) return styleHeading ? { headingLevel: styleHeading } : null;
1244
+ if (ref.type === "BULLET") {
1245
+ const char = sm.bullets.get(ref.idRef);
1246
+ return char ? { prefix: char } : null;
1247
+ }
1248
+ const numId = ref.type === "OUTLINE" ? ctx.outlineNumId || "1" : ref.idRef;
1249
+ const level = Math.min(ref.level + 1, 10);
1250
+ const headingLevel = ref.type === "OUTLINE" ? Math.min(ref.level + 1, 6) : void 0;
1251
+ const numDef = sm.numberings.get(numId);
1252
+ if (!numDef) return headingLevel ? { headingLevel } : null;
1253
+ let counters = ctx.shared.numState.get(numId);
1254
+ if (!counters) {
1255
+ counters = new Array(11).fill(-1);
1256
+ ctx.shared.numState.set(numId, counters);
1257
+ }
1258
+ const head = numDef.heads.get(level);
1259
+ counters[level] = counters[level] < 0 ? head?.start ?? 1 : counters[level] + 1;
1260
+ for (let l = level + 1; l <= 10; l++) counters[l] = -1;
1261
+ const fmtText = head ? head.text.trim() : `^${level}.`;
1262
+ const prefix = fmtText.replace(/\^(10|[1-9])/g, (_, d) => {
1263
+ const lv = parseInt(d, 10);
1264
+ const refHead = numDef.heads.get(lv);
1265
+ const n = counters[lv] >= 0 ? counters[lv] : refHead?.start ?? 1;
1266
+ return formatHeadNumber(n, refHead?.numFormat || "DIGIT");
1267
+ });
1268
+ return { prefix: prefix || void 0, headingLevel };
1036
1269
  }
1037
1270
 
1038
- // src/hwpx/gongmun.ts
1039
- var OFFICIAL_MARGINS = { top: 20, bottom: 15, left: 20, right: 15 };
1040
- var GAEJOSIK_MARGINS = { top: 15, bottom: 15, left: 20, right: 20 };
1041
- var GAEJOSIK_HEADER_FOOTER = 4251;
1042
- var PRESET_DEFAULTS = {
1043
- // 서울시 실결재 104건 중 12pt 64건(13pt 30, 15pt 5) — 지배값을 기본으로 사용.
1044
- official: { bodyPt: 12, lineSpacing: 160, numbering: "standard" },
1045
- report: { bodyPt: 15, lineSpacing: 160, numbering: "report" },
1046
- // 실측 추진계획안: * 계층.
1047
- plan: { bodyPt: 15, lineSpacing: 160, numbering: "report" },
1048
- notice: { bodyPt: 15, lineSpacing: 160, numbering: "standard" },
1049
- minutes: { bodyPt: 14, lineSpacing: 130, numbering: "standard" },
1050
- gaejosik: { bodyPt: 15, lineSpacing: 160, numbering: "gaejosik" },
1051
- // 보도자료 실측(국토부 실물): 본문 바탕 14pt 160%, □→ㅇ→*(각주) 부호
1052
- press: { bodyPt: 14, lineSpacing: 160, numbering: "report" }
1053
- };
1054
- var PRESET_ALIAS = {
1055
- official: "official",
1056
- \uAE30\uC548\uBB38: "official",
1057
- \uC2DC\uD589\uBB38: "official",
1058
- \uACF5\uBB38: "official",
1059
- \uACF5\uBB38\uC11C: "official",
1060
- report: "report",
1061
- \uBCF4\uACE0\uC11C: "report",
1062
- plan: "plan",
1063
- \uACC4\uD68D\uC11C: "plan",
1064
- \uACC4\uD68D: "plan",
1065
- notice: "notice",
1066
- \uD1B5\uC9C0: "notice",
1067
- \uC54C\uB9BC: "notice",
1068
- \uC548\uB0B4: "notice",
1069
- minutes: "minutes",
1070
- \uD68C\uC758\uB85D: "minutes",
1071
- gaejosik: "gaejosik",
1072
- \uAC1C\uC870\uC2DD: "gaejosik",
1073
- \uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
1074
- \uC815\uBD80\uBCF4\uACE0\uC11C: "gaejosik",
1075
- \uC815\uBD80\uD45C\uC900\uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
1076
- press: "press",
1077
- \uBCF4\uB3C4\uC790\uB8CC: "press"
1078
- };
1079
- function normalizeGongmunPreset(preset) {
1080
- if (!preset) return "official";
1081
- return PRESET_ALIAS[preset.trim()] ?? "official";
1082
- }
1083
- function usesReportFonts(preset) {
1084
- return preset === "gaejosik" || preset === "report" || preset === "plan";
1085
- }
1086
- function usesAsteriskThird(preset) {
1087
- return preset === "plan" || preset === "press";
1088
- }
1089
- function needsGaejosikAssets(gongmun) {
1090
- return usesReportFonts(gongmun.preset) || gongmun.cover !== null || gongmun.toc;
1091
- }
1092
- function assertFiniteRange(name, value, min, max) {
1093
- if (!Number.isFinite(value) || value < min || value > max) {
1094
- throw new KordocError(`${name} must be a finite number between ${min} and ${max}`);
1271
+ // src/hwpx/table-build.ts
1272
+ function buildTableWithCellMeta(state, keepAnchoredEmptyCols, keepEmptyParagraphs) {
1273
+ const table2 = buildTable(state.rows, { keepAnchoredEmptyCols, keepEmptyParagraphs });
1274
+ if (state.caption) table2.caption = state.caption;
1275
+ if (state.captionBlocks?.length) table2.captionBlocks = state.captionBlocks;
1276
+ const anchors = [];
1277
+ {
1278
+ const covered = /* @__PURE__ */ new Set();
1279
+ for (let r = 0; r < table2.rows; r++) {
1280
+ for (let c = 0; c < table2.cols; c++) {
1281
+ if (covered.has(`${r},${c}`)) continue;
1282
+ const cell2 = table2.cells[r]?.[c];
1283
+ if (!cell2) continue;
1284
+ for (let dr = 0; dr < cell2.rowSpan; dr++) {
1285
+ for (let dc = 0; dc < cell2.colSpan; dc++) {
1286
+ if (dr === 0 && dc === 0) continue;
1287
+ if (r + dr < table2.rows && c + dc < table2.cols) covered.add(`${r + dr},${c + dc}`);
1288
+ }
1289
+ }
1290
+ anchors.push(cell2);
1291
+ c += cell2.colSpan - 1;
1292
+ }
1293
+ }
1294
+ }
1295
+ const srcCount = state.rows.reduce((s, r) => s + r.length, 0);
1296
+ const ordinalReliable = anchors.length === srcCount;
1297
+ const claimed = /* @__PURE__ */ new Set();
1298
+ let flatIdx = -1;
1299
+ for (const row of state.rows) {
1300
+ for (const src of row) {
1301
+ flatIdx++;
1302
+ const needsBlocks = !!src.blocks && src.blocks.length > 0 && (src.hasStructure || src.blocks.some((b) => b.spans));
1303
+ if (!needsBlocks && !src.isHeader) continue;
1304
+ let target;
1305
+ const trimmed = keepEmptyParagraphs ? src.text : src.text.trim();
1306
+ if (src.rowAddr !== void 0 && src.colAddr !== void 0) {
1307
+ const cand = table2.cells[src.rowAddr]?.[src.colAddr];
1308
+ if (cand && cand.text === trimmed && !claimed.has(cand)) target = cand;
1309
+ }
1310
+ if (!target) {
1311
+ outer: for (const irRow of table2.cells) {
1312
+ for (const cand of irRow) {
1313
+ if (!claimed.has(cand) && cand.text === trimmed && cand.colSpan === src.colSpan && cand.rowSpan === src.rowSpan) {
1314
+ target = cand;
1315
+ break outer;
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1320
+ if (!target && ordinalReliable) {
1321
+ const cand = anchors[flatIdx];
1322
+ if (cand && !claimed.has(cand)) target = cand;
1323
+ }
1324
+ if (!target) continue;
1325
+ claimed.add(target);
1326
+ if (needsBlocks) target.blocks = src.blocks;
1327
+ if (src.isHeader) target.isHeader = true;
1328
+ }
1095
1329
  }
1330
+ return table2;
1096
1331
  }
1097
- function validateGongmunOptions(opts) {
1098
- if (opts.bodyPt !== void 0) assertFiniteRange("bodyPt", opts.bodyPt, 6, 40);
1099
- if (opts.lineSpacing !== void 0) assertFiniteRange("lineSpacing", opts.lineSpacing, 50, 300);
1100
- if (typeof opts.autoFit === "object" && opts.autoFit.minRatio !== void 0) {
1101
- assertFiniteRange("autoFit.minRatio", opts.autoFit.minRatio, 50, 99);
1332
+ function completeTable(newTable, tableStack, blocks, ctx) {
1333
+ const parentTable = tableStack.length > 0 ? tableStack.pop() : null;
1334
+ if (newTable.rows.length === 0) {
1335
+ if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
1336
+ return parentTable;
1102
1337
  }
1103
- if (opts.margins) {
1104
- for (const side of ["top", "bottom", "left", "right"]) {
1105
- const value = opts.margins[side];
1106
- assertFiniteRange(`margins.${side}`, value, 0, 200);
1107
- }
1108
- if (opts.margins.left + opts.margins.right >= 210) {
1109
- throw new KordocError("margins.left + margins.right must be less than 210");
1110
- }
1111
- if (opts.margins.top + opts.margins.bottom >= 297) {
1112
- throw new KordocError("margins.top + margins.bottom must be less than 297");
1338
+ const ir = buildTableWithCellMeta(newTable, ctx.shared.keepTrailingEmptyCols, ctx.shared.keepEmptyParagraphs);
1339
+ const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
1340
+ if (parentTable?.cell) {
1341
+ const cell2 = parentTable.cell;
1342
+ (cell2.blocks ??= []).push(block);
1343
+ cell2.hasStructure = true;
1344
+ let flat = convertTableToText(newTable.rows);
1345
+ if (newTable.caption) flat = newTable.caption + (flat ? "\n" + flat : "");
1346
+ if (flat) {
1347
+ const inlineJoin = newTable.inline === true && cell2.lineOpen === true;
1348
+ cell2.text += (cell2.text ? inlineJoin ? " " : "\n" : "") + flat;
1349
+ cell2.lineOpen = newTable.inline === true;
1113
1350
  }
1351
+ } else {
1352
+ blocks.push(block);
1114
1353
  }
1115
- if (opts.sizes) {
1116
- for (const [key, value] of Object.entries(opts.sizes)) {
1117
- if (value !== void 0) assertFiniteRange(`sizes.${key}`, value, 6, 60);
1354
+ return parentTable;
1355
+ }
1356
+
1357
+ // src/hwpx/section-walker.ts
1358
+ function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
1359
+ const parser = createXmlParser(warnings);
1360
+ const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1361
+ if (!doc.documentElement) return [];
1362
+ const ctx = { styleMap, warnings, sectionNum, shared: shared ?? createSectionShared() };
1363
+ ctx.shared.track.deleteDepth = 0;
1364
+ for (const tagName of ["hp:secPr", "secPr"]) {
1365
+ const els = doc.getElementsByTagName(tagName);
1366
+ if (els.length > 0) {
1367
+ const v = els[0].getAttribute("outlineShapeIDRef");
1368
+ if (v) ctx.outlineNumId = v;
1369
+ break;
1118
1370
  }
1119
1371
  }
1120
- if (opts.approval && opts.approval.length > 6) {
1121
- throw new KordocError("approval must contain at most 6 labels");
1122
- }
1372
+ const blocks = [];
1373
+ walkSection(doc.documentElement, blocks, null, [], ctx);
1374
+ return blocks;
1123
1375
  }
1124
- function incompatibleGongmunWarnings(opts) {
1125
- const warns = [];
1126
- const preset = normalizeGongmunPreset(opts.preset);
1127
- if (opts.docHead && preset !== "official") warns.push(`doc_head(\uB450\uBB38)\uB294 \uAE30\uC548\uBB38(official) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
1128
- if (opts.docFoot && preset !== "official") warns.push(`doc_foot(\uACB0\uBB38)\uB294 \uAE30\uC548\uBB38(official) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
1129
- if (opts.noticeHead && preset !== "notice") warns.push(`notice_head(\uACF5\uACE0\uBC88\uD638\xB7\uBC1C\uC2E0\uBA85\uC758)\uB294 \uD1B5\uC9C0(notice) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
1130
- if (opts.press && preset !== "press") warns.push(`press(\uBA38\uB9AC\uBC15\uC2A4\xB7\uBD80\uC81C\xB7\uB2F4\uB2F9)\uB294 \uBCF4\uB3C4\uC790\uB8CC(press) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
1131
- if (preset === "press" && (opts.cover === true || typeof opts.cover === "object" || opts.toc === true)) {
1132
- warns.push("\uBCF4\uB3C4\uC790\uB8CC\uB294 \uBA38\uB9AC\uBC15\uC2A4 \uC11C\uC2DD\uACFC \uC591\uB9BD \uBD88\uAC00\uB77C \uD45C\uC9C0\xB7\uBAA9\uCC28\uAC00 \uBB34\uC2DC\uB428");
1133
- }
1134
- if (opts.sizes && Object.keys(opts.sizes).length > 0 && !usesReportFonts(preset)) {
1135
- warns.push(`sizes(\uAC1C\uC870\uC2DD \uC694\uC18C \uD06C\uAE30)\uB294 \uAC1C\uC870\uC2DD\xB7\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
1136
- }
1137
- if (opts.suppressSingle && (opts.numbering ?? PRESET_DEFAULTS[preset].numbering) !== "standard") {
1138
- warns.push(`suppress_single(\uB2E8\uC77C \uD615\uC81C \uBD80\uD638 \uC0DD\uB7B5)\uC740 \uBC95\uC815 \uBC88\uD638(standard) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B(\uBD88\uB9BF \uCCB4\uACC4)\uC5D0\uC11C \uBB34\uB3D9\uC791`);
1376
+ function extractImageRef(el, depth = 0) {
1377
+ if (depth > MAX_XML_DEPTH) return null;
1378
+ const children = el.childNodes;
1379
+ if (!children) return null;
1380
+ for (let i = 0; i < children.length; i++) {
1381
+ const child = children[i];
1382
+ if (child.nodeType !== 1) continue;
1383
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1384
+ if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
1385
+ const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
1386
+ if (ref) return ref;
1387
+ }
1388
+ const nested = extractImageRef(child, depth + 1);
1389
+ if (nested) return nested;
1139
1390
  }
1140
- return warns;
1141
- }
1142
- function resolveGongmun(opts) {
1143
- validateGongmunOptions(opts);
1144
- const preset = normalizeGongmunPreset(opts.preset);
1145
- const d = PRESET_DEFAULTS[preset];
1146
- const bodyPt = opts.bodyPt ?? d.bodyPt;
1147
- const autoFitMinRatio = opts.autoFit === false ? null : typeof opts.autoFit === "object" ? Math.min(Math.max(opts.autoFit.minRatio ?? 90, 50), 99) : 90;
1148
- const coverOn = opts.cover !== void 0 ? opts.cover !== false : preset === "gaejosik";
1149
- const coverOpts = typeof opts.cover === "object" ? opts.cover : {};
1150
- const gaejosik = preset === "gaejosik";
1151
- const reportFamily = gaejosik || preset === "report" || preset === "plan" || preset === "notice" || preset === "press";
1152
- return {
1153
- preset,
1154
- bodyFont: opts.bodyFont ?? "myeongjo",
1155
- bodyHeight: Math.round(bodyPt * 100),
1156
- lineSpacing: opts.lineSpacing ?? d.lineSpacing,
1157
- numbering: opts.numbering ?? d.numbering,
1158
- margins: opts.margins ?? (reportFamily ? GAEJOSIK_MARGINS : OFFICIAL_MARGINS),
1159
- centerTitle: opts.centerTitle ?? true,
1160
- autoFitMinRatio,
1161
- // 보도자료는 머리박스가 1페이지 최상단을 차지하는 서식이라 표지·목차와 양립 불가 —
1162
- // 켜면 머리박스가 표지에 얹히고 25pt 제목·부제가 유실된다 (docHead 프리셋 게이팅과 동일 관례)
1163
- cover: coverOn && preset !== "press" ? { date: coverOpts.date ?? null, org: coverOpts.org ?? "" } : null,
1164
- toc: preset !== "press" && (opts.toc ?? gaejosik),
1165
- fonts: opts.fonts ?? {},
1166
- sizes: opts.sizes ?? {},
1167
- // 쪽번호 — 보고서 계열 관행(실측: 2_보고서 양식·추진계획·공고문 전부 하단 중앙)
1168
- pageNumbers: opts.pageNumbers ?? (gaejosik || preset === "report" || preset === "plan"),
1169
- // 머리말·꼬리말 — 실측: 보고서 계열 15mm(GT3·t2·춘천·브라더), 공고·보도 10mm,
1170
- // 기안문 0(실결재 41/60건 h0/f0)
1171
- headerFooter: usesReportFonts(preset) ? GAEJOSIK_HEADER_FOOTER : preset === "notice" || preset === "press" ? 2835 : 0,
1172
- // "끝." — 기안문 규정(본문 끝 2타+"끝."). 그 외는 opt-in
1173
- endMark: opts.endMark ?? preset === "official",
1174
- approval: opts.approval && opts.approval.length > 0 ? opts.approval : null,
1175
- // 본문 제목박스 — 실측(GT3·GT12): 목차 뒤 본문 시작에 제목 반복. 표지 켜진 개조식 기본
1176
- bodyTitleBox: opts.bodyTitleBox ?? (gaejosik && coverOn),
1177
- // h2 말머리 — 실측: 보고서 양식 □ 대항목(QA-2), 공고문 아라비아("1. 사업개요", 바이오헬스 실측)
1178
- h2Marker: opts.h2Marker ?? (preset === "report" || preset === "plan" ? "box" : preset === "notice" ? "number" : "none"),
1179
- // 2단계 부호 — 실결재 기안문·공고문 ㅇ 지배(60건 중 ㅇ134:○5), 보고서 양식 계열 ○
1180
- bullet2: opts.bullet2 ?? (preset === "plan" || preset === "notice" || preset === "press" ? "\u3147" : "\u25CB"),
1181
- // 단일 형제 부호 생략 — 규정이지만 부호 없는 계단이 실무 눈에 어색 (실무자 QA)
1182
- suppressSingle: opts.suppressSingle ?? false,
1183
- docHead: preset === "official" && opts.docHead ? opts.docHead : null,
1184
- docFoot: preset === "official" && opts.docFoot ? opts.docFoot : null,
1185
- reportInfo: opts.reportInfo?.trim() || null,
1186
- noticeHead: preset === "notice" && opts.noticeHead ? opts.noticeHead : null,
1187
- // 보도자료 머리박스는 프리셋 자체가 요구 — 옵션 미지정이어도 빈 객체
1188
- press: preset === "press" ? opts.press ?? {} : null
1189
- };
1190
- }
1191
- var HANGUL_INITIALS = [0, 2, 3, 5, 6, 7, 9, 11, 12, 14, 15, 16, 17, 18];
1192
- var HANGUL_MEDIALS = [0, 4, 8, 13, 18, 20];
1193
- function hangulOrdinal(n) {
1194
- const cols = HANGUL_INITIALS.length;
1195
- const vowel = HANGUL_MEDIALS[Math.min(Math.floor(n / cols), HANGUL_MEDIALS.length - 1)];
1196
- const init = HANGUL_INITIALS[n % cols];
1197
- return String.fromCodePoint(44032 + init * 588 + vowel * 28);
1198
- }
1199
- function circledNumber(n) {
1200
- if (n < 20) return String.fromCodePoint(9312 + n);
1201
- if (n < 35) return String.fromCodePoint(12881 + (n - 20));
1202
- if (n < 50) return String.fromCodePoint(12977 + (n - 35));
1203
- return `(${n + 1})`;
1204
- }
1205
- function circledHangul(n) {
1206
- return n < 14 ? String.fromCodePoint(12910 + n) : hangulOrdinal(n);
1207
- }
1208
- var REPORT_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
1209
- var ASTERISK_BULLETS = ["\u25A1", "\u25CB", "*", "\u318D"];
1210
- function standardMarker(depth, n) {
1211
- switch (depth) {
1212
- case 0:
1213
- return `${n + 1}.`;
1214
- case 1:
1215
- return `${hangulOrdinal(n)}.`;
1216
- case 2:
1217
- return `${n + 1})`;
1218
- case 3:
1219
- return `${hangulOrdinal(n)})`;
1220
- case 4:
1221
- return `(${n + 1})`;
1222
- case 5:
1223
- return `(${hangulOrdinal(n)})`;
1224
- case 6:
1225
- return circledNumber(n);
1226
- case 7:
1227
- return circledHangul(n);
1228
- default:
1229
- return circledHangul(n);
1230
- }
1231
- }
1232
- function reportMarker(depth, bullet2 = "\u25CB", asteriskThird = false) {
1233
- const bullets = asteriskThird ? ASTERISK_BULLETS : REPORT_BULLETS;
1234
- const m = bullets[Math.min(depth, bullets.length - 1)];
1235
- return depth === 1 ? bullet2 : m;
1236
- }
1237
- function markerWidth(marker, bodyHeight) {
1238
- let em = SPACE_EM_FIXED;
1239
- for (const c of marker) em += charWidthEm1000(c.codePointAt(0));
1240
- return Math.round(em / 1e3 * bodyHeight);
1241
- }
1242
- function levelIndent(depth, bodyHeight, numbering, sizes = {}, bullet2 = "\u25CB", asteriskThird = false) {
1243
- if (numbering === "gaejosik") return gaejosikLevelIndent(depth, bodyHeight, sizes, bullet2);
1244
- const marker = numbering === "report" ? reportMarker(depth, bullet2, asteriskThird) : standardMarker(depth, 0);
1245
- return { left: Math.round(depth * bodyHeight), indent: -markerWidth(marker, bodyHeight) };
1246
- }
1247
- function computeSuppression(depths) {
1248
- const counts = /* @__PURE__ */ new Map();
1249
- const keys = [];
1250
- const path = [];
1251
- for (const depth of depths) {
1252
- path.length = depth + 1;
1253
- path[depth] = (path[depth] ?? 0) + 1;
1254
- const parentKey = path.slice(0, depth).join(".") + "|" + depth;
1255
- keys.push(parentKey);
1256
- counts.set(parentKey, (counts.get(parentKey) ?? 0) + 1);
1257
- }
1258
- return keys.map((k) => (counts.get(k) ?? 0) <= 1);
1259
- }
1260
- var GongmunNumberer = class {
1261
- constructor(numbering, bullet2 = "\u25CB", asteriskThird = false) {
1262
- this.numbering = numbering;
1263
- this.bullet2 = bullet2;
1264
- this.asteriskThird = asteriskThird;
1265
- }
1266
- counts = [];
1267
- /** depth 항목 하나에 대한 마커. suppress=true면 빈 문자열(부호 없음) */
1268
- next(depth, suppress) {
1269
- this.counts.length = depth + 1;
1270
- const n = this.counts[depth] ?? 0;
1271
- this.counts[depth] = n + 1;
1272
- if (suppress) return "";
1273
- if (this.numbering === "gaejosik") return gaejosikMarker(depth, this.bullet2);
1274
- return this.numbering === "report" ? reportMarker(depth, this.bullet2, this.asteriskThird) : standardMarker(depth, n);
1275
- }
1276
- reset() {
1277
- this.counts = [];
1278
- }
1279
- };
1280
-
1281
- // src/hwpx/para-heading.ts
1282
- var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
1283
- function toRoman(n) {
1284
- if (n <= 0 || n >= 4e3) return String(n);
1285
- const table2 = [
1286
- [1e3, "M"],
1287
- [900, "CM"],
1288
- [500, "D"],
1289
- [400, "CD"],
1290
- [100, "C"],
1291
- [90, "XC"],
1292
- [50, "L"],
1293
- [40, "XL"],
1294
- [10, "X"],
1295
- [9, "IX"],
1296
- [5, "V"],
1297
- [4, "IV"],
1298
- [1, "I"]
1299
- ];
1300
- let out = "";
1301
- for (const [v, s] of table2) {
1302
- while (n >= v) {
1303
- out += s;
1304
- n -= v;
1305
- }
1306
- }
1307
- return out;
1308
- }
1309
- function formatHeadNumber(n, numFormat) {
1310
- if (n === 0 && numFormat === "DIGIT") return "0";
1311
- if (n <= 0) n = 1;
1312
- switch (numFormat) {
1313
- case "DIGIT":
1314
- return String(n);
1315
- // 서수 시퀀스는 생성기(gongmun.ts)와 단일 소스 (v4.0.5) — 종전 mod-14 순환은
1316
- // 15번째 형제를 다시 '가'로 만들어 한글 실렌더(가→하→거→너 단모음 연속)와 어긋났다
1317
- case "CIRCLED_DIGIT":
1318
- return n <= 50 ? circledNumber(n - 1) : `(${n})`;
1319
- case "HANGUL_SYLLABLE":
1320
- return hangulOrdinal(n - 1);
1321
- case "CIRCLED_HANGUL_SYLLABLE":
1322
- return n <= 14 ? circledHangul(n - 1) : hangulOrdinal(n - 1);
1323
- case "HANGUL_JAMO":
1324
- return HANGUL_JAMO_SEQ[(n - 1) % HANGUL_JAMO_SEQ.length];
1325
- case "CIRCLED_HANGUL_JAMO":
1326
- return n <= 14 ? String.fromCodePoint(12896 + n - 1) : HANGUL_JAMO_SEQ[(n - 1) % 14];
1327
- case "LATIN_CAPITAL":
1328
- return String.fromCharCode(65 + (n - 1) % 26);
1329
- case "LATIN_SMALL":
1330
- return String.fromCharCode(97 + (n - 1) % 26);
1331
- case "CIRCLED_LATIN_CAPITAL":
1332
- return n <= 26 ? String.fromCodePoint(9398 + n - 1) : String.fromCharCode(65 + (n - 1) % 26);
1333
- case "CIRCLED_LATIN_SMALL":
1334
- return n <= 26 ? String.fromCodePoint(9424 + n - 1) : String.fromCharCode(97 + (n - 1) % 26);
1335
- case "ROMAN_CAPITAL":
1336
- return toRoman(n);
1337
- case "ROMAN_SMALL":
1338
- return toRoman(n).toLowerCase();
1339
- default:
1340
- return String(n);
1341
- }
1342
- }
1343
- function resolveParaHeading(paraEl, ctx) {
1344
- const sm = ctx.styleMap;
1345
- if (!sm) return null;
1346
- const styleId = paraEl.getAttribute("styleIDRef");
1347
- let styleHeading;
1348
- if (styleId && styleId !== "0") {
1349
- const st = sm.styles.get(styleId);
1350
- const m = st && /^개요\s*([1-6])$/.exec(st.name);
1351
- if (m) styleHeading = Math.min(parseInt(m[1], 10), 6);
1352
- }
1353
- const prId = paraEl.getAttribute("paraPrIDRef");
1354
- if (!prId) return styleHeading ? { headingLevel: styleHeading } : null;
1355
- const ref = sm.paraHeadings.get(prId);
1356
- if (!ref) return styleHeading ? { headingLevel: styleHeading } : null;
1357
- if (ref.type === "BULLET") {
1358
- const char = sm.bullets.get(ref.idRef);
1359
- return char ? { prefix: char } : null;
1360
- }
1361
- const numId = ref.type === "OUTLINE" ? ctx.outlineNumId || "1" : ref.idRef;
1362
- const level = Math.min(ref.level + 1, 10);
1363
- const headingLevel = ref.type === "OUTLINE" ? Math.min(ref.level + 1, 6) : void 0;
1364
- const numDef = sm.numberings.get(numId);
1365
- if (!numDef) return headingLevel ? { headingLevel } : null;
1366
- let counters = ctx.shared.numState.get(numId);
1367
- if (!counters) {
1368
- counters = new Array(11).fill(-1);
1369
- ctx.shared.numState.set(numId, counters);
1370
- }
1371
- const head = numDef.heads.get(level);
1372
- counters[level] = counters[level] < 0 ? head?.start ?? 1 : counters[level] + 1;
1373
- for (let l = level + 1; l <= 10; l++) counters[l] = -1;
1374
- const fmtText = head ? head.text.trim() : `^${level}.`;
1375
- const prefix = fmtText.replace(/\^(10|[1-9])/g, (_, d) => {
1376
- const lv = parseInt(d, 10);
1377
- const refHead = numDef.heads.get(lv);
1378
- const n = counters[lv] >= 0 ? counters[lv] : refHead?.start ?? 1;
1379
- return formatHeadNumber(n, refHead?.numFormat || "DIGIT");
1380
- });
1381
- return { prefix: prefix || void 0, headingLevel };
1382
- }
1383
-
1384
- // src/hwpx/table-build.ts
1385
- function buildTableWithCellMeta(state, keepAnchoredEmptyCols) {
1386
- const table2 = buildTable(state.rows, { keepAnchoredEmptyCols });
1387
- if (state.caption) table2.caption = state.caption;
1388
- if (state.captionBlocks?.length) table2.captionBlocks = state.captionBlocks;
1389
- const anchors = [];
1390
- {
1391
- const covered = /* @__PURE__ */ new Set();
1392
- for (let r = 0; r < table2.rows; r++) {
1393
- for (let c = 0; c < table2.cols; c++) {
1394
- if (covered.has(`${r},${c}`)) continue;
1395
- const cell2 = table2.cells[r]?.[c];
1396
- if (!cell2) continue;
1397
- for (let dr = 0; dr < cell2.rowSpan; dr++) {
1398
- for (let dc = 0; dc < cell2.colSpan; dc++) {
1399
- if (dr === 0 && dc === 0) continue;
1400
- if (r + dr < table2.rows && c + dc < table2.cols) covered.add(`${r + dr},${c + dc}`);
1401
- }
1402
- }
1403
- anchors.push(cell2);
1404
- c += cell2.colSpan - 1;
1405
- }
1406
- }
1407
- }
1408
- const srcCount = state.rows.reduce((s, r) => s + r.length, 0);
1409
- const ordinalReliable = anchors.length === srcCount;
1410
- const claimed = /* @__PURE__ */ new Set();
1411
- let flatIdx = -1;
1412
- for (const row of state.rows) {
1413
- for (const src of row) {
1414
- flatIdx++;
1415
- const needsBlocks = !!src.blocks && src.blocks.length > 0 && (src.hasStructure || src.blocks.some((b) => b.spans));
1416
- if (!needsBlocks && !src.isHeader) continue;
1417
- let target;
1418
- const trimmed = src.text.trim();
1419
- if (src.rowAddr !== void 0 && src.colAddr !== void 0) {
1420
- const cand = table2.cells[src.rowAddr]?.[src.colAddr];
1421
- if (cand && cand.text === trimmed && !claimed.has(cand)) target = cand;
1422
- }
1423
- if (!target) {
1424
- outer: for (const irRow of table2.cells) {
1425
- for (const cand of irRow) {
1426
- if (!claimed.has(cand) && cand.text === trimmed && cand.colSpan === src.colSpan && cand.rowSpan === src.rowSpan) {
1427
- target = cand;
1428
- break outer;
1429
- }
1430
- }
1431
- }
1432
- }
1433
- if (!target && ordinalReliable) {
1434
- const cand = anchors[flatIdx];
1435
- if (cand && !claimed.has(cand)) target = cand;
1436
- }
1437
- if (!target) continue;
1438
- claimed.add(target);
1439
- if (needsBlocks) target.blocks = src.blocks;
1440
- if (src.isHeader) target.isHeader = true;
1441
- }
1442
- }
1443
- return table2;
1444
- }
1445
- function completeTable(newTable, tableStack, blocks, ctx) {
1446
- const parentTable = tableStack.length > 0 ? tableStack.pop() : null;
1447
- if (newTable.rows.length === 0) {
1448
- if (newTable.caption) blocks.push({ type: "paragraph", text: newTable.caption, pageNumber: ctx.sectionNum });
1449
- return parentTable;
1450
- }
1451
- const ir = buildTableWithCellMeta(newTable, ctx.shared.keepTrailingEmptyCols);
1452
- const block = { type: "table", table: ir, pageNumber: ctx.sectionNum };
1453
- if (parentTable?.cell) {
1454
- const cell2 = parentTable.cell;
1455
- (cell2.blocks ??= []).push(block);
1456
- cell2.hasStructure = true;
1457
- let flat = convertTableToText(newTable.rows);
1458
- if (newTable.caption) flat = newTable.caption + (flat ? "\n" + flat : "");
1459
- if (flat) {
1460
- const inlineJoin = newTable.inline === true && cell2.lineOpen === true;
1461
- cell2.text += (cell2.text ? inlineJoin ? " " : "\n" : "") + flat;
1462
- cell2.lineOpen = newTable.inline === true;
1463
- }
1464
- } else {
1465
- blocks.push(block);
1466
- }
1467
- return parentTable;
1468
- }
1469
-
1470
- // src/hwpx/section-walker.ts
1471
- function parseSectionXml(xml, styleMap, warnings, sectionNum, shared) {
1472
- const parser = createXmlParser(warnings);
1473
- const doc = parser.parseFromString(stripDtd(xml), "text/xml");
1474
- if (!doc.documentElement) return [];
1475
- const ctx = { styleMap, warnings, sectionNum, shared: shared ?? createSectionShared() };
1476
- ctx.shared.track.deleteDepth = 0;
1477
- for (const tagName of ["hp:secPr", "secPr"]) {
1478
- const els = doc.getElementsByTagName(tagName);
1479
- if (els.length > 0) {
1480
- const v = els[0].getAttribute("outlineShapeIDRef");
1481
- if (v) ctx.outlineNumId = v;
1482
- break;
1483
- }
1484
- }
1485
- const blocks = [];
1486
- walkSection(doc.documentElement, blocks, null, [], ctx);
1487
- return blocks;
1488
- }
1489
- function extractImageRef(el, depth = 0) {
1490
- if (depth > MAX_XML_DEPTH) return null;
1491
- const children = el.childNodes;
1492
- if (!children) return null;
1493
- for (let i = 0; i < children.length; i++) {
1494
- const child = children[i];
1495
- if (child.nodeType !== 1) continue;
1496
- const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
1497
- if (tag === "imgRect" || tag === "img" || tag === "imgClip") {
1498
- const ref = child.getAttribute("binaryItemIDRef") || child.getAttribute("href") || "";
1499
- if (ref) return ref;
1500
- }
1501
- const nested = extractImageRef(child, depth + 1);
1502
- if (nested) return nested;
1503
- }
1504
- const directRef = el.getAttribute("binaryItemIDRef") || "";
1505
- if (directRef) return directRef;
1506
- return null;
1391
+ const directRef = el.getAttribute("binaryItemIDRef") || "";
1392
+ if (directRef) return directRef;
1393
+ return null;
1507
1394
  }
1508
1395
  function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1509
1396
  if (depth > MAX_XML_DEPTH) return;
@@ -1625,7 +1512,8 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1625
1512
  if (tableCtx?.cell) {
1626
1513
  const cell2 = tableCtx.cell;
1627
1514
  if (footnote) text += ` (\uC8FC: ${footnote})`;
1628
- cell2.text += (cell2.text ? "\n" : "") + text;
1515
+ cell2.text += (cell2.text || cell2.paraSeen ? "\n" : "") + text;
1516
+ cell2.paraSeen = true;
1629
1517
  const cellBlock = { type: "paragraph", text, pageNumber: ctx.sectionNum };
1630
1518
  const cellSpanMode = spanModeOf(ctx.shared.kordocLayout);
1631
1519
  if (cellSpanMode && !ph?.prefix) {
@@ -1670,6 +1558,16 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1670
1558
  } else {
1671
1559
  blocks.push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
1672
1560
  }
1561
+ } else if (ctx.shared.keepEmptyParagraphs && !hasObjectDescendant(el)) {
1562
+ if (tableCtx?.cell) {
1563
+ const cell2 = tableCtx.cell;
1564
+ if (cell2.text || cell2.paraSeen) cell2.text += "\n";
1565
+ cell2.paraSeen = true;
1566
+ cell2.lineOpen = false;
1567
+ (cell2.blocks ??= []).push({ type: "paragraph", text: "", pageNumber: ctx.sectionNum });
1568
+ } else if (!tableCtx) {
1569
+ blocks.push({ type: "paragraph", text: "", pageNumber: ctx.sectionNum });
1570
+ }
1673
1571
  }
1674
1572
  tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1675
1573
  break;
@@ -1846,10 +1744,10 @@ function buildSubListTable(el, ctx, depth) {
1846
1744
  }
1847
1745
  function findTopLevelTbls(el, out, depth = 0) {
1848
1746
  if (depth > MAX_XML_DEPTH) return;
1849
- const kids2 = el.childNodes;
1850
- if (!kids2) return;
1851
- for (let i = 0; i < kids2.length; i++) {
1852
- const ch = kids2[i];
1747
+ const kids = el.childNodes;
1748
+ if (!kids) return;
1749
+ for (let i = 0; i < kids.length; i++) {
1750
+ const ch = kids[i];
1853
1751
  if (ch.nodeType !== 1) continue;
1854
1752
  const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1855
1753
  if (tag === "tbl") {
@@ -1865,10 +1763,10 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth =
1865
1763
  if (!children) return tableCtx;
1866
1764
  const walkChildren = (parent, d, inShape = false) => {
1867
1765
  if (d > MAX_XML_DEPTH) return;
1868
- const kids2 = parent.childNodes;
1869
- if (!kids2) return;
1870
- for (let i = 0; i < kids2.length; i++) {
1871
- const el = kids2[i];
1766
+ const kids = parent.childNodes;
1767
+ if (!kids) return;
1768
+ for (let i = 0; i < kids.length; i++) {
1769
+ const el = kids[i];
1872
1770
  if (el.nodeType !== 1) continue;
1873
1771
  const tag = el.tagName || el.localName || "";
1874
1772
  const localTag = tag.replace(/^[^:]+:/, "");
@@ -1922,6 +1820,20 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth =
1922
1820
  walkChildren(node, depth);
1923
1821
  return tableCtx;
1924
1822
  }
1823
+ var OBJECT_TAGS = /* @__PURE__ */ new Set(["tbl", "pic", "shape", "drawingObject", "drawText"]);
1824
+ function hasObjectDescendant(node, depth = 0) {
1825
+ if (depth > MAX_XML_DEPTH) return false;
1826
+ const kids = node.childNodes;
1827
+ if (!kids) return false;
1828
+ for (let i = 0; i < kids.length; i++) {
1829
+ const ch = kids[i];
1830
+ if (ch.nodeType !== 1) continue;
1831
+ const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1832
+ if (OBJECT_TAGS.has(tag)) return true;
1833
+ if (hasObjectDescendant(ch, depth + 1)) return true;
1834
+ }
1835
+ return false;
1836
+ }
1925
1837
  function isInlineTbl(tbl2) {
1926
1838
  return findChildByLocalName(tbl2, "pos")?.getAttribute("treatAsChar") === "1";
1927
1839
  }
@@ -2009,10 +1921,10 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2009
1921
  if (open?.rangeIdx !== void 0) linkRanges[open.rangeIdx].end = text.length;
2010
1922
  };
2011
1923
  const handleCtrl = (ctrlEl) => {
2012
- const kids2 = ctrlEl.childNodes;
2013
- if (!kids2) return;
2014
- for (let j = 0; j < kids2.length; j++) {
2015
- const k = kids2[j];
1924
+ const kids = ctrlEl.childNodes;
1925
+ if (!kids) return;
1926
+ for (let j = 0; j < kids.length; j++) {
1927
+ const k = kids[j];
2016
1928
  if (k.nodeType !== 1) continue;
2017
1929
  const ktag = (k.tagName || k.localName || "").replace(/^[^:]+:/, "");
2018
1930
  switch (ktag) {
@@ -2205,6 +2117,13 @@ function extractParagraphInfo(para2, styleMap, ctx) {
2205
2117
  const latex = hmlToLatex(raw).trim();
2206
2118
  if (latex) text += " $" + latex + "$ ";
2207
2119
  } catch {
2120
+ if (ctx?.warnings) {
2121
+ ctx.warnings.push({
2122
+ page: ctx.sectionNum,
2123
+ message: `\uC218\uC2DD LaTeX \uBCC0\uD658 \uC2E4\uD328 \u2014 \uC218\uC2DD \uD14D\uC2A4\uD2B8 \uC81C\uC678: ${raw.trim().slice(0, 40)}`,
2124
+ code: "PARTIAL_PARSE"
2125
+ });
2126
+ }
2208
2127
  }
2209
2128
  }
2210
2129
  break;
@@ -2276,10 +2195,10 @@ function extractRunSpans(para2, ctx, mode, requireMixed) {
2276
2195
  if (!styleMap) return null;
2277
2196
  const spans = [];
2278
2197
  let styled = false;
2279
- const kids2 = para2.childNodes;
2280
- if (!kids2) return null;
2281
- for (let i = 0; i < kids2.length; i++) {
2282
- const child = kids2[i];
2198
+ const kids = para2.childNodes;
2199
+ if (!kids) return null;
2200
+ for (let i = 0; i < kids.length; i++) {
2201
+ const child = kids[i];
2283
2202
  if (child.nodeType !== 1) continue;
2284
2203
  const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
2285
2204
  if (tag === "linesegarray") continue;
@@ -2334,9 +2253,9 @@ function gongmunDepthFromIndent(para2, left, ctx) {
2334
2253
  const styleMap = ctx.styleMap;
2335
2254
  if (!styleMap) return null;
2336
2255
  let unit = 0;
2337
- const kids2 = para2.childNodes;
2338
- for (let i = 0; i < (kids2?.length ?? 0); i++) {
2339
- const child = kids2[i];
2256
+ const kids = para2.childNodes;
2257
+ for (let i = 0; i < (kids?.length ?? 0); i++) {
2258
+ const child = kids[i];
2340
2259
  if (child.nodeType !== 1) continue;
2341
2260
  const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
2342
2261
  if (tag !== "run" && tag !== "r") continue;
@@ -2650,17 +2569,19 @@ function normalizeBinPayload(data) {
2650
2569
  return data;
2651
2570
  }
2652
2571
  var BIN_ENTRY_RE = /(?:^|\/)BIN([0-9A-Fa-f]{4,8})(?:\.[^./\\]*)?$/;
2653
- function collectImageBlocks(blocks, out) {
2572
+ var MAX_BLOCK_DEPTH = 200;
2573
+ function collectImageBlocks(blocks, out, depth = 0) {
2574
+ if (depth > MAX_BLOCK_DEPTH) return;
2654
2575
  for (const b of blocks) {
2655
2576
  if (b.type === "image") out.push(b);
2656
2577
  if (b.table) {
2657
2578
  for (const row of b.table.cells) {
2658
2579
  for (const cell2 of row) {
2659
- if (cell2.blocks) collectImageBlocks(cell2.blocks, out);
2580
+ if (cell2.blocks) collectImageBlocks(cell2.blocks, out, depth + 1);
2660
2581
  }
2661
2582
  }
2662
2583
  }
2663
- if (b.children) collectImageBlocks(b.children, out);
2584
+ if (b.children) collectImageBlocks(b.children, out, depth + 1);
2664
2585
  }
2665
2586
  }
2666
2587
  function forEachTableCell(blocks, fn) {
@@ -2828,6 +2749,7 @@ function collectImageBlocks2(blocks, out, ownerCell, depth = 0) {
2828
2749
  }
2829
2750
  }
2830
2751
  }
2752
+ if (block.children?.length) collectImageBlocks2(block.children, out, ownerCell, depth + 1);
2831
2753
  }
2832
2754
  }
2833
2755
  async function extractImagesFromZip(zip, blocks, decompressed, warnings, sweepUnreferenced) {
@@ -2925,7 +2847,7 @@ async function extractImagesFromZip(zip, blocks, decompressed, warnings, sweepUn
2925
2847
  }
2926
2848
 
2927
2849
  // src/hwpx/metadata.ts
2928
- import JSZip from "jszip";
2850
+ import JSZip2 from "jszip";
2929
2851
 
2930
2852
  // src/hwpx/zip-sections.ts
2931
2853
  import { inflateRawSync as inflateRawSync2 } from "zlib";
@@ -3089,7 +3011,7 @@ function parseDublinCoreMetadata(xml, metadata) {
3089
3011
  async function extractHwpxMetadataOnly(buffer) {
3090
3012
  let zip;
3091
3013
  try {
3092
- zip = await JSZip.loadAsync(buffer);
3014
+ zip = await JSZip2.loadAsync(buffer);
3093
3015
  } catch {
3094
3016
  throw new KordocError("HWPX ZIP\uC744 \uC5F4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
3095
3017
  }
@@ -3105,7 +3027,7 @@ async function parseHwpxDocument(buffer, options) {
3105
3027
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE, MAX_ZIP_ENTRIES);
3106
3028
  let zip;
3107
3029
  try {
3108
- zip = await JSZip2.loadAsync(buffer);
3030
+ zip = await JSZip3.loadAsync(buffer);
3109
3031
  } catch {
3110
3032
  return extractFromBrokenZip(buffer);
3111
3033
  }
@@ -3140,6 +3062,7 @@ async function parseHwpxDocument(buffer, options) {
3140
3062
  const shared = createSectionShared();
3141
3063
  shared.kordocLayout = await readKordocLayout(zip);
3142
3064
  shared.keepTrailingEmptyCols = options?.keepTrailingEmptyCols;
3065
+ shared.keepEmptyParagraphs = options?.keepEmptyParagraphs;
3143
3066
  let parsedSections = 0;
3144
3067
  for (let si = 0; si < sectionPaths.length; si++) {
3145
3068
  if (pageFilter && !pageFilter.has(si + 1)) continue;
@@ -3240,28 +3163,11 @@ function shapeFormatToNumFmt(code) {
3240
3163
  return "digit";
3241
3164
  }
3242
3165
  }
3243
- var CIRCLED_DIGITS = "\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468\u2469\u246A\u246B\u246C\u246D\u246E\u246F\u2470\u2471\u2472\u2473";
3244
- var GANADA = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
3245
- var CIRCLED_GANADA = "\u326E\u326F\u3270\u3271\u3272\u3273\u3274\u3275\u3276\u3277\u3278\u3279\u327A\u327B";
3246
3166
  var JAMO = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
3247
3167
  var CIRCLED_JAMO = "\u3260\u3261\u3262\u3263\u3264\u3265\u3266\u3267\u3268\u3269\u326A\u326B\u326C\u326D";
3248
3168
  function fromTable(n, table2) {
3249
3169
  return n >= 1 && n <= table2.length ? table2[n - 1] : String(n);
3250
3170
  }
3251
- function formatRoman(n, upper) {
3252
- if (n <= 0 || n > 3999) return String(n);
3253
- const values = [1e3, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
3254
- const symbols = upper ? ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"] : ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"];
3255
- let result = "";
3256
- let num2 = n;
3257
- for (let i = 0; i < values.length; i++) {
3258
- while (num2 >= values[i]) {
3259
- result += symbols[i];
3260
- num2 -= values[i];
3261
- }
3262
- }
3263
- return result;
3264
- }
3265
3171
  function formatLatin(n, upper) {
3266
3172
  if (n <= 0) return "";
3267
3173
  let result = "";
@@ -3297,19 +3203,19 @@ var HANJA_UNITS = ["", "\u5341", "\u767E", "\u5343", "\u842C"];
3297
3203
  function formatNumber(n, fmt) {
3298
3204
  switch (fmt) {
3299
3205
  case "circled":
3300
- return fromTable(n, CIRCLED_DIGITS);
3206
+ return n >= 1 ? circledNumber(n - 1) : String(n);
3301
3207
  case "romanUpper":
3302
- return formatRoman(n, true);
3208
+ return romanNumeral(n, true);
3303
3209
  case "romanLower":
3304
- return formatRoman(n, false);
3210
+ return romanNumeral(n, false);
3305
3211
  case "latinUpper":
3306
3212
  return formatLatin(n, true) || String(n);
3307
3213
  case "latinLower":
3308
3214
  return formatLatin(n, false) || String(n);
3309
3215
  case "ganada":
3310
- return fromTable(n, GANADA);
3216
+ return n >= 1 ? hangulOrdinal(n - 1) : String(n);
3311
3217
  case "circledGanada":
3312
- return fromTable(n, CIRCLED_GANADA);
3218
+ return n >= 1 ? circledHangul(n - 1) : String(n);
3313
3219
  case "jamo":
3314
3220
  return fromTable(n, JAMO);
3315
3221
  case "circledJamo":
@@ -4047,220 +3953,75 @@ function decryptViewText(viewTextRaw, compressed) {
4047
3953
  }
4048
3954
 
4049
3955
  // src/hwp5/equation.ts
4050
- var WORD_COMMANDS = /* @__PURE__ */ new Map([
4051
- ["alpha", "\\alpha"],
4052
- ["beta", "\\beta"],
4053
- ["gamma", "\\gamma"],
4054
- ["delta", "\\delta"],
4055
- ["epsilon", "\\epsilon"],
4056
- ["theta", "\\theta"],
4057
- ["lambda", "\\lambda"],
4058
- ["mu", "\\mu"],
4059
- ["pi", "\\pi"],
4060
- ["sigma", "\\sigma"],
4061
- ["tau", "\\tau"],
4062
- ["phi", "\\phi"],
4063
- ["omega", "\\omega"],
4064
- ["sin", "\\sin"],
4065
- ["cos", "\\cos"],
4066
- ["tan", "\\tan"],
4067
- ["sec", "\\sec"],
4068
- ["csc", "\\csc"],
4069
- ["cot", "\\cot"],
4070
- ["log", "\\log"],
4071
- ["ln", "\\ln"],
4072
- ["lim", "\\lim"],
4073
- ["inf", "\\infty"],
4074
- ["sum", "\\sum"],
4075
- ["smallsum", "\\sum"],
4076
- ["prod", "\\prod"],
4077
- ["int", "\\int"],
4078
- ["oint", "\\oint"],
4079
- ["rightarrow", "\\rightarrow"],
4080
- ["leftarrow", "\\leftarrow"],
4081
- ["partial", "\\partial"],
4082
- ["nabla", "\\nabla"],
4083
- ["angle", "\\angle"],
4084
- ["triangle", "\\triangle"],
4085
- ["vec", "\\vec"],
4086
- ["bar", "\\overline"],
4087
- ["dot", "\\dot"],
4088
- ["hat", "\\hat"],
4089
- ["left", "\\left"],
4090
- ["right", "\\right"]
4091
- ]);
4092
- var SYMBOL_WORDS = /* @__PURE__ */ new Map([
4093
- ["times", "\\times"],
4094
- ["divide", "\\div"],
4095
- ["div", "\\div"],
4096
- ["le", "\\leq"],
4097
- ["ge", "\\geq"],
4098
- ["geq", "\\geq"],
4099
- ["deg", "^\\circ"],
4100
- ["rarrow", "\\rightarrow"],
4101
- ["larrow", "\\leftarrow"],
4102
- ["lrarrow", "\\leftrightarrow"],
4103
- ["in", "\\in"],
4104
- ["notin", "\\notin"],
4105
- ["emptyset", "\\emptyset"],
4106
- ["subset", "\\subset"],
4107
- ["nsubset", "\\nsubseteq"],
4108
- ["cup", "\\cup"],
4109
- ["cap", "\\cap"],
4110
- ["smallinter", "\\cap"],
4111
- ["sim", "\\sim"],
4112
- ["circ", "\\circ"],
4113
- ["bot", "\\perp"],
4114
- ["dyad", "\\overleftrightarrow"],
4115
- ["arch", "\\overset{\\frown}"]
4116
- ]);
4117
- function hwpEquationToLatex(equation) {
4118
- return convertEquation(equation.replace(/\0/g, "").trim(), 0);
4119
- }
4120
- function convertEquation(equation, depth) {
4121
- if (!equation || depth > 12) return equation;
4122
- let result = equation.replace(/\s+/g, " ").replace(/`+/g, "\\,").replace(/~+/g, "\\,").trim();
4123
- result = convertMatrixLike(result);
4124
- result = convertRoots(result, depth);
4125
- result = convertOver(result, depth);
4126
- result = convertSqrt(result, depth);
4127
- result = convertScripts(result);
4128
- result = convertOperators(result);
4129
- result = removeFontDirectives(result);
4130
- result = convertWords(result);
4131
- result = cleanupLatexSpacing(result);
4132
- return result;
4133
- }
4134
- function convertMatrixLike(input) {
4135
- return input.replace(
4136
- /\bmatrix\s*\{([^{}]*)\}/gi,
4137
- (_match, body) => `\\begin{matrix} ${body.split("#").map((part) => part.trim()).join(" & ")} \\end{matrix}`
4138
- ).replace(
4139
- /\bcases\s*\{([^{}]*)\}/gi,
4140
- (_match, body) => `\\begin{cases} ${body.split("#").map((part) => part.trim()).join(" \\\\ ")} \\end{cases}`
4141
- );
4142
- }
4143
- function convertRoots(input, depth) {
4144
- return input.replace(/(?<!\\)\broot\s+({[^{}]*}|\S+)\s+of\s+({[^{}]*}|\S+)/gi, (_match, degree, radicand) => {
4145
- return `\\sqrt[${convertEquation(unwrapGroup(degree), depth + 1)}]{${convertEquation(unwrapGroup(radicand), depth + 1)}}`;
4146
- });
4147
- }
4148
- function convertSqrt(input, depth) {
4149
- return input.replace(/(?<!\\)\bsqrt\s*({[^{}]*}|\S+)/gi, (_match, radicand) => {
4150
- return `\\sqrt{${convertEquation(unwrapGroup(radicand), depth + 1)}}`;
4151
- });
4152
- }
4153
- function convertOver(input, depth) {
4154
- let result = input;
4155
- for (let guard = 0; guard < 50; guard++) {
4156
- const over = findTopLevelWord(result, "over");
4157
- if (over < 0) break;
4158
- const left = readLeftAtom(result, over);
4159
- const right = readRightAtom(result, over + "over".length);
4160
- if (!left || !right) break;
4161
- const numerator = convertEquation(unwrapGroup(left.atom), depth + 1);
4162
- const denominator = convertEquation(unwrapGroup(right.atom), depth + 1);
4163
- result = result.slice(0, left.start) + `\\frac{${numerator}}{${denominator}}` + result.slice(right.end);
4164
- }
4165
- return result;
4166
- }
4167
- function convertScripts(input) {
4168
- return input.replace(/\s*\^\s*/g, "^").replace(/\s*_\s*/g, "_").replace(/\^(?!\{)([^\s{}_^]+)/g, "^{$1}").replace(/_(?!\{)([^\s{}_^]+)/g, "_{$1}");
4169
- }
4170
- function convertOperators(input) {
4171
- return input.replace(/\+-/g, "\\pm").replace(/-\+/g, "\\mp").replace(/\/\//g, "\\parallel").replace(/△/g, "\\triangle ").replace(/□/g, "\\square ").replace(/‧/g, "\\cdot ").replace(/!=/g, "\\neq").replace(/<=/g, "\\leq").replace(/>=/g, "\\geq").replace(/==/g, "\\equiv");
4172
- }
4173
- function removeFontDirectives(input) {
4174
- return input.replace(/(?<!\\)\b(?:rm|it)\b\s*/gi, "");
4175
- }
4176
- function convertWords(input) {
3956
+ var WORD_ALIASES = {
3957
+ // 삼각/로그 함수 — 정본 맵 부재
3958
+ sin: "\\sin",
3959
+ cos: "\\cos",
3960
+ tan: "\\tan",
3961
+ sec: "\\sec",
3962
+ csc: "\\csc",
3963
+ cot: "\\cot",
3964
+ log: "\\log",
3965
+ ln: "\\ln",
3966
+ // 연산자·관계 기호 (정본은 대문자 키만 보유)
3967
+ divide: "\\div",
3968
+ div: "\\div",
3969
+ le: "\\leq",
3970
+ ge: "\\geq",
3971
+ geq: "\\geq",
3972
+ deg: "^\\circ",
3973
+ rarrow: "\\rightarrow",
3974
+ lrarrow: "\\leftrightarrow",
3975
+ rightarrow: "\\rightarrow",
3976
+ leftarrow: "\\leftarrow",
3977
+ in: "\\in",
3978
+ notin: "\\notin",
3979
+ emptyset: "\\emptyset",
3980
+ subset: "\\subset",
3981
+ nsubset: "\\nsubseteq",
3982
+ cup: "\\cup",
3983
+ cap: "\\cap",
3984
+ smallinter: "\\cap",
3985
+ smallsum: "\\sum",
3986
+ sim: "\\sim",
3987
+ circ: "\\circ",
3988
+ bot: "\\perp",
3989
+ partial: "\\partial",
3990
+ nabla: "\\nabla",
3991
+ angle: "\\angle",
3992
+ triangle: "\\triangle",
3993
+ inf: "\\infty",
3994
+ left: "\\left",
3995
+ right: "\\right",
3996
+ // 글꼴 지시자 — 제거
3997
+ rm: "",
3998
+ it: ""
3999
+ };
4000
+ var STRUCTURAL_FOLD = /* @__PURE__ */ new Set(["over", "root", "of"]);
4001
+ function normalizeWords(input) {
4177
4002
  return input.replace(/(?<![\\A-Za-z0-9])([A-Za-z][A-Za-z0-9]*)(?![A-Za-z0-9])/g, (word) => {
4178
- const exact = SYMBOL_WORDS.get(word);
4179
- if (exact) return exact;
4003
+ if (word in CONVERT_MAP || word in MIDDLE_CONVERT_MAP) return word;
4180
4004
  const lower = word.toLowerCase();
4181
- return SYMBOL_WORDS.get(lower) ?? WORD_COMMANDS.get(lower) ?? word;
4005
+ if (STRUCTURAL_FOLD.has(lower)) return lower;
4006
+ if (lower in WORD_ALIASES) return WORD_ALIASES[lower];
4007
+ if (lower !== word && (lower in CONVERT_MAP || lower in MIDDLE_CONVERT_MAP)) return lower;
4008
+ return word;
4182
4009
  });
4183
4010
  }
4184
- function cleanupLatexSpacing(input) {
4185
- return input.replace(/\\left\s*\{/g, "\\left\\{").replace(/\\right\s*\}/g, "\\right\\}").replace(/\\left\s*([\[\]\(\)\|])/g, "\\left$1").replace(/\\right\s*([\[\]\(\)\|])/g, "\\right$1").replace(/\s*\\,\s*/g, "\\,").replace(/\s+/g, " ").replace(/\{\s+/g, "{").replace(/\s+\}/g, "}").trim();
4186
- }
4187
- function findTopLevelWord(input, word) {
4188
- let curly = 0;
4189
- let paren = 0;
4190
- for (let i = 0; i <= input.length - word.length; i++) {
4191
- const ch = input[i];
4192
- if (ch === "{") curly++;
4193
- else if (ch === "}") curly = Math.max(0, curly - 1);
4194
- else if (ch === "(") paren++;
4195
- else if (ch === ")") paren = Math.max(0, paren - 1);
4196
- if (curly !== 0 || paren !== 0) continue;
4197
- if (input.slice(i, i + word.length).toLowerCase() !== word) continue;
4198
- if (isWordChar(input[i - 1]) || isWordChar(input[i + word.length])) continue;
4199
- return i;
4200
- }
4201
- return -1;
4202
- }
4203
- function readLeftAtom(input, end) {
4204
- let pos = end - 1;
4205
- while (pos >= 0 && /\s/.test(input[pos])) pos--;
4206
- if (pos < 0) return null;
4207
- if (input[pos] === "}") {
4208
- const start2 = findMatchingLeft(input, pos, "{", "}");
4209
- if (start2 >= 0) return { start: start2, atom: input.slice(start2, pos + 1) };
4210
- }
4211
- if (input[pos] === ")") {
4212
- const start2 = findMatchingLeft(input, pos, "(", ")");
4213
- if (start2 >= 0) return { start: start2, atom: input.slice(start2, pos + 1) };
4214
- }
4215
- let start = pos;
4216
- while (start >= 0 && !/\s/.test(input[start]) && !/[+\-=<>]/.test(input[start])) start--;
4217
- return { start: start + 1, atom: input.slice(start + 1, pos + 1) };
4218
- }
4219
- function readRightAtom(input, start) {
4220
- let pos = start;
4221
- while (pos < input.length && /\s/.test(input[pos])) pos++;
4222
- if (pos >= input.length) return null;
4223
- if (input[pos] === "{") {
4224
- const end2 = findMatchingRight(input, pos, "{", "}");
4225
- if (end2 >= 0) return { end: end2 + 1, atom: input.slice(pos, end2 + 1) };
4226
- }
4227
- if (input[pos] === "(") {
4228
- const end2 = findMatchingRight(input, pos, "(", ")");
4229
- if (end2 >= 0) return { end: end2 + 1, atom: input.slice(pos, end2 + 1) };
4230
- }
4231
- let end = pos;
4232
- while (end < input.length && !/\s/.test(input[end]) && !/[+\-=<>]/.test(input[end])) end++;
4233
- return { end, atom: input.slice(pos, end) };
4234
- }
4235
- function findMatchingLeft(input, closeIndex, open, close) {
4236
- let depth = 0;
4237
- for (let i = closeIndex; i >= 0; i--) {
4238
- if (input[i] === close) depth++;
4239
- else if (input[i] === open) {
4240
- depth--;
4241
- if (depth === 0) return i;
4242
- }
4243
- }
4244
- return -1;
4245
- }
4246
- function findMatchingRight(input, openIndex, open, close) {
4247
- let depth = 0;
4248
- for (let i = openIndex; i < input.length; i++) {
4249
- if (input[i] === open) depth++;
4250
- else if (input[i] === close) {
4251
- depth--;
4252
- if (depth === 0) return i;
4253
- }
4254
- }
4255
- return -1;
4011
+ function normalizeScripts(input) {
4012
+ return input.replace(/\s*\^\s*/g, "^").replace(/\s*_\s*/g, "_").replace(/\^(?!\{)([^\s{}_^]+)/g, "^{$1}").replace(/_(?!\{)([^\s{}_^]+)/g, "_{$1}").replace(/([_^])/g, " $1 ");
4256
4013
  }
4257
- function unwrapGroup(input) {
4258
- const trimmed = input.trim();
4259
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed.slice(1, -1);
4260
- return trimmed;
4014
+ function normalizeOperators(input) {
4015
+ return input.replace(/\+-/g, " \\pm ").replace(/-\+/g, " \\mp ").replace(/\/\//g, " \\parallel ").replace(/!=/g, " \\neq ").replace(/<=/g, " \\leq ").replace(/>=/g, " \\geq ").replace(/==/g, " \\equiv ").replace(/△/g, " \\triangle ").replace(/□/g, " \\square ").replace(/‧/g, " \\cdot ");
4261
4016
  }
4262
- function isWordChar(ch) {
4263
- return !!ch && /[A-Za-z0-9_]/.test(ch);
4017
+ function hwpEquationToLatex(equation) {
4018
+ let s = equation.replace(/\0/g, "").replace(/\s+/g, " ").trim();
4019
+ if (!s) return "";
4020
+ s = normalizeScripts(s);
4021
+ s = normalizeOperators(s);
4022
+ s = normalizeWords(s);
4023
+ s = s.replace(/\broot\s+(?!\{)(\S+)\s+of(?![A-Za-z0-9])/gi, "root { $1 } of");
4024
+ return hmlToLatex(s).replace(/\s+/g, " ").trim();
4264
4025
  }
4265
4026
 
4266
4027
  // src/hwp5/parser.ts
@@ -17255,9 +17016,7 @@ function parseHwp3Document(buffer, _options) {
17255
17016
  const headReader = new Reader(Buffer.from(buffer));
17256
17017
  const header = readHeader(headReader);
17257
17018
  if (header.encrypted !== 0) {
17258
- const e = new Error("HWP3 \uBCF8\uBB38\uC774 \uC554\uD638\uB85C \uBCF4\uD638\uB418\uC5B4 \uC788\uC5B4 \uCD94\uCD9C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
17259
- e.code = "ENCRYPTED";
17260
- throw e;
17019
+ throw new KordocError("\uC554\uD638\uD654\uB41C HWP3 \uBB38\uC11C \u2014 \uBCF8\uBB38\uC774 \uC554\uD638\uB85C \uBCF4\uD638\uB418\uC5B4 \uC788\uC5B4 \uCD94\uCD9C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
17261
17020
  }
17262
17021
  headReader.skip(header.infoBlockLength);
17263
17022
  const tail = headReader.readToEnd();
@@ -17268,10 +17027,10 @@ function parseHwp3Document(buffer, _options) {
17268
17027
  body = inflateRawSync3(tail, { maxOutputLength: MAX_DECOMPRESS_SIZE3 });
17269
17028
  } catch (err) {
17270
17029
  if (err?.code === "ERR_BUFFER_TOO_LARGE") {
17271
- throw new Error(`HWP3 \uC555\uCD95 \uD574\uC81C \uACB0\uACFC\uAC00 \uCD5C\uB300 \uD5C8\uC6A9 \uD06C\uAE30(${MAX_DECOMPRESS_SIZE3 / 1024 / 1024}MB)\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4`);
17030
+ throw new KordocError(`HWP3 \uC555\uCD95 \uD574\uC81C \uACB0\uACFC\uAC00 \uCD5C\uB300 \uD5C8\uC6A9 \uD06C\uAE30(${MAX_DECOMPRESS_SIZE3 / 1024 / 1024}MB)\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4`);
17272
17031
  }
17273
17032
  const msg2 = err instanceof Error ? err.message : String(err);
17274
- throw new Error(`HWP3 \uC555\uCD95 \uD574\uC81C \uC2E4\uD328: ${msg2}`);
17033
+ throw new KordocError(`HWP3 \uC555\uCD95 \uD574\uC81C \uC2E4\uD328: ${msg2}`);
17275
17034
  }
17276
17035
  } else {
17277
17036
  body = tail;
@@ -17472,7 +17231,7 @@ function isDistributionSentinel(markdown) {
17472
17231
  }
17473
17232
 
17474
17233
  // src/xlsx/parser.ts
17475
- import JSZip3 from "jszip";
17234
+ import JSZip4 from "jszip";
17476
17235
  import { DOMParser } from "@xmldom/xmldom";
17477
17236
  var MAX_SHEETS = 100;
17478
17237
  var MAX_DECOMPRESS_SIZE4 = 100 * 1024 * 1024;
@@ -17757,7 +17516,7 @@ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex, keep
17757
17516
  }
17758
17517
  async function parseXlsxDocument(buffer, options) {
17759
17518
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
17760
- const zip = await JSZip3.loadAsync(buffer);
17519
+ const zip = await JSZip4.loadAsync(buffer);
17761
17520
  const warnings = [];
17762
17521
  const workbookFile = zip.file("xl/workbook.xml");
17763
17522
  if (!workbookFile) {
@@ -17778,6 +17537,10 @@ async function parseXlsxDocument(buffer, options) {
17778
17537
  try {
17779
17538
  dateXfs = parseStyleDateXfs(await stylesFile.async("text"));
17780
17539
  } catch {
17540
+ warnings.push({
17541
+ code: "PARTIAL_PARSE",
17542
+ message: "xl/styles.xml \uB0A0\uC9DC \uC11C\uC2DD \uD310\uC815 \uC2E4\uD328 \u2014 \uB0A0\uC9DC \uC140\uC774 \uC2DC\uB9AC\uC5BC \uC22B\uC790\uB85C \uCD9C\uB825\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4"
17543
+ });
17781
17544
  }
17782
17545
  }
17783
17546
  let relsMap = /* @__PURE__ */ new Map();
@@ -18474,17 +18237,7 @@ async function parseXlsDocument(buffer, options) {
18474
18237
  const globals = processGlobals(records);
18475
18238
  const warnings = [];
18476
18239
  if (globals.encrypted) {
18477
- return {
18478
- markdown: "",
18479
- blocks: [],
18480
- metadata: { pageCount: globals.sheets.length },
18481
- warnings: [
18482
- {
18483
- message: "XLS \uD30C\uC77C\uC774 \uC554\uD638\uD654\uB418\uC5B4 \uC788\uC5B4 \uD30C\uC2F1\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
18484
- code: "PARTIAL_PARSE"
18485
- }
18486
- ]
18487
- };
18240
+ throw new KordocError("XLS \uD30C\uC77C\uC774 \uC554\uD638\uD654\uB418\uC5B4 \uC788\uC5B4 \uD30C\uC2F1\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
18488
18241
  }
18489
18242
  const convertNum = globals.dateXfs.size > 0 ? (n, ixfe) => {
18490
18243
  const kind = globals.dateXfs.get(ixfe);
@@ -18543,41 +18296,13 @@ async function parseXlsDocument(buffer, options) {
18543
18296
  }
18544
18297
 
18545
18298
  // src/docx/parser.ts
18546
- import JSZip4 from "jszip";
18299
+ import JSZip5 from "jszip";
18547
18300
  import { DOMParser as DOMParser2 } from "@xmldom/xmldom";
18548
18301
 
18549
18302
  // src/docx/equation.ts
18550
- function lname(el) {
18551
- return el.localName || el.tagName?.replace(/^[^:]+:/, "") || "";
18552
- }
18553
- function kids(parent, name) {
18554
- const out = [];
18555
- const nodes = parent.childNodes;
18556
- if (!nodes) return out;
18557
- for (let i = 0; i < nodes.length; i++) {
18558
- const n = nodes[i];
18559
- if (n.nodeType !== 1) continue;
18560
- const el = n;
18561
- if (lname(el) === name) out.push(el);
18562
- }
18563
- return out;
18564
- }
18565
- function firstKid(parent, name) {
18566
- const list = kids(parent, name);
18567
- return list[0] ?? null;
18568
- }
18569
- function eachChild(parent) {
18570
- const out = [];
18571
- const nodes = parent.childNodes;
18572
- if (!nodes) return out;
18573
- for (let i = 0; i < nodes.length; i++) {
18574
- if (nodes[i].nodeType === 1) out.push(nodes[i]);
18575
- }
18576
- return out;
18577
- }
18578
18303
  function runToLatex(r) {
18579
18304
  let out = "";
18580
- for (const t of kids(r, "t")) out += t.textContent ?? "";
18305
+ for (const t of childrenByLocalName(r, "t")) out += t.textContent ?? "";
18581
18306
  return out;
18582
18307
  }
18583
18308
  var FUNC_NAMES = /* @__PURE__ */ new Set([
@@ -18698,13 +18423,13 @@ function grp(body) {
18698
18423
  }
18699
18424
  function childrenToLatex(parent) {
18700
18425
  let out = "";
18701
- for (const ch of eachChild(parent)) {
18426
+ for (const ch of elementChildren(parent)) {
18702
18427
  out += nodeToLatex(ch);
18703
18428
  }
18704
18429
  return out;
18705
18430
  }
18706
18431
  function nodeToLatex(el) {
18707
- const tag = lname(el);
18432
+ const tag = localName(el);
18708
18433
  switch (tag) {
18709
18434
  case "r":
18710
18435
  return runToLatex(el);
@@ -18720,33 +18445,33 @@ function nodeToLatex(el) {
18720
18445
  return childrenToLatex(el);
18721
18446
  // 분수
18722
18447
  case "f": {
18723
- const n = firstKid(el, "num");
18724
- const d = firstKid(el, "den");
18448
+ const n = findChildByLocalName(el, "num");
18449
+ const d = findChildByLocalName(el, "den");
18725
18450
  const num2 = n ? childrenToLatex(n) : "";
18726
18451
  const den = d ? childrenToLatex(d) : "";
18727
18452
  return "\\frac" + grp(num2) + grp(den);
18728
18453
  }
18729
18454
  // 첨자
18730
18455
  case "sSup": {
18731
- const e = firstKid(el, "e");
18732
- const sup = firstKid(el, "sup");
18456
+ const e = findChildByLocalName(el, "e");
18457
+ const sup = findChildByLocalName(el, "sup");
18733
18458
  return grp(e ? childrenToLatex(e) : "") + "^" + grp(sup ? childrenToLatex(sup) : "");
18734
18459
  }
18735
18460
  case "sSub": {
18736
- const e = firstKid(el, "e");
18737
- const sub = firstKid(el, "sub");
18461
+ const e = findChildByLocalName(el, "e");
18462
+ const sub = findChildByLocalName(el, "sub");
18738
18463
  return grp(e ? childrenToLatex(e) : "") + "_" + grp(sub ? childrenToLatex(sub) : "");
18739
18464
  }
18740
18465
  case "sSubSup": {
18741
- const e = firstKid(el, "e");
18742
- const sub = firstKid(el, "sub");
18743
- const sup = firstKid(el, "sup");
18466
+ const e = findChildByLocalName(el, "e");
18467
+ const sub = findChildByLocalName(el, "sub");
18468
+ const sup = findChildByLocalName(el, "sup");
18744
18469
  return grp(e ? childrenToLatex(e) : "") + "_" + grp(sub ? childrenToLatex(sub) : "") + "^" + grp(sup ? childrenToLatex(sup) : "");
18745
18470
  }
18746
18471
  case "sPre": {
18747
- const sub = firstKid(el, "sub");
18748
- const sup = firstKid(el, "sup");
18749
- const e = firstKid(el, "e");
18472
+ const sub = findChildByLocalName(el, "sub");
18473
+ const sup = findChildByLocalName(el, "sup");
18474
+ const e = findChildByLocalName(el, "e");
18750
18475
  const preSub = sub ? grp(childrenToLatex(sub)) : "{}";
18751
18476
  const preSup = sup ? grp(childrenToLatex(sup)) : "{}";
18752
18477
  const body = e ? childrenToLatex(e) : "";
@@ -18754,13 +18479,13 @@ function nodeToLatex(el) {
18754
18479
  }
18755
18480
  // 근호
18756
18481
  case "rad": {
18757
- const deg = firstKid(el, "deg");
18758
- const e = firstKid(el, "e");
18482
+ const deg = findChildByLocalName(el, "deg");
18483
+ const e = findChildByLocalName(el, "e");
18759
18484
  const body = e ? childrenToLatex(e) : "";
18760
- const radPr = firstKid(el, "radPr");
18485
+ const radPr = findChildByLocalName(el, "radPr");
18761
18486
  let hide = false;
18762
18487
  if (radPr) {
18763
- const degHide = firstKid(radPr, "degHide");
18488
+ const degHide = findChildByLocalName(radPr, "degHide");
18764
18489
  if (degHide) {
18765
18490
  const val = degHide.getAttribute("m:val") ?? degHide.getAttribute("val");
18766
18491
  hide = val === "1" || val === "on" || val === "true";
@@ -18771,13 +18496,13 @@ function nodeToLatex(el) {
18771
18496
  }
18772
18497
  // n-ary 연산자 (sum, prod, int, …)
18773
18498
  case "nary": {
18774
- const naryPr = firstKid(el, "naryPr");
18499
+ const naryPr = findChildByLocalName(el, "naryPr");
18775
18500
  let op = "\\int";
18776
18501
  let subHide = false;
18777
18502
  let supHide = false;
18778
18503
  let limLoc = "";
18779
18504
  if (naryPr) {
18780
- const chr = firstKid(naryPr, "chr");
18505
+ const chr = findChildByLocalName(naryPr, "chr");
18781
18506
  if (chr) {
18782
18507
  const v = chr.getAttribute("m:val") ?? chr.getAttribute("val") ?? "";
18783
18508
  if (v && NARY_MAP[v]) op = NARY_MAP[v];
@@ -18785,16 +18510,16 @@ function nodeToLatex(el) {
18785
18510
  } else {
18786
18511
  op = "\\int";
18787
18512
  }
18788
- const sh = firstKid(naryPr, "subHide");
18789
- const ph = firstKid(naryPr, "supHide");
18513
+ const sh = findChildByLocalName(naryPr, "subHide");
18514
+ const ph = findChildByLocalName(naryPr, "supHide");
18790
18515
  if (sh) subHide = onOffVal(sh.getAttribute("m:val") ?? sh.getAttribute("val"));
18791
18516
  if (ph) supHide = onOffVal(ph.getAttribute("m:val") ?? ph.getAttribute("val"));
18792
- const ll = firstKid(naryPr, "limLoc");
18517
+ const ll = findChildByLocalName(naryPr, "limLoc");
18793
18518
  if (ll) limLoc = ll.getAttribute("m:val") ?? ll.getAttribute("val") ?? "";
18794
18519
  }
18795
- const sub = firstKid(el, "sub");
18796
- const sup = firstKid(el, "sup");
18797
- const e = firstKid(el, "e");
18520
+ const sub = findChildByLocalName(el, "sub");
18521
+ const sup = findChildByLocalName(el, "sup");
18522
+ const e = findChildByLocalName(el, "e");
18798
18523
  const subStr = !subHide && sub ? childrenToLatex(sub) : "";
18799
18524
  const supStr = !supHide && sup ? childrenToLatex(sup) : "";
18800
18525
  const body = e ? childrenToLatex(e) : "";
@@ -18810,27 +18535,27 @@ function nodeToLatex(el) {
18810
18535
  }
18811
18536
  // 괄호 (delimiter)
18812
18537
  case "d": {
18813
- const dPr = firstKid(el, "dPr");
18538
+ const dPr = findChildByLocalName(el, "dPr");
18814
18539
  let beg = "(";
18815
18540
  let end = ")";
18816
18541
  let sep = ",";
18817
18542
  if (dPr) {
18818
- const begChr = firstKid(dPr, "begChr");
18819
- const endChr = firstKid(dPr, "endChr");
18820
- const sepChr = firstKid(dPr, "sepChr");
18543
+ const begChr = findChildByLocalName(dPr, "begChr");
18544
+ const endChr = findChildByLocalName(dPr, "endChr");
18545
+ const sepChr = findChildByLocalName(dPr, "sepChr");
18821
18546
  if (begChr) beg = begChr.getAttribute("m:val") ?? begChr.getAttribute("val") ?? beg;
18822
18547
  if (endChr) end = endChr.getAttribute("m:val") ?? endChr.getAttribute("val") ?? end;
18823
18548
  if (sepChr) sep = sepChr.getAttribute("m:val") ?? sepChr.getAttribute("val") ?? sep;
18824
18549
  }
18825
- const items = kids(el, "e").map(childrenToLatex);
18550
+ const items = childrenByLocalName(el, "e").map(childrenToLatex);
18826
18551
  const body = items.join(sep);
18827
18552
  return "\\left" + mapDelim(beg, true) + body + "\\right" + mapDelim(end, false);
18828
18553
  }
18829
18554
  // 행렬
18830
18555
  case "m": {
18831
18556
  const rows = [];
18832
- for (const mr of kids(el, "mr")) {
18833
- const cells = kids(mr, "e").map(childrenToLatex);
18557
+ for (const mr of childrenByLocalName(el, "mr")) {
18558
+ const cells = childrenByLocalName(mr, "e").map(childrenToLatex);
18834
18559
  rows.push(cells.join(" & "));
18835
18560
  }
18836
18561
  return "\\begin{matrix}" + rows.join(" \\\\ ") + "\\end{matrix}";
@@ -18840,8 +18565,8 @@ function nodeToLatex(el) {
18840
18565
  return childrenToLatex(el);
18841
18566
  // 함수 적용 (sin, cos, log …)
18842
18567
  case "func": {
18843
- const fn = firstKid(el, "fName");
18844
- const e = firstKid(el, "e");
18568
+ const fn = findChildByLocalName(el, "fName");
18569
+ const e = findChildByLocalName(el, "e");
18845
18570
  const fnStr = fn ? childrenToLatex(fn).trim() : "";
18846
18571
  const body = e ? childrenToLatex(e) : "";
18847
18572
  const fnLatex = FUNC_NAMES.has(fnStr) ? "\\" + fnStr : fnStr;
@@ -18849,42 +18574,42 @@ function nodeToLatex(el) {
18849
18574
  }
18850
18575
  // 악센트 (hat/bar/vec/…)
18851
18576
  case "acc": {
18852
- const accPr = firstKid(el, "accPr");
18577
+ const accPr = findChildByLocalName(el, "accPr");
18853
18578
  let chr = "";
18854
18579
  if (accPr) {
18855
- const chrEl = firstKid(accPr, "chr");
18580
+ const chrEl = findChildByLocalName(accPr, "chr");
18856
18581
  if (chrEl) chr = chrEl.getAttribute("m:val") ?? chrEl.getAttribute("val") ?? "";
18857
18582
  }
18858
18583
  if (!chr) chr = "\u0302";
18859
- const e = firstKid(el, "e");
18584
+ const e = findChildByLocalName(el, "e");
18860
18585
  const body = e ? childrenToLatex(e) : "";
18861
18586
  const cmd = ACCENT_MAP[chr] ?? "\\hat";
18862
18587
  return cmd + grp(body);
18863
18588
  }
18864
18589
  // bar (위/아래 줄)
18865
18590
  case "bar": {
18866
- const barPr = firstKid(el, "barPr");
18591
+ const barPr = findChildByLocalName(el, "barPr");
18867
18592
  let pos = "top";
18868
18593
  if (barPr) {
18869
- const posEl = firstKid(barPr, "pos");
18594
+ const posEl = findChildByLocalName(barPr, "pos");
18870
18595
  if (posEl) pos = posEl.getAttribute("m:val") ?? posEl.getAttribute("val") ?? pos;
18871
18596
  }
18872
- const e = firstKid(el, "e");
18597
+ const e = findChildByLocalName(el, "e");
18873
18598
  const body = e ? childrenToLatex(e) : "";
18874
18599
  return (pos === "bot" ? "\\underline" : "\\overline") + grp(body);
18875
18600
  }
18876
18601
  // lim 위/아래
18877
18602
  case "limLow": {
18878
- const e = firstKid(el, "e");
18879
- const lim = firstKid(el, "lim");
18603
+ const e = findChildByLocalName(el, "e");
18604
+ const lim = findChildByLocalName(el, "lim");
18880
18605
  const base = e ? childrenToLatex(e).trim() : "";
18881
18606
  const below = lim ? childrenToLatex(lim) : "";
18882
18607
  if (FUNC_NAMES.has(base)) return "\\" + base + "_" + grp(below);
18883
18608
  return base + "_" + grp(below);
18884
18609
  }
18885
18610
  case "limUpp": {
18886
- const e = firstKid(el, "e");
18887
- const lim = firstKid(el, "lim");
18611
+ const e = findChildByLocalName(el, "e");
18612
+ const lim = findChildByLocalName(el, "lim");
18888
18613
  const base = e ? childrenToLatex(e).trim() : "";
18889
18614
  const above = lim ? childrenToLatex(lim) : "";
18890
18615
  if (FUNC_NAMES.has(base)) return "\\" + base + "^" + grp(above);
@@ -18892,7 +18617,7 @@ function nodeToLatex(el) {
18892
18617
  }
18893
18618
  // group character (over/underset 비슷)
18894
18619
  case "groupChr":
18895
- return childrenToLatex(firstKid(el, "e") ?? el);
18620
+ return childrenToLatex(findChildByLocalName(el, "e") ?? el);
18896
18621
  // box/borderBox/phantom/eqArr/… 는 자식 본문만 유지
18897
18622
  case "borderBox":
18898
18623
  case "phant":
@@ -18922,7 +18647,7 @@ function nodeToLatex(el) {
18922
18647
  }
18923
18648
  }
18924
18649
  function isOmmlRoot(el) {
18925
- const t = lname(el);
18650
+ const t = localName(el);
18926
18651
  return t === "oMath" || t === "oMathPara";
18927
18652
  }
18928
18653
  function ommlElementToLatex(el) {
@@ -18931,7 +18656,7 @@ function ommlElementToLatex(el) {
18931
18656
  return raw.replace(/\s+/g, " ").trim();
18932
18657
  }
18933
18658
  function isDisplayMath(el) {
18934
- return lname(el) === "oMathPara";
18659
+ return localName(el) === "oMathPara";
18935
18660
  }
18936
18661
 
18937
18662
  // src/docx/parser.ts
@@ -19446,7 +19171,7 @@ function emitParagraphImages(p, imageMap, linked, out) {
19446
19171
  }
19447
19172
  async function parseDocxDocument(buffer, options) {
19448
19173
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE5);
19449
- const zip = await JSZip4.loadAsync(buffer);
19174
+ const zip = await JSZip5.loadAsync(buffer);
19450
19175
  const warnings = [];
19451
19176
  const docFile = zip.file("word/document.xml");
19452
19177
  if (!docFile) {
@@ -19561,13 +19286,12 @@ async function parseDocxDocument(buffer, options) {
19561
19286
 
19562
19287
  // src/hwpml/parser.ts
19563
19288
  import { DOMParser as DOMParser3 } from "@xmldom/xmldom";
19564
- var MAX_XML_DEPTH2 = 200;
19565
19289
  var MAX_TABLE_ROWS = 5e3;
19566
19290
  var MAX_TABLE_COLS = 500;
19567
19291
  var MAX_HWPML_BYTES = 50 * 1024 * 1024;
19568
19292
  function parseHwpmlDocument(buffer, options) {
19569
19293
  if (buffer.byteLength > MAX_HWPML_BYTES) {
19570
- throw new Error(`HWPML \uD30C\uC77C \uD06C\uAE30 \uCD08\uACFC (${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB > 50MB)`);
19294
+ throw new KordocError(`HWPML \uD30C\uC77C \uD06C\uAE30 \uCD08\uACFC (${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB > 50MB)`);
19571
19295
  }
19572
19296
  const text = new TextDecoder("utf-8").decode(buffer).replace(/^\uFEFF/, "");
19573
19297
  const normalized = text.replace(/&nbsp;/g, "&#160;");
@@ -19584,17 +19308,17 @@ function parseHwpmlDocument(buffer, options) {
19584
19308
  }
19585
19309
  const root = doc.documentElement;
19586
19310
  const metadata = {};
19587
- const docSummary = findChild(root, "DOCSUMMARY");
19311
+ const docSummary = findChildByLocalName(root, "DOCSUMMARY");
19588
19312
  if (docSummary) {
19589
- const title = findChild(docSummary, "TITLE");
19590
- const author = findChild(docSummary, "AUTHOR");
19591
- const date = findChild(docSummary, "DATE");
19592
- if (title) metadata.title = textContent(title).trim();
19593
- if (author) metadata.author = textContent(author).trim();
19594
- if (date) metadata.createdAt = textContent(date).trim() || void 0;
19313
+ const title = findChildByLocalName(docSummary, "TITLE");
19314
+ const author = findChildByLocalName(docSummary, "AUTHOR");
19315
+ const date = findChildByLocalName(docSummary, "DATE");
19316
+ if (title) metadata.title = rawTextContent(title).trim();
19317
+ if (author) metadata.author = rawTextContent(author).trim();
19318
+ if (date) metadata.createdAt = rawTextContent(date).trim() || void 0;
19595
19319
  }
19596
19320
  const paraShapeMap = buildParaShapeMap(root);
19597
- const body = findChild(root, "BODY");
19321
+ const body = findChildByLocalName(root, "BODY");
19598
19322
  if (!body) {
19599
19323
  return { markdown: "", blocks: [], metadata, warnings };
19600
19324
  }
@@ -19622,11 +19346,11 @@ function parseHwpmlDocument(buffer, options) {
19622
19346
  }
19623
19347
  function buildParaShapeMap(root) {
19624
19348
  const map = /* @__PURE__ */ new Map();
19625
- const head = findChild(root, "HEAD");
19349
+ const head = findChildByLocalName(root, "HEAD");
19626
19350
  if (!head) return map;
19627
- const mappingTable = findChild(head, "MAPPINGTABLE");
19351
+ const mappingTable = findChildByLocalName(head, "MAPPINGTABLE");
19628
19352
  if (!mappingTable) return map;
19629
- const paraShapeList = findChild(mappingTable, "PARASHAPELIST");
19353
+ const paraShapeList = findChildByLocalName(mappingTable, "PARASHAPELIST");
19630
19354
  if (!paraShapeList) return map;
19631
19355
  const children = paraShapeList.childNodes;
19632
19356
  for (let i = 0; i < children.length; i++) {
@@ -19648,7 +19372,7 @@ function parseSection2(section, blocks, paraShapeMap, sectionNum, warnings, keep
19648
19372
  walkContent(section, blocks, paraShapeMap, sectionNum, warnings, false, keep);
19649
19373
  }
19650
19374
  function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, keep, depth = 0) {
19651
- if (depth > MAX_XML_DEPTH2) return;
19375
+ if (depth > MAX_XML_DEPTH) return;
19652
19376
  const children = node.childNodes;
19653
19377
  for (let i = 0; i < children.length; i++) {
19654
19378
  const el = children[i];
@@ -19678,7 +19402,7 @@ function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderF
19678
19402
  }
19679
19403
  }
19680
19404
  function walkTablesInP(node, blocks, paraShapeMap, sectionNum, warnings, keep, depth = 0) {
19681
- if (depth > MAX_XML_DEPTH2) return;
19405
+ if (depth > MAX_XML_DEPTH) return;
19682
19406
  const children = node.childNodes;
19683
19407
  for (let i = 0; i < children.length; i++) {
19684
19408
  const el = children[i];
@@ -19709,14 +19433,14 @@ function extractParagraphText(p) {
19709
19433
  return parts.join("").trim();
19710
19434
  }
19711
19435
  function collectCharText(node, parts, depth = 0) {
19712
- if (depth > MAX_XML_DEPTH2) return;
19436
+ if (depth > MAX_XML_DEPTH) return;
19713
19437
  const children = node.childNodes;
19714
19438
  for (let i = 0; i < children.length; i++) {
19715
19439
  const el = children[i];
19716
19440
  if (el.nodeType !== 1) continue;
19717
19441
  const tag = localName(el);
19718
19442
  if (tag === "CHAR") {
19719
- const t = textContent(el);
19443
+ const t = rawTextContent(el);
19720
19444
  if (t) parts.push(t);
19721
19445
  } else if (tag === "TABLE" || tag === "PICTURE" || tag === "SHAPEOBJECT") {
19722
19446
  } else if (tag === "AUTONUM") {
@@ -19725,14 +19449,28 @@ function collectCharText(node, parts, depth = 0) {
19725
19449
  }
19726
19450
  }
19727
19451
  }
19728
- function parseTable2(el, blocks, paraShapeMap, sectionNum, warnings, keep) {
19452
+ function parseTable2(el, blocks, _paraShapeMap, sectionNum, warnings, keep) {
19453
+ const table2 = buildHmlTable(el, warnings, keep, 0);
19454
+ if (!table2) return;
19455
+ const caption = extractShapeCaption(el);
19456
+ if (caption.text && caption.before) {
19457
+ blocks.push({ type: "paragraph", text: caption.text, pageNumber: sectionNum });
19458
+ }
19459
+ blocks.push({ type: "table", table: table2, pageNumber: sectionNum });
19460
+ if (caption.text && !caption.before) {
19461
+ blocks.push({ type: "paragraph", text: caption.text, pageNumber: sectionNum });
19462
+ }
19463
+ }
19464
+ var MAX_TABLE_NEST = 8;
19465
+ function buildHmlTable(el, warnings, keep, tableDepth) {
19729
19466
  const cells = [];
19467
+ const structured = [];
19730
19468
  const rowCount = parseInt(el.getAttribute("RowCount") ?? "0", 10);
19731
19469
  const colCount = parseInt(el.getAttribute("ColCount") ?? "0", 10);
19732
- if (isNaN(rowCount) || isNaN(colCount) || rowCount === 0 || colCount === 0) return;
19470
+ if (isNaN(rowCount) || isNaN(colCount) || rowCount === 0 || colCount === 0) return null;
19733
19471
  if (rowCount > MAX_TABLE_ROWS || colCount > MAX_TABLE_COLS) {
19734
19472
  warnings.push({ message: `\uD14C\uC774\uBE14 \uD06C\uAE30 \uCD08\uACFC (${rowCount}x${colCount}) \u2014 \uC2A4\uD0B5`, code: "TRUNCATED_TABLE" });
19735
- return;
19473
+ return null;
19736
19474
  }
19737
19475
  const children = el.childNodes;
19738
19476
  for (let i = 0; i < children.length; i++) {
@@ -19747,28 +19485,106 @@ function parseTable2(el, blocks, paraShapeMap, sectionNum, warnings, keep) {
19747
19485
  const colSpan = Math.min(Math.max(1, parseInt(cellEl.getAttribute("ColSpan") ?? "1", 10) || 1), MAX_TABLE_COLS);
19748
19486
  const rowSpan = Math.min(Math.max(1, parseInt(cellEl.getAttribute("RowSpan") ?? "1", 10) || 1), MAX_TABLE_ROWS);
19749
19487
  const cellText = extractCellText(cellEl);
19750
- cells.push({ text: cellText, colSpan, rowSpan, colAddr, rowAddr });
19488
+ const ctx = { text: cellText, colSpan, rowSpan, colAddr, rowAddr };
19489
+ cells.push(ctx);
19490
+ if (tableDepth < MAX_TABLE_NEST && cellHasNestedTable(cellEl, 0)) {
19491
+ const cellBlocks = [];
19492
+ collectCellBlocks(cellEl, cellBlocks, warnings, keep, 0, tableDepth);
19493
+ if (cellBlocks.some((b) => b.type === "table")) structured.push({ ctx, cellBlocks });
19494
+ }
19751
19495
  }
19752
19496
  }
19753
- if (cells.length === 0) return;
19497
+ if (cells.length === 0) return null;
19754
19498
  const cellRows = Array.from({ length: rowCount }, () => []);
19755
19499
  for (const cell2 of cells) {
19756
19500
  const r = Math.min(Math.max(cell2.rowAddr ?? 0, 0), rowCount - 1);
19757
19501
  cellRows[r].push(cell2);
19758
19502
  }
19759
19503
  const table2 = buildTable(cellRows, { keepAnchoredEmptyCols: keep });
19760
- const caption = extractShapeCaption(el);
19761
- if (caption.text && caption.before) {
19762
- blocks.push({ type: "paragraph", text: caption.text, pageNumber: sectionNum });
19504
+ if (structured.length > 0) {
19505
+ const claimed = /* @__PURE__ */ new Set();
19506
+ for (const { ctx, cellBlocks } of structured) {
19507
+ let target = table2.cells[ctx.rowAddr ?? -1]?.[ctx.colAddr ?? -1];
19508
+ if (!target || claimed.has(target) || target.text !== ctx.text) {
19509
+ target = void 0;
19510
+ outer: for (const irRow of table2.cells) {
19511
+ for (const cand of irRow) {
19512
+ if (!claimed.has(cand) && cand.text === ctx.text && cand.colSpan === ctx.colSpan && cand.rowSpan === ctx.rowSpan) {
19513
+ target = cand;
19514
+ break outer;
19515
+ }
19516
+ }
19517
+ }
19518
+ }
19519
+ if (!target) continue;
19520
+ claimed.add(target);
19521
+ target.blocks = cellBlocks;
19522
+ }
19763
19523
  }
19764
- blocks.push({ type: "table", table: table2, pageNumber: sectionNum });
19765
- if (caption.text && !caption.before) {
19766
- blocks.push({ type: "paragraph", text: caption.text, pageNumber: sectionNum });
19524
+ return table2;
19525
+ }
19526
+ function cellHasNestedTable(node, depth) {
19527
+ if (depth > 20) return false;
19528
+ const children = node.childNodes;
19529
+ for (let i = 0; i < children.length; i++) {
19530
+ const el = children[i];
19531
+ if (el.nodeType !== 1) continue;
19532
+ const tag = localName(el);
19533
+ if (tag === "TABLE") return true;
19534
+ if (tag === "FOOTNOTE" || tag === "ENDNOTE" || tag === "HEADER" || tag === "FOOTER") continue;
19535
+ if (cellHasNestedTable(el, depth + 1)) return true;
19536
+ }
19537
+ return false;
19538
+ }
19539
+ function collectCellBlocks(node, out, warnings, keep, depth, tableDepth) {
19540
+ if (depth > 20) return;
19541
+ const children = node.childNodes;
19542
+ for (let i = 0; i < children.length; i++) {
19543
+ const el = children[i];
19544
+ if (el.nodeType !== 1) continue;
19545
+ const tag = localName(el);
19546
+ if (tag === "P") {
19547
+ const t = extractParagraphText(el);
19548
+ if (t) out.push({ type: "paragraph", text: t });
19549
+ collectNestedTableBlocks(el, out, warnings, keep, depth + 1, tableDepth);
19550
+ } else if (tag === "TABLE") {
19551
+ pushNestedTableBlock(el, out, warnings, keep, tableDepth);
19552
+ } else {
19553
+ collectCellBlocks(el, out, warnings, keep, depth + 1, tableDepth);
19554
+ }
19555
+ }
19556
+ }
19557
+ function collectNestedTableBlocks(node, out, warnings, keep, depth, tableDepth) {
19558
+ if (depth > 20) return;
19559
+ const children = node.childNodes;
19560
+ for (let i = 0; i < children.length; i++) {
19561
+ const el = children[i];
19562
+ if (el.nodeType !== 1) continue;
19563
+ const tag = localName(el);
19564
+ if (tag === "TABLE") {
19565
+ pushNestedTableBlock(el, out, warnings, keep, tableDepth);
19566
+ continue;
19567
+ }
19568
+ if (tag === "FOOTNOTE" || tag === "ENDNOTE" || tag === "HEADER" || tag === "FOOTER") continue;
19569
+ collectNestedTableBlocks(el, out, warnings, keep, depth + 1, tableDepth);
19570
+ }
19571
+ }
19572
+ function pushNestedTableBlock(el, out, warnings, keep, tableDepth) {
19573
+ const nested = buildHmlTable(el, warnings, keep, tableDepth + 1);
19574
+ if (nested) {
19575
+ const cap = extractShapeCaption(el);
19576
+ if (cap.text) nested.caption = cap.text;
19577
+ out.push({ type: "table", table: nested });
19578
+ return;
19767
19579
  }
19580
+ const parts = [];
19581
+ collectCellText2(el, parts, 0);
19582
+ const flat = parts.filter(Boolean).join("\n").trim();
19583
+ if (flat) out.push({ type: "paragraph", text: flat });
19768
19584
  }
19769
19585
  function extractShapeCaption(tableEl) {
19770
- const shape = findChild(tableEl, "SHAPEOBJECT");
19771
- const caption = shape && findChild(shape, "CAPTION");
19586
+ const shape = findChildByLocalName(tableEl, "SHAPEOBJECT");
19587
+ const caption = shape && findChildByLocalName(shape, "CAPTION");
19772
19588
  if (!caption) return { text: "", before: false };
19773
19589
  const parts = [];
19774
19590
  collectCellText2(caption, parts, 0);
@@ -19813,30 +19629,6 @@ function collectNestedTableText(node, parts, depth) {
19813
19629
  collectNestedTableText(el, parts, depth + 1);
19814
19630
  }
19815
19631
  }
19816
- function localName(el) {
19817
- return (el.tagName || el.localName || "").replace(/^[^:]+:/, "");
19818
- }
19819
- function findChild(parent, tag) {
19820
- const children = parent.childNodes;
19821
- for (let i = 0; i < children.length; i++) {
19822
- const el = children[i];
19823
- if (el.nodeType === 1 && localName(el) === tag) return el;
19824
- }
19825
- return null;
19826
- }
19827
- function textContent(el) {
19828
- const children = el.childNodes;
19829
- const parts = [];
19830
- for (let i = 0; i < children.length; i++) {
19831
- const node = children[i];
19832
- if (node.nodeType === 3) {
19833
- parts.push(node.nodeValue || "");
19834
- } else if (node.nodeType === 1) {
19835
- parts.push(textContent(node));
19836
- }
19837
- }
19838
- return parts.join("");
19839
- }
19840
19632
  function countSections(body) {
19841
19633
  let count = 0;
19842
19634
  const children = body.childNodes;
@@ -20586,10 +20378,136 @@ function fillInlineFields(text, values, filled, matchedLabels, blockedLabels) {
20586
20378
  }
20587
20379
 
20588
20380
  // src/form/filler-hwpx.ts
20589
- import JSZip5 from "jszip";
20381
+ import JSZip7 from "jszip";
20382
+
20383
+ // src/form/click-here.ts
20384
+ import JSZip6 from "jszip";
20385
+ var FIELD_BEGIN_RE = /<([A-Za-z0-9_]+:)?fieldBegin\b([^>]*?)(\/?)>/g;
20386
+ function attrOf(attrs, name) {
20387
+ const m = attrs.match(new RegExp(`(?:^|\\s)${name}="([^"]*)"`));
20388
+ return m ? m[1] : void 0;
20389
+ }
20390
+ function extractPlaceholder(fieldBeginXml) {
20391
+ const m = fieldBeginXml.match(/Direction:wstring:(\d+):/);
20392
+ if (!m) return void 0;
20393
+ const start = (m.index ?? 0) + m[0].length;
20394
+ const n = parseInt(m[1], 10);
20395
+ if (!Number.isFinite(n) || n < 0 || n > 1e3) return void 0;
20396
+ return decodeXmlEntities(fieldBeginXml.slice(start, start + n));
20397
+ }
20398
+ function regionText(inner) {
20399
+ return decodeXmlEntities(
20400
+ inner.replace(/<(?:[A-Za-z0-9_]+:)?(?:lineBreak|br)\b[^>]*\/?>/g, "\n").replace(/<(?:[A-Za-z0-9_]+:)?tab\b[^>]*\/?>/g, " ").replace(/<[^>]+>/g, "")
20401
+ );
20402
+ }
20403
+ function scanClickHereRegions(xml) {
20404
+ const regions = [];
20405
+ FIELD_BEGIN_RE.lastIndex = 0;
20406
+ let m;
20407
+ let lastEnd = -1;
20408
+ while ((m = FIELD_BEGIN_RE.exec(xml)) !== null) {
20409
+ const prefix = m[1] ?? "";
20410
+ const attrs = m[2];
20411
+ const selfClosed = m[3] === "/";
20412
+ if ((attrOf(attrs, "type") ?? "").toUpperCase() !== "CLICK_HERE") continue;
20413
+ const name = attrOf(attrs, "name");
20414
+ if (name === void 0) continue;
20415
+ const id = attrOf(attrs, "id");
20416
+ let beginElemEnd;
20417
+ if (selfClosed) {
20418
+ beginElemEnd = FIELD_BEGIN_RE.lastIndex;
20419
+ } else {
20420
+ const closeTag = `</${prefix}fieldBegin>`;
20421
+ const ci = xml.indexOf(closeTag, FIELD_BEGIN_RE.lastIndex);
20422
+ if (ci < 0) continue;
20423
+ beginElemEnd = ci + closeTag.length;
20424
+ }
20425
+ const fieldBeginXml = xml.slice(m.index, beginElemEnd);
20426
+ const ctrlClose = `</${prefix}ctrl>`;
20427
+ let start = beginElemEnd;
20428
+ let wrapped = false;
20429
+ if (xml.startsWith(ctrlClose, beginElemEnd)) {
20430
+ start = beginElemEnd + ctrlClose.length;
20431
+ wrapped = true;
20432
+ }
20433
+ const feRe = new RegExp(`<${prefix}fieldEnd\\b[^>]*>`, "g");
20434
+ feRe.lastIndex = start;
20435
+ let end = -1;
20436
+ let fe;
20437
+ while ((fe = feRe.exec(xml)) !== null) {
20438
+ const ref = attrOf(fe[0], "beginIDRef");
20439
+ if (id !== void 0 && ref !== void 0 && ref !== id) continue;
20440
+ end = fe.index;
20441
+ break;
20442
+ }
20443
+ if (end < 0) continue;
20444
+ const ctrlOpen = `<${prefix}ctrl>`;
20445
+ if (wrapped && xml.slice(end - ctrlOpen.length, end) === ctrlOpen) {
20446
+ end -= ctrlOpen.length;
20447
+ }
20448
+ if (start > end) continue;
20449
+ if (start < lastEnd) continue;
20450
+ lastEnd = end;
20451
+ regions.push({
20452
+ name: decodeXmlEntities(name),
20453
+ placeholder: extractPlaceholder(fieldBeginXml),
20454
+ start,
20455
+ end,
20456
+ prefix
20457
+ });
20458
+ }
20459
+ return regions;
20460
+ }
20461
+ function valueXml(value, prefix) {
20462
+ if (value === "") return "";
20463
+ const lines = value.split("\n").map(escapeXmlText);
20464
+ return `<${prefix}t>${lines.join(`<${prefix}lineBreak/>`)}</${prefix}t>`;
20465
+ }
20466
+ function fillClickHereInXml(xml, cursor, blockedLabels) {
20467
+ const filled = [];
20468
+ const matchedKeys = /* @__PURE__ */ new Set();
20469
+ const splices = [];
20470
+ for (const region of scanClickHereRegions(xml)) {
20471
+ const key = normalizeLabel(region.name);
20472
+ if (!key || blockedLabels?.has(key)) continue;
20473
+ if (!cursor.has(key)) continue;
20474
+ const value = cursor.consume(key);
20475
+ if (value === void 0) continue;
20476
+ const replacement = valueXml(value, region.prefix);
20477
+ const current = xml.slice(region.start, region.end);
20478
+ matchedKeys.add(key);
20479
+ filled.push({ label: region.name, value, row: -1, col: -1, key });
20480
+ if (current === replacement) continue;
20481
+ splices.push({ start: region.start, end: region.end, replacement });
20482
+ }
20483
+ return {
20484
+ xml: splices.length > 0 ? applySplices(xml, splices) : null,
20485
+ filled,
20486
+ matchedKeys
20487
+ };
20488
+ }
20489
+ async function extractClickHereFields(hwpxBuffer) {
20490
+ const zip = await JSZip6.loadAsync(hwpxBuffer);
20491
+ const sectionPaths = Object.keys(zip.files).filter((name) => /[Ss]ection\d+\.xml$/i.test(name)).sort();
20492
+ const fields = [];
20493
+ for (let si = 0; si < sectionPaths.length; si++) {
20494
+ const xml = await zip.file(sectionPaths[si]).async("text");
20495
+ for (const r of scanClickHereRegions(xml)) {
20496
+ fields.push({
20497
+ name: r.name,
20498
+ ...r.placeholder !== void 0 ? { placeholder: r.placeholder } : {},
20499
+ value: regionText(xml.slice(r.start, r.end)),
20500
+ section: si
20501
+ });
20502
+ }
20503
+ }
20504
+ return fields;
20505
+ }
20506
+
20507
+ // src/form/filler-hwpx.ts
20590
20508
  async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20591
20509
  const u8 = new Uint8Array(hwpxBuffer);
20592
- const zip = await JSZip5.loadAsync(hwpxBuffer);
20510
+ const zip = await JSZip7.loadAsync(hwpxBuffer);
20593
20511
  const sectionPaths = Object.keys(zip.files).filter((name) => /[Ss]ection\d+\.xml$/i.test(name)).sort();
20594
20512
  if (sectionPaths.length === 0) {
20595
20513
  throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -20603,8 +20521,23 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20603
20521
  const succeededKeys = /* @__PURE__ */ new Set();
20604
20522
  const replacements = /* @__PURE__ */ new Map();
20605
20523
  const encoder = new TextEncoder();
20524
+ const sectionXmls = /* @__PURE__ */ new Map();
20525
+ const fieldModified = /* @__PURE__ */ new Set();
20526
+ const fieldMatchedKeys = /* @__PURE__ */ new Set();
20527
+ for (const p of sectionPaths) {
20528
+ const xml = await zip.file(p).async("text");
20529
+ const outcome = fillClickHereInXml(xml, cursor, blockedLabels);
20530
+ sectionXmls.set(p, outcome.xml ?? xml);
20531
+ if (outcome.xml !== null) fieldModified.add(p);
20532
+ for (const f of outcome.filled) filled.push(f);
20533
+ for (const k of outcome.matchedKeys) fieldMatchedKeys.add(k);
20534
+ }
20535
+ for (const k of fieldMatchedKeys) {
20536
+ matchedLabels.add(k);
20537
+ normalizedValues.delete(k);
20538
+ }
20606
20539
  for (let si = 0; si < sectionPaths.length; si++) {
20607
- const xml = await zip.file(sectionPaths[si]).async("text");
20540
+ const xml = sectionXmls.get(sectionPaths[si]);
20608
20541
  const scan = scanSectionXml(xml, si);
20609
20542
  const ledger = /* @__PURE__ */ new Map();
20610
20543
  const led = (p) => {
@@ -20786,94 +20719,460 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20786
20719
  }
20787
20720
  }
20788
20721
  }
20789
- for (const para2 of [...scan.bodyParagraphs, ...scan.excludedParagraphs]) {
20790
- const existing = ledger.get(para2);
20791
- if (existing?.fullText !== void 0) continue;
20792
- const text = matchText(para2);
20793
- const segments = scanInlineSegments(text);
20794
- const matches = segments.map((seg) => matchInlineSegment(seg, cursor, blockedLabels));
20795
- for (let i = 0; i < segments.length; i++) {
20796
- const seg = segments[i];
20797
- const matched = matches[i];
20798
- if (matched === void 0) continue;
20799
- const matchKey = matched.key;
20800
- const newValue = cursor.consume(matchKey);
20801
- if (newValue === void 0) continue;
20802
- const ve = clampSegmentEnd(text, seg, segments[i + 1], matches[i + 1]?.viaExt ?? false);
20803
- const replacement = seg.valueStart === ve ? padInsertion(text, seg.valueStart, newValue) : newValue;
20804
- const l = led(para2);
20805
- l.ranges.push({ start: seg.valueStart, end: ve, replacement });
20806
- matchedLabels.add(matchKey);
20807
- l.filledIdx.push(filled.length);
20808
- l.matchKeys.push(matchKey);
20809
- filled.push({ label: matched.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20810
- }
20722
+ for (const para2 of [...scan.bodyParagraphs, ...scan.excludedParagraphs]) {
20723
+ const existing = ledger.get(para2);
20724
+ if (existing?.fullText !== void 0) continue;
20725
+ const text = matchText(para2);
20726
+ const segments = scanInlineSegments(text);
20727
+ const matches = segments.map((seg) => matchInlineSegment(seg, cursor, blockedLabels));
20728
+ for (let i = 0; i < segments.length; i++) {
20729
+ const seg = segments[i];
20730
+ const matched = matches[i];
20731
+ if (matched === void 0) continue;
20732
+ const matchKey = matched.key;
20733
+ const newValue = cursor.consume(matchKey);
20734
+ if (newValue === void 0) continue;
20735
+ const ve = clampSegmentEnd(text, seg, segments[i + 1], matches[i + 1]?.viaExt ?? false);
20736
+ const replacement = seg.valueStart === ve ? padInsertion(text, seg.valueStart, newValue) : newValue;
20737
+ const l = led(para2);
20738
+ l.ranges.push({ start: seg.valueStart, end: ve, replacement });
20739
+ matchedLabels.add(matchKey);
20740
+ l.filledIdx.push(filled.length);
20741
+ l.matchKeys.push(matchKey);
20742
+ filled.push({ label: matched.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20743
+ }
20744
+ }
20745
+ const splices = [];
20746
+ for (const [para2, l] of ledger) {
20747
+ let paraSplices = null;
20748
+ if (l.fullText !== void 0) {
20749
+ paraSplices = buildParagraphSplices(para2, l.fullText, xml);
20750
+ } else if (l.ranges.length > 0) {
20751
+ const sorted = [...l.ranges].sort((a, b) => a.start - b.start || a.end - b.end);
20752
+ const merged = [];
20753
+ for (const r of sorted) {
20754
+ const prev = merged[merged.length - 1];
20755
+ if (prev && r.start < prev.end) continue;
20756
+ merged.push(r);
20757
+ }
20758
+ if (paraTText(para2, xml) !== null) {
20759
+ const precise = [];
20760
+ let ok = true;
20761
+ for (const r of merged) {
20762
+ const sp = buildRangeSplices(para2, xml, r.start, r.end, r.replacement);
20763
+ if (!sp) {
20764
+ ok = false;
20765
+ break;
20766
+ }
20767
+ precise.push(...sp);
20768
+ }
20769
+ paraSplices = ok ? precise : null;
20770
+ } else if (paraTextPureT(para2, xml)) {
20771
+ let text = para2.text;
20772
+ for (let k = merged.length - 1; k >= 0; k--) {
20773
+ const r = merged[k];
20774
+ text = text.slice(0, r.start) + r.replacement + text.slice(r.end);
20775
+ }
20776
+ paraSplices = buildParagraphSplices(para2, text, xml);
20777
+ } else {
20778
+ paraSplices = null;
20779
+ }
20780
+ }
20781
+ if (paraSplices === null) {
20782
+ for (const idx of l.filledIdx) filled[idx] = null;
20783
+ for (const k of l.matchKeys) failedKeys.add(k);
20784
+ continue;
20785
+ }
20786
+ for (const k of l.matchKeys) succeededKeys.add(k);
20787
+ splices.push(...paraSplices);
20788
+ }
20789
+ if (splices.length > 0 || fieldModified.has(sectionPaths[si])) {
20790
+ splices.push(...allLinesegRemovalSplices(xml));
20791
+ replacements.set(sectionPaths[si], encoder.encode(applySplices(xml, splices)));
20792
+ }
20793
+ }
20794
+ for (const k of failedKeys) {
20795
+ if (!succeededKeys.has(k) || cursor.isArray(k)) matchedLabels.delete(k);
20796
+ }
20797
+ const cleanFilled = filled.filter((f) => f !== null);
20798
+ const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
20799
+ const out = replacements.size > 0 ? patchZipEntries(u8, replacements) : new Uint8Array(u8);
20800
+ return {
20801
+ buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
20802
+ filled: cleanFilled,
20803
+ unmatched,
20804
+ ...warnings.length > 0 ? { warnings } : {}
20805
+ };
20806
+ }
20807
+
20808
+ // src/hwpx/generator.ts
20809
+ import JSZip8 from "jszip";
20810
+
20811
+ // src/hwpx/geometry.ts
20812
+ var A4_W_HU = 59528;
20813
+ var A4_H_HU = 84188;
20814
+ var HU_PER_MM = 7200 / 25.4;
20815
+ function mmToHwpunit(mm) {
20816
+ return Math.round(mm * 7200 / 25.4);
20817
+ }
20818
+ var BODY_WIDTH_20MM = mmToHwpunit(170);
20819
+ var COVER_MEASURED_W = 48180;
20820
+ var DATA_TABLE_ID_BASE = 1e3;
20821
+ var CHART_TABLE_ID_BASE = 91e5;
20822
+ var GJ_TABLE_ID_BASE = 92e5;
20823
+ var EXTRA_TABLE_ID_BASE = 93e5;
20824
+ var PIC_ID_BASE = 94e5;
20825
+
20826
+ // src/hwpx/gaejosik.ts
20827
+ var GAEJOSIK_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
20828
+ function gaejosikMarker(depth, bullet2 = "\u25CB") {
20829
+ if (depth === 1) return bullet2;
20830
+ return GAEJOSIK_BULLETS[Math.min(depth, GAEJOSIK_BULLETS.length - 1)];
20831
+ }
20832
+ function gaejosikSizes(bodyHeight, ov = {}) {
20833
+ const s = bodyHeight / 1500;
20834
+ const r = (v, pt) => pt ? Math.round(pt * 100) : Math.round(v * s);
20835
+ return {
20836
+ /** □ 대항목 — HY헤드라인M 16pt */
20837
+ dae: r(1600, ov.dae),
20838
+ /** ※ 참고 — 한양중고딕 13pt */
20839
+ cham: r(1300, ov.cham),
20840
+ /** 장 헤더(로마숫자·제목) 17pt */
20841
+ chapter: r(1700, ov.chapter),
20842
+ /** 표지 제목 30pt */
20843
+ coverTitle: r(3e3, ov.coverTitle),
20844
+ /** 표지 날짜·기관명 25pt */
20845
+ coverSub: r(2500, ov.coverSub),
20846
+ /** 목차 라벨 "목 차" 28pt */
20847
+ tocLabel: r(2800, ov.tocLabel),
20848
+ /** 목차 로마숫자 21pt(한양신명조) */
20849
+ tocRoman: r(2100, ov.tocRoman),
20850
+ /** 목차 항목 18pt */
20851
+ tocItem: r(1800, ov.tocItem),
20852
+ /** 표 셀 — 맑은 고딕 12pt (실측: 정부 양식 표) */
20853
+ table: r(1200, ov.table),
20854
+ /** 표지 장식 바 셀 빈 문단 — 6pt (실측: 바 높이 818 안에 줄높이 수납) */
20855
+ bar: r(600),
20856
+ /** 본문 첫 페이지 제목 박스 — HY헤드라인M 22pt (실측: GT3 표④) */
20857
+ bodyTitle: r(2200, ov.bodyTitle)
20858
+ };
20859
+ }
20860
+ function gaejosikSpaceBefore(depth, bodyHeight) {
20861
+ const s = bodyHeight / 1500;
20862
+ const ui = [3e3, 2e3, 1200, 600][Math.min(depth, 3)];
20863
+ return Math.round(ui * s);
20864
+ }
20865
+ function gaejosikLevelIndent(depth, bodyHeight, sizes = {}, bullet2 = "\u25CB") {
20866
+ const lefts = [0, 1, 1.5, 2];
20867
+ const left = depth <= 3 ? Math.round(lefts[depth] * bodyHeight) : Math.round((2 + (depth - 3) * 0.5) * bodyHeight);
20868
+ const h = depth === 0 ? gaejosikSizes(bodyHeight, sizes).dae : bodyHeight;
20869
+ return { left, indent: -markerWidth(gaejosikMarker(depth, bullet2), h) };
20870
+ }
20871
+ function gaejosikChamIndent(bodyHeight, sizes = {}) {
20872
+ const cham = gaejosikSizes(bodyHeight, sizes).cham;
20873
+ return { left: Math.round(2.5 * cham), indent: -markerWidth("\u203B", cham) };
20874
+ }
20875
+ function gaejosikTocItemIndent(bodyHeight, sizes = {}) {
20876
+ const sz = gaejosikSizes(bodyHeight, sizes);
20877
+ const hang = markerWidth("\u2167.", sz.tocRoman);
20878
+ return { left: 2e3 + hang, indent: -hang };
20879
+ }
20880
+ function formatGaejosikDate(d) {
20881
+ return `${d.getFullYear()}. ${d.getMonth() + 1}. ${d.getDate()}.`;
20882
+ }
20883
+ function stripChapterNumber(title) {
20884
+ return title.replace(/^\s*[□○ㅇㆍ-]\s+/u, "").replace(/^\s*(?:\d+(?:\.\d+)*|[Ⅰ-Ⅻ]+)\s*[.)]\s*/u, "").trim();
20885
+ }
20886
+ var GAEJOSIK_COLORS = {
20887
+ /** 장헤더 로마숫자 셀·표지 진한 바 음영 */
20888
+ primary: "#193AAA",
20889
+ /** 장헤더 셀 테두리 */
20890
+ border: "#006699",
20891
+ /** 표지 연한 바 */
20892
+ accent: "#A0B4E6",
20893
+ /** 장헤더 제목 셀 음영 */
20894
+ titleFill: "#F2F2F2",
20895
+ /** 장헤더 제목 셀 상하선 */
20896
+ titleLine: "#A6A6A6",
20897
+ /** 목차 박스 테두리 */
20898
+ tocBorder: "#514BAC",
20899
+ /** 목차 배너 라벤더 스트라이프 (실측: GT3 표②) */
20900
+ tocStripe: "#E0E5FA"
20901
+ };
20902
+ var GAEJOSIK_BASE_WIDTH = BODY_WIDTH_20MM;
20903
+ var CHAPTER_GEOM = { numW: 3327, gapW: 848, titleW: 43513, rowH: 2832 };
20904
+ var COVER_GEOM = {
20905
+ totalW: COVER_MEASURED_W,
20906
+ topDarkW: 38219,
20907
+ topLightW: 9961,
20908
+ barH: 818,
20909
+ titleH: 11060,
20910
+ botDarkW: 10466,
20911
+ botLightW: 37714,
20912
+ botBarH: 812
20913
+ };
20914
+ var TOC_GEOM = { boxW: 46492, boxH: 57840 };
20915
+ function chapterGeom(bodyHeight, ov = {}, bodyWidth = GAEJOSIK_BASE_WIDTH) {
20916
+ const s = gaejosikSizes(bodyHeight, ov).chapter / 1700;
20917
+ const ws = bodyWidth / GAEJOSIK_BASE_WIDTH;
20918
+ const numW = Math.round(CHAPTER_GEOM.numW * ws);
20919
+ const gapW = Math.round(CHAPTER_GEOM.gapW * ws);
20920
+ const titleW = Math.round((CHAPTER_GEOM.numW + CHAPTER_GEOM.gapW + CHAPTER_GEOM.titleW) * ws) - numW - gapW;
20921
+ return { numW, gapW, titleW, rowH: Math.round(CHAPTER_GEOM.rowH * s) };
20922
+ }
20923
+ function coverGeom(bodyHeight, ov = {}, bodyWidth = GAEJOSIK_BASE_WIDTH) {
20924
+ const s = gaejosikSizes(bodyHeight, ov).coverTitle / 3e3;
20925
+ const w = (v) => Math.round(v * bodyWidth / GAEJOSIK_BASE_WIDTH);
20926
+ const totalW = w(COVER_GEOM.totalW);
20927
+ return {
20928
+ ...COVER_GEOM,
20929
+ totalW,
20930
+ topDarkW: w(COVER_GEOM.topDarkW),
20931
+ topLightW: totalW - w(COVER_GEOM.topDarkW),
20932
+ botDarkW: w(COVER_GEOM.botDarkW),
20933
+ botLightW: totalW - w(COVER_GEOM.botDarkW),
20934
+ titleH: Math.round(COVER_GEOM.titleH * s)
20935
+ };
20936
+ }
20937
+ var TOC_BANNER_GEOM = {
20938
+ colWs: [565, 565, 1414, 13191, 1414, 565, 565],
20939
+ h: 3931
20940
+ };
20941
+ function tocBannerGeom(bodyHeight, ov = {}) {
20942
+ const s = gaejosikSizes(bodyHeight, ov).tocLabel / 2800;
20943
+ return { colWs: TOC_BANNER_GEOM.colWs, h: Math.round(TOC_BANNER_GEOM.h * s) };
20944
+ }
20945
+ var BODY_TITLE_GEOM = { barH: 600, titleH: 3566 };
20946
+ function bodyTitleGeom(bodyHeight, ov = {}) {
20947
+ const s = gaejosikSizes(bodyHeight, ov).bodyTitle / 2200;
20948
+ return { barH: BODY_TITLE_GEOM.barH, titleH: Math.round(BODY_TITLE_GEOM.titleH * s) };
20949
+ }
20950
+
20951
+ // src/hwpx/gongmun.ts
20952
+ var OFFICIAL_MARGINS = { top: 20, bottom: 15, left: 20, right: 15 };
20953
+ var GAEJOSIK_MARGINS = { top: 15, bottom: 15, left: 20, right: 20 };
20954
+ var GAEJOSIK_HEADER_FOOTER = 4251;
20955
+ var PRESET_DEFAULTS = {
20956
+ // 서울시 실결재 104건 중 12pt 64건(13pt 30, 15pt 5) — 지배값을 기본으로 사용.
20957
+ official: { bodyPt: 12, lineSpacing: 160, numbering: "standard" },
20958
+ report: { bodyPt: 15, lineSpacing: 160, numbering: "report" },
20959
+ // 실측 추진계획안: □ → ㅇ → * 계층.
20960
+ plan: { bodyPt: 15, lineSpacing: 160, numbering: "report" },
20961
+ notice: { bodyPt: 15, lineSpacing: 160, numbering: "standard" },
20962
+ minutes: { bodyPt: 14, lineSpacing: 130, numbering: "standard" },
20963
+ gaejosik: { bodyPt: 15, lineSpacing: 160, numbering: "gaejosik" },
20964
+ // 보도자료 — 실측(국토부 실물): 본문 바탕 14pt 160%, □→ㅇ→*(각주) 부호
20965
+ press: { bodyPt: 14, lineSpacing: 160, numbering: "report" }
20966
+ };
20967
+ var PRESET_ALIAS = {
20968
+ official: "official",
20969
+ \uAE30\uC548\uBB38: "official",
20970
+ \uC2DC\uD589\uBB38: "official",
20971
+ \uACF5\uBB38: "official",
20972
+ \uACF5\uBB38\uC11C: "official",
20973
+ report: "report",
20974
+ \uBCF4\uACE0\uC11C: "report",
20975
+ plan: "plan",
20976
+ \uACC4\uD68D\uC11C: "plan",
20977
+ \uACC4\uD68D: "plan",
20978
+ notice: "notice",
20979
+ \uD1B5\uC9C0: "notice",
20980
+ \uC54C\uB9BC: "notice",
20981
+ \uC548\uB0B4: "notice",
20982
+ minutes: "minutes",
20983
+ \uD68C\uC758\uB85D: "minutes",
20984
+ gaejosik: "gaejosik",
20985
+ \uAC1C\uC870\uC2DD: "gaejosik",
20986
+ \uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
20987
+ \uC815\uBD80\uBCF4\uACE0\uC11C: "gaejosik",
20988
+ \uC815\uBD80\uD45C\uC900\uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
20989
+ press: "press",
20990
+ \uBCF4\uB3C4\uC790\uB8CC: "press"
20991
+ };
20992
+ function normalizeGongmunPreset(preset) {
20993
+ if (!preset) return "official";
20994
+ return PRESET_ALIAS[preset.trim()] ?? "official";
20995
+ }
20996
+ function usesReportFonts(preset) {
20997
+ return preset === "gaejosik" || preset === "report" || preset === "plan";
20998
+ }
20999
+ function usesAsteriskThird(preset) {
21000
+ return preset === "plan" || preset === "press";
21001
+ }
21002
+ function needsGaejosikAssets(gongmun) {
21003
+ return usesReportFonts(gongmun.preset) || gongmun.cover !== null || gongmun.toc;
21004
+ }
21005
+ function assertFiniteRange(name, value, min, max) {
21006
+ if (!Number.isFinite(value) || value < min || value > max) {
21007
+ throw new KordocError(`${name} must be a finite number between ${min} and ${max}`);
21008
+ }
21009
+ }
21010
+ function validateGongmunOptions(opts) {
21011
+ if (opts.bodyPt !== void 0) assertFiniteRange("bodyPt", opts.bodyPt, 6, 40);
21012
+ if (opts.lineSpacing !== void 0) assertFiniteRange("lineSpacing", opts.lineSpacing, 50, 300);
21013
+ if (typeof opts.autoFit === "object" && opts.autoFit.minRatio !== void 0) {
21014
+ assertFiniteRange("autoFit.minRatio", opts.autoFit.minRatio, 50, 99);
21015
+ }
21016
+ if (opts.margins) {
21017
+ for (const side of ["top", "bottom", "left", "right"]) {
21018
+ const value = opts.margins[side];
21019
+ assertFiniteRange(`margins.${side}`, value, 0, 200);
21020
+ }
21021
+ if (opts.margins.left + opts.margins.right >= 210) {
21022
+ throw new KordocError("margins.left + margins.right must be less than 210");
20811
21023
  }
20812
- const splices = [];
20813
- for (const [para2, l] of ledger) {
20814
- let paraSplices = null;
20815
- if (l.fullText !== void 0) {
20816
- paraSplices = buildParagraphSplices(para2, l.fullText, xml);
20817
- } else if (l.ranges.length > 0) {
20818
- const sorted = [...l.ranges].sort((a, b) => a.start - b.start || a.end - b.end);
20819
- const merged = [];
20820
- for (const r of sorted) {
20821
- const prev = merged[merged.length - 1];
20822
- if (prev && r.start < prev.end) continue;
20823
- merged.push(r);
20824
- }
20825
- if (paraTText(para2, xml) !== null) {
20826
- const precise = [];
20827
- let ok = true;
20828
- for (const r of merged) {
20829
- const sp = buildRangeSplices(para2, xml, r.start, r.end, r.replacement);
20830
- if (!sp) {
20831
- ok = false;
20832
- break;
20833
- }
20834
- precise.push(...sp);
20835
- }
20836
- paraSplices = ok ? precise : null;
20837
- } else if (paraTextPureT(para2, xml)) {
20838
- let text = para2.text;
20839
- for (let k = merged.length - 1; k >= 0; k--) {
20840
- const r = merged[k];
20841
- text = text.slice(0, r.start) + r.replacement + text.slice(r.end);
20842
- }
20843
- paraSplices = buildParagraphSplices(para2, text, xml);
20844
- } else {
20845
- paraSplices = null;
20846
- }
20847
- }
20848
- if (paraSplices === null) {
20849
- for (const idx of l.filledIdx) filled[idx] = null;
20850
- for (const k of l.matchKeys) failedKeys.add(k);
20851
- continue;
20852
- }
20853
- for (const k of l.matchKeys) succeededKeys.add(k);
20854
- splices.push(...paraSplices);
21024
+ if (opts.margins.top + opts.margins.bottom >= 297) {
21025
+ throw new KordocError("margins.top + margins.bottom must be less than 297");
20855
21026
  }
20856
- if (splices.length > 0) {
20857
- splices.push(...allLinesegRemovalSplices(xml));
20858
- replacements.set(sectionPaths[si], encoder.encode(applySplices(xml, splices)));
21027
+ }
21028
+ if (opts.sizes) {
21029
+ for (const [key, value] of Object.entries(opts.sizes)) {
21030
+ if (value !== void 0) assertFiniteRange(`sizes.${key}`, value, 6, 60);
20859
21031
  }
20860
21032
  }
20861
- for (const k of failedKeys) {
20862
- if (!succeededKeys.has(k) || cursor.isArray(k)) matchedLabels.delete(k);
21033
+ if (opts.approval && opts.approval.length > 6) {
21034
+ throw new KordocError("approval must contain at most 6 labels");
20863
21035
  }
20864
- const cleanFilled = filled.filter((f) => f !== null);
20865
- const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
20866
- const out = replacements.size > 0 ? patchZipEntries(u8, replacements) : new Uint8Array(u8);
21036
+ }
21037
+ function incompatibleGongmunWarnings(opts) {
21038
+ const warns = [];
21039
+ const preset = normalizeGongmunPreset(opts.preset);
21040
+ if (opts.docHead && preset !== "official") warns.push(`doc_head(\uB450\uBB38)\uB294 \uAE30\uC548\uBB38(official) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
21041
+ if (opts.docFoot && preset !== "official") warns.push(`doc_foot(\uACB0\uBB38)\uB294 \uAE30\uC548\uBB38(official) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
21042
+ if (opts.noticeHead && preset !== "notice") warns.push(`notice_head(\uACF5\uACE0\uBC88\uD638\xB7\uBC1C\uC2E0\uBA85\uC758)\uB294 \uD1B5\uC9C0(notice) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
21043
+ if (opts.press && preset !== "press") warns.push(`press(\uBA38\uB9AC\uBC15\uC2A4\xB7\uBD80\uC81C\xB7\uB2F4\uB2F9)\uB294 \uBCF4\uB3C4\uC790\uB8CC(press) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
21044
+ if (preset === "press" && (opts.cover === true || typeof opts.cover === "object" || opts.toc === true)) {
21045
+ warns.push("\uBCF4\uB3C4\uC790\uB8CC\uB294 \uBA38\uB9AC\uBC15\uC2A4 \uC11C\uC2DD\uACFC \uC591\uB9BD \uBD88\uAC00\uB77C \uD45C\uC9C0\xB7\uBAA9\uCC28\uAC00 \uBB34\uC2DC\uB428");
21046
+ }
21047
+ if (opts.sizes && Object.keys(opts.sizes).length > 0 && !usesReportFonts(preset)) {
21048
+ warns.push(`sizes(\uAC1C\uC870\uC2DD \uC694\uC18C \uD06C\uAE30)\uB294 \uAC1C\uC870\uC2DD\xB7\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B\uC5D0\uC11C \uBB34\uC2DC\uB428`);
21049
+ }
21050
+ if (opts.suppressSingle && (opts.numbering ?? PRESET_DEFAULTS[preset].numbering) !== "standard") {
21051
+ warns.push(`suppress_single(\uB2E8\uC77C \uD615\uC81C \uBD80\uD638 \uC0DD\uB7B5)\uC740 \uBC95\uC815 \uBC88\uD638(standard) \uC804\uC6A9 \u2014 '${preset}' \uD504\uB9AC\uC14B(\uBD88\uB9BF \uCCB4\uACC4)\uC5D0\uC11C \uBB34\uB3D9\uC791`);
21052
+ }
21053
+ return warns;
21054
+ }
21055
+ function resolveGongmun(opts) {
21056
+ validateGongmunOptions(opts);
21057
+ const preset = normalizeGongmunPreset(opts.preset);
21058
+ const d = PRESET_DEFAULTS[preset];
21059
+ const bodyPt = opts.bodyPt ?? d.bodyPt;
21060
+ const autoFitMinRatio = opts.autoFit === false ? null : typeof opts.autoFit === "object" ? Math.min(Math.max(opts.autoFit.minRatio ?? 90, 50), 99) : 90;
21061
+ const coverOn = opts.cover !== void 0 ? opts.cover !== false : preset === "gaejosik";
21062
+ const coverOpts = typeof opts.cover === "object" ? opts.cover : {};
21063
+ const gaejosik = preset === "gaejosik";
21064
+ const reportFamily = gaejosik || preset === "report" || preset === "plan" || preset === "notice" || preset === "press";
20867
21065
  return {
20868
- buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
20869
- filled: cleanFilled,
20870
- unmatched,
20871
- ...warnings.length > 0 ? { warnings } : {}
21066
+ preset,
21067
+ bodyFont: opts.bodyFont ?? "myeongjo",
21068
+ bodyHeight: Math.round(bodyPt * 100),
21069
+ lineSpacing: opts.lineSpacing ?? d.lineSpacing,
21070
+ numbering: opts.numbering ?? d.numbering,
21071
+ margins: opts.margins ?? (reportFamily ? GAEJOSIK_MARGINS : OFFICIAL_MARGINS),
21072
+ centerTitle: opts.centerTitle ?? true,
21073
+ autoFitMinRatio,
21074
+ // 보도자료는 머리박스가 1페이지 최상단을 차지하는 서식이라 표지·목차와 양립 불가 —
21075
+ // 켜면 머리박스가 표지에 얹히고 25pt 제목·부제가 유실된다 (docHead 프리셋 게이팅과 동일 관례)
21076
+ cover: coverOn && preset !== "press" ? { date: coverOpts.date ?? null, org: coverOpts.org ?? "" } : null,
21077
+ toc: preset !== "press" && (opts.toc ?? gaejosik),
21078
+ fonts: opts.fonts ?? {},
21079
+ sizes: opts.sizes ?? {},
21080
+ // 쪽번호 — 보고서 계열 관행(실측: 2_보고서 양식·추진계획·공고문 전부 하단 중앙)
21081
+ pageNumbers: opts.pageNumbers ?? (gaejosik || preset === "report" || preset === "plan"),
21082
+ // 머리말·꼬리말 — 실측: 보고서 계열 15mm(GT3·t2·춘천·브라더), 공고·보도 10mm,
21083
+ // 기안문 0(실결재 41/60건 h0/f0)
21084
+ headerFooter: usesReportFonts(preset) ? GAEJOSIK_HEADER_FOOTER : preset === "notice" || preset === "press" ? 2835 : 0,
21085
+ // "끝." — 기안문 규정(본문 끝 2타+"끝."). 그 외는 opt-in
21086
+ endMark: opts.endMark ?? preset === "official",
21087
+ approval: opts.approval && opts.approval.length > 0 ? opts.approval : null,
21088
+ // 본문 제목박스 — 실측(GT3·GT12): 목차 뒤 본문 시작에 제목 반복. 표지 켜진 개조식 기본
21089
+ bodyTitleBox: opts.bodyTitleBox ?? (gaejosik && coverOn),
21090
+ // h2 말머리 — 실측: 보고서 양식 □ 대항목(QA-2), 공고문 아라비아("1. 사업개요", 바이오헬스 실측)
21091
+ h2Marker: opts.h2Marker ?? (preset === "report" || preset === "plan" ? "box" : preset === "notice" ? "number" : "none"),
21092
+ // 2단계 부호 — 실결재 기안문·공고문 ㅇ 지배(60건 중 ㅇ134:○5), 보고서 양식 계열 ○
21093
+ bullet2: opts.bullet2 ?? (preset === "plan" || preset === "notice" || preset === "press" ? "\u3147" : "\u25CB"),
21094
+ // 단일 형제 부호 생략 — 규정이지만 부호 없는 계단이 실무 눈에 어색 (실무자 QA)
21095
+ suppressSingle: opts.suppressSingle ?? false,
21096
+ docHead: preset === "official" && opts.docHead ? opts.docHead : null,
21097
+ docFoot: preset === "official" && opts.docFoot ? opts.docFoot : null,
21098
+ reportInfo: opts.reportInfo?.trim() || null,
21099
+ noticeHead: preset === "notice" && opts.noticeHead ? opts.noticeHead : null,
21100
+ // 보도자료 머리박스는 프리셋 자체가 요구 — 옵션 미지정이어도 빈 객체
21101
+ press: preset === "press" ? opts.press ?? {} : null
20872
21102
  };
20873
21103
  }
20874
-
20875
- // src/hwpx/generator.ts
20876
- import JSZip6 from "jszip";
21104
+ var REPORT_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
21105
+ var ASTERISK_BULLETS = ["\u25A1", "\u25CB", "*", "\u318D"];
21106
+ function standardMarker(depth, n) {
21107
+ switch (depth) {
21108
+ case 0:
21109
+ return `${n + 1}.`;
21110
+ case 1:
21111
+ return `${hangulOrdinal(n)}.`;
21112
+ case 2:
21113
+ return `${n + 1})`;
21114
+ case 3:
21115
+ return `${hangulOrdinal(n)})`;
21116
+ case 4:
21117
+ return `(${n + 1})`;
21118
+ case 5:
21119
+ return `(${hangulOrdinal(n)})`;
21120
+ case 6:
21121
+ return circledNumber(n);
21122
+ case 7:
21123
+ return circledHangul(n);
21124
+ default:
21125
+ return circledHangul(n);
21126
+ }
21127
+ }
21128
+ function reportMarker(depth, bullet2 = "\u25CB", asteriskThird = false) {
21129
+ const bullets = asteriskThird ? ASTERISK_BULLETS : REPORT_BULLETS;
21130
+ const m = bullets[Math.min(depth, bullets.length - 1)];
21131
+ return depth === 1 ? bullet2 : m;
21132
+ }
21133
+ function markerWidth(marker, bodyHeight) {
21134
+ let em = SPACE_EM_FIXED;
21135
+ for (const c of marker) em += charWidthEm1000(c.codePointAt(0));
21136
+ return Math.round(em / 1e3 * bodyHeight);
21137
+ }
21138
+ function levelIndent(depth, bodyHeight, numbering, sizes = {}, bullet2 = "\u25CB", asteriskThird = false) {
21139
+ if (numbering === "gaejosik") return gaejosikLevelIndent(depth, bodyHeight, sizes, bullet2);
21140
+ const marker = numbering === "report" ? reportMarker(depth, bullet2, asteriskThird) : standardMarker(depth, 0);
21141
+ return { left: Math.round(depth * bodyHeight), indent: -markerWidth(marker, bodyHeight) };
21142
+ }
21143
+ function computeSuppression(depths) {
21144
+ const counts = /* @__PURE__ */ new Map();
21145
+ const keys = [];
21146
+ const path = [];
21147
+ for (const depth of depths) {
21148
+ path.length = depth + 1;
21149
+ path[depth] = (path[depth] ?? 0) + 1;
21150
+ const parentKey = path.slice(0, depth).join(".") + "|" + depth;
21151
+ keys.push(parentKey);
21152
+ counts.set(parentKey, (counts.get(parentKey) ?? 0) + 1);
21153
+ }
21154
+ return keys.map((k) => (counts.get(k) ?? 0) <= 1);
21155
+ }
21156
+ var GongmunNumberer = class {
21157
+ constructor(numbering, bullet2 = "\u25CB", asteriskThird = false) {
21158
+ this.numbering = numbering;
21159
+ this.bullet2 = bullet2;
21160
+ this.asteriskThird = asteriskThird;
21161
+ }
21162
+ counts = [];
21163
+ /** depth 항목 하나에 대한 마커. suppress=true면 빈 문자열(부호 없음) */
21164
+ next(depth, suppress) {
21165
+ this.counts.length = depth + 1;
21166
+ const n = this.counts[depth] ?? 0;
21167
+ this.counts[depth] = n + 1;
21168
+ if (suppress) return "";
21169
+ if (this.numbering === "gaejosik") return gaejosikMarker(depth, this.bullet2);
21170
+ return this.numbering === "report" ? reportMarker(depth, this.bullet2, this.asteriskThird) : standardMarker(depth, n);
21171
+ }
21172
+ reset() {
21173
+ this.counts = [];
21174
+ }
21175
+ };
20877
21176
 
20878
21177
  // src/hwpx/gen-ids.ts
20879
21178
  var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
@@ -24240,7 +24539,7 @@ async function markdownToHwpx(markdown, options) {
24240
24539
  }
24241
24540
  }
24242
24541
  const extraBorderFills = [...remap?.borderFillXmls ?? [], ...bfReg?.emit() ?? []];
24243
- const zip = new JSZip6();
24542
+ const zip = new JSZip8();
24244
24543
  zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
24245
24544
  zip.file("META-INF/container.xml", generateContainerXml());
24246
24545
  zip.file("Contents/content.hpf", generateManifest(chartParts, images.manifestItems(), gongmun ? "gongmun" : "default"));
@@ -24402,8 +24701,55 @@ function diffTableCells(a, b) {
24402
24701
  return result;
24403
24702
  }
24404
24703
 
24704
+ // src/form/templates.ts
24705
+ import { existsSync, readFileSync } from "fs";
24706
+ import { fileURLToPath } from "url";
24707
+ import { dirname, join } from "path";
24708
+ var BUILTIN_TEMPLATES = [
24709
+ {
24710
+ id: "gian",
24711
+ aliases: ["\uC77C\uBC18\uAE30\uC548\uBB38", "\uAE30\uC548\uBB38", "\uC77C\uBC18\uAE30\uC548\uBB38_\uC11C\uC2DD"],
24712
+ file: "\uC77C\uBC18\uAE30\uC548\uBB38_\uC11C\uC2DD.hwpx",
24713
+ sampleFile: "\uC77C\uBC18\uAE30\uC548\uBB38_\uC608\uC2DC\uAC12.json",
24714
+ title: "\uC77C\uBC18\uAE30\uC548\uBB38 \u2014 \uBCC4\uC9C0 \uC81C1\uD638\uC11C\uC2DD (\uB300\uC678 \uC2DC\uD589\uBB38\xB7\uD611\uC870\uBB38, \uB204\uB984\uD2C0 23\uACF3)"
24715
+ },
24716
+ {
24717
+ id: "gian-simple",
24718
+ aliases: ["\uAC04\uC774\uAE30\uC548\uBB38", "\uAC04\uC774\uAE30\uC548\uBB38_\uC11C\uC2DD"],
24719
+ file: "\uAC04\uC774\uAE30\uC548\uBB38_\uC11C\uC2DD.hwpx",
24720
+ sampleFile: "\uAC04\uC774\uAE30\uC548\uBB38_\uC608\uC2DC\uAC12.json",
24721
+ title: "\uAC04\uC774\uAE30\uC548\uBB38 \u2014 \uBCC4\uC9C0 \uC81C2\uD638\uC11C\uC2DD (\uB0B4\uBD80\uACB0\uC7AC \uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C, \uACB0\uC7AC\uB780 \uD45C, \uB204\uB984\uD2C0 13\uACF3)"
24722
+ }
24723
+ ];
24724
+ function resolveBuiltinTemplate(name) {
24725
+ const n = name.trim().replace(/^templates?:/i, "").trim();
24726
+ if (!n) return void 0;
24727
+ const low = n.toLowerCase();
24728
+ return BUILTIN_TEMPLATES.find((t) => t.id === low || t.aliases.includes(n));
24729
+ }
24730
+ function builtinTemplateDir() {
24731
+ const here = dirname(fileURLToPath(import.meta.url));
24732
+ const candidates = [
24733
+ join(here, "..", "..", "templates"),
24734
+ // src/form/templates.ts → 패키지 루트
24735
+ join(here, "..", "templates")
24736
+ // dist/*.js (tsup 번들) → 패키지 루트
24737
+ ];
24738
+ for (const c of candidates) {
24739
+ if (existsSync(join(c, BUILTIN_TEMPLATES[0].file))) return c;
24740
+ }
24741
+ throw new KordocError("\uB0B4\uC7A5 templates \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 (\uD328\uD0A4\uC9C0 \uC124\uCE58\uAC00 \uC190\uC0C1\uB418\uC5C8\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4)");
24742
+ }
24743
+ function readBuiltinTemplate(t) {
24744
+ const buf = readFileSync(join(builtinTemplateDir(), t.file));
24745
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
24746
+ }
24747
+ function readBuiltinTemplateSample(t) {
24748
+ return JSON.parse(readFileSync(join(builtinTemplateDir(), t.sampleFile), "utf-8"));
24749
+ }
24750
+
24405
24751
  // src/hwpx/extract-profile.ts
24406
- import JSZip7 from "jszip";
24752
+ import JSZip9 from "jszip";
24407
24753
  function elemsByLocal(root, name) {
24408
24754
  const all = root.getElementsByTagName("*");
24409
24755
  const out = [];
@@ -24606,7 +24952,7 @@ function pick(map, keys) {
24606
24952
  }
24607
24953
  async function hwpxToProfile(input) {
24608
24954
  const buf = input instanceof ArrayBuffer ? input : toArrayBuffer(input);
24609
- const zip = await JSZip7.loadAsync(buf);
24955
+ const zip = await JSZip9.loadAsync(buf);
24610
24956
  const parser = createXmlParser();
24611
24957
  const headerFile = zip.file("Contents/header.xml") ?? zip.file(/[Hh]eader\.xml$/)?.[0];
24612
24958
  let headerXml = "<root/>";
@@ -24921,7 +25267,7 @@ function gongmunLintWarnings(text, limit = 10) {
24921
25267
  }
24922
25268
 
24923
25269
  // src/roundtrip/patcher.ts
24924
- import JSZip8 from "jszip";
25270
+ import JSZip10 from "jszip";
24925
25271
 
24926
25272
  // src/roundtrip/table-rows.ts
24927
25273
  var ROW_OBJECT_RE = /<(?:[A-Za-z0-9_]+:)?(?:tbl|pic|equation|ole|container|shape|drawingObject|drawText|video|chart|fieldBegin|fieldEnd|ctrl)\b/;
@@ -25568,7 +25914,7 @@ async function patchHwpx(original, editedMarkdown, options) {
25568
25914
  }
25569
25915
  let zip;
25570
25916
  try {
25571
- zip = await JSZip8.loadAsync(original);
25917
+ zip = await JSZip10.loadAsync(original);
25572
25918
  } catch {
25573
25919
  return { success: false, applied: 0, skipped, error: "ZIP \uB85C\uB4DC \uC2E4\uD328" };
25574
25920
  }
@@ -25928,6 +26274,8 @@ function replaceOleStream(file, path, newData) {
25928
26274
  }
25929
26275
  var Surgeon = class {
25930
26276
  buf;
26277
+ /** 논리 파일 길이 — buf는 성장 시 기하급수 capacity로 재할당되므로 buf.length와 다를 수 있음 */
26278
+ len;
25931
26279
  fat = [];
25932
26280
  /** FAT 배열을 구성하는 섹터 번호들 (DIFAT 순서) */
25933
26281
  fatSectors = [];
@@ -25947,6 +26295,7 @@ var Surgeon = class {
25947
26295
  }
25948
26296
  const padded = Math.ceil((file.length - SECTOR) / SECTOR) * SECTOR + SECTOR;
25949
26297
  this.buf = Buffer.alloc(padded);
26298
+ this.len = padded;
25950
26299
  file.copy(this.buf);
25951
26300
  this.loadFat();
25952
26301
  this.loadMiniFat();
@@ -26001,7 +26350,7 @@ var Surgeon = class {
26001
26350
  // ── 헬퍼 ──
26002
26351
  sectorOffset(n) {
26003
26352
  const off = SECTOR + n * SECTOR;
26004
- if (n >= 4294967290 || off + SECTOR > this.buf.length) throw new OleSurgeonError(`\uC139\uD130 \uBC94\uC704 \uCD08\uACFC: ${n}`);
26353
+ if (n >= 4294967290 || off + SECTOR > this.len) throw new OleSurgeonError(`\uC139\uD130 \uBC94\uC704 \uCD08\uACFC: ${n}`);
26005
26354
  return off;
26006
26355
  }
26007
26356
  chain(start) {
@@ -26059,26 +26408,40 @@ var Surgeon = class {
26059
26408
  const out = [];
26060
26409
  for (let i = 0; i < this.fat.length && out.length < n; i++) {
26061
26410
  if (this.fat[i] !== FREESECT) continue;
26062
- if (SECTOR + (i + 1) * SECTOR > this.buf.length) continue;
26411
+ if (SECTOR + (i + 1) * SECTOR > this.len) continue;
26063
26412
  this.fat[i] = ENDOFCHAIN;
26064
26413
  const off = this.sectorOffset(i);
26065
26414
  this.buf.fill(0, off, off + SECTOR);
26066
26415
  out.push(i);
26067
26416
  }
26068
26417
  while (out.length < n) {
26069
- this.ensureFatCapacity((this.buf.length - SECTOR) / SECTOR + 2);
26070
- const idx = (this.buf.length - SECTOR) / SECTOR;
26071
- this.buf = Buffer.concat([this.buf, Buffer.alloc(SECTOR)]);
26418
+ this.ensureFatCapacity((this.len - SECTOR) / SECTOR + 2);
26419
+ const idx = this.appendSector();
26072
26420
  this.fat[idx] = ENDOFCHAIN;
26073
26421
  out.push(idx);
26074
26422
  }
26075
26423
  return out;
26076
26424
  }
26425
+ /**
26426
+ * 파일 끝에 빈 섹터 1개 추가 — 새 섹터 번호 반환.
26427
+ * 섹터마다 Buffer.concat(전체 복사)하면 대형 스트림 삽입이 O(n²)이라,
26428
+ * capacity를 기하급수(×2)로 재할당하고 논리 길이(len)만 늘린다 (amortized O(n)).
26429
+ * Buffer.alloc은 0으로 채워지고 len 너머엔 아무도 쓰지 않으므로 새 섹터는 항상 0.
26430
+ */
26431
+ appendSector() {
26432
+ const idx = (this.len - SECTOR) / SECTOR;
26433
+ if (this.len + SECTOR > this.buf.length) {
26434
+ const grown = Buffer.alloc(Math.max(this.buf.length * 2, this.len + SECTOR));
26435
+ this.buf.copy(grown, 0, 0, this.len);
26436
+ this.buf = grown;
26437
+ }
26438
+ this.len += SECTOR;
26439
+ return idx;
26440
+ }
26077
26441
  /** FAT 배열이 sectorCount개 엔트리를 담도록 확장 (FAT 섹터 추가 + DIFAT 갱신) */
26078
26442
  ensureFatCapacity(sectorCount) {
26079
26443
  while (this.fat.length < sectorCount) {
26080
- const idx = (this.buf.length - SECTOR) / SECTOR;
26081
- this.buf = Buffer.concat([this.buf, Buffer.alloc(SECTOR)]);
26444
+ const idx = this.appendSector();
26082
26445
  for (let i = 0; i < 128; i++) this.fat.push(FREESECT);
26083
26446
  this.fat[idx] = FATSECT;
26084
26447
  this.fatSectors.push(idx);
@@ -26201,7 +26564,7 @@ var Surgeon = class {
26201
26564
  finish() {
26202
26565
  this.wipeFreedSectors();
26203
26566
  this.flushFat();
26204
- return this.buf;
26567
+ return this.len === this.buf.length ? this.buf : this.buf.subarray(0, this.len);
26205
26568
  }
26206
26569
  /** 해제 후 재할당되지 않고 남은 FREESECT 섹터의 바이트를 0으로 채움 (데이터 remanence 제거) */
26207
26570
  wipeFreedSectors() {
@@ -27019,7 +27382,7 @@ function stageParaPatch(scan, para2, newPlain, skip) {
27019
27382
  }
27020
27383
 
27021
27384
  // src/validate.ts
27022
- import JSZip9 from "jszip";
27385
+ import JSZip11 from "jszip";
27023
27386
  import { DOMParser as DOMParser4 } from "@xmldom/xmldom";
27024
27387
  var REQUIRED_FILES = [
27025
27388
  "mimetype",
@@ -27037,7 +27400,7 @@ async function validateHwpx(buffer) {
27037
27400
  const issues = [];
27038
27401
  let zip;
27039
27402
  try {
27040
- zip = await JSZip9.loadAsync(buffer);
27403
+ zip = await JSZip11.loadAsync(buffer);
27041
27404
  } catch (err) {
27042
27405
  return {
27043
27406
  ok: false,
@@ -27105,10 +27468,10 @@ async function validateHwpx(buffer) {
27105
27468
  }
27106
27469
 
27107
27470
  // src/roundtrip/session.ts
27108
- import JSZip10 from "jszip";
27471
+ import JSZip12 from "jszip";
27109
27472
  async function buildState(bytes) {
27110
27473
  const parsed = await parseHwpxDocument(u8ToArrayBuffer2(bytes));
27111
- const zip = await JSZip10.loadAsync(bytes);
27474
+ const zip = await JSZip12.loadAsync(bytes);
27112
27475
  const sectionPaths = await resolveSectionEntryNames(zip);
27113
27476
  if (sectionPaths.length === 0) {
27114
27477
  throw new Error("HWPX \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -27450,7 +27813,7 @@ async function patchHwpxBlocks(original, edits, options) {
27450
27813
  }
27451
27814
 
27452
27815
  // src/print/renderer.ts
27453
- import { existsSync } from "fs";
27816
+ import { existsSync as existsSync2 } from "fs";
27454
27817
  import MarkdownIt from "markdown-it";
27455
27818
  var PRESETS = {
27456
27819
  default: `
@@ -27589,7 +27952,7 @@ function findChromiumPath() {
27589
27952
  const linux = ["/usr/bin/google-chrome", "/usr/bin/chromium-browser", "/usr/bin/chromium"];
27590
27953
  const candidates = process.platform === "win32" ? win : process.platform === "darwin" ? mac : linux;
27591
27954
  for (const p of candidates) {
27592
- if (p && existsSync(p)) return p;
27955
+ if (p && existsSync2(p)) return p;
27593
27956
  }
27594
27957
  return null;
27595
27958
  }
@@ -27649,7 +28012,7 @@ async function parse(input, options) {
27649
28012
  }
27650
28013
  async function parseImage(buffer, options) {
27651
28014
  try {
27652
- const { parseImageDocument } = await import("./image-ocr-KZTNJ62U.js");
28015
+ const { parseImageDocument } = await import("./image-ocr-R44RYDBU.js");
27653
28016
  const { blocks, warnings } = await parseImageDocument(buffer, options);
27654
28017
  return {
27655
28018
  success: true,
@@ -27666,7 +28029,7 @@ async function parseImage(buffer, options) {
27666
28029
  async function parseHwp3(buffer, options) {
27667
28030
  try {
27668
28031
  const { markdown, blocks, metadata, outline, warnings } = parseHwp3Document(buffer, options);
27669
- return { success: true, fileType: "hwp3", markdown, blocks, metadata, outline, warnings };
28032
+ return { success: true, fileType: "hwp3", markdown, blocks, metadata, outline, warnings, pageCount: metadata?.pageCount };
27670
28033
  } catch (err) {
27671
28034
  return { success: false, fileType: "hwp3", error: sanitizeError(err), code: classifyError(err) };
27672
28035
  }
@@ -27674,7 +28037,7 @@ async function parseHwp3(buffer, options) {
27674
28037
  async function parseHwpx(buffer, options) {
27675
28038
  try {
27676
28039
  const { markdown, blocks, metadata, outline, warnings, images } = await parseHwpxDocument(buffer, options);
27677
- return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
28040
+ return { success: true, fileType: "hwpx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0, pageCount: metadata?.pageCount };
27678
28041
  } catch (err) {
27679
28042
  return { success: false, fileType: "hwpx", error: sanitizeError(err), code: classifyError(err) };
27680
28043
  }
@@ -27693,13 +28056,14 @@ async function parseHwp(buffer, options) {
27693
28056
  markdown: com.markdown,
27694
28057
  blocks: com.blocks,
27695
28058
  metadata: com.metadata,
27696
- warnings: com.warnings
28059
+ warnings: com.warnings,
28060
+ pageCount
27697
28061
  };
27698
28062
  }
27699
28063
  } catch {
27700
28064
  }
27701
28065
  }
27702
- return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
28066
+ return { success: true, fileType: "hwp", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0, pageCount: metadata?.pageCount };
27703
28067
  } catch (err) {
27704
28068
  return { success: false, fileType: "hwp", error: sanitizeError(err), code: classifyError(err) };
27705
28069
  }
@@ -27707,7 +28071,7 @@ async function parseHwp(buffer, options) {
27707
28071
  async function parsePdf(buffer, options) {
27708
28072
  let parsePdfDocument;
27709
28073
  try {
27710
- const mod = await import("./parser-WNFWMBSL.js");
28074
+ const mod = await import("./parser-W3POOYQQ.js");
27711
28075
  parsePdfDocument = mod.parsePdfDocument;
27712
28076
  } catch {
27713
28077
  return {
@@ -27719,7 +28083,7 @@ async function parsePdf(buffer, options) {
27719
28083
  }
27720
28084
  try {
27721
28085
  const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images } = await parsePdfDocument(buffer, options);
27722
- return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images };
28086
+ return { success: true, fileType: "pdf", markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images, pageCount: metadata?.pageCount };
27723
28087
  } catch (err) {
27724
28088
  const isImageBased = err instanceof Error && "isImageBased" in err ? true : void 0;
27725
28089
  return { success: false, fileType: "pdf", error: sanitizeError(err), code: classifyError(err), isImageBased };
@@ -27728,7 +28092,7 @@ async function parsePdf(buffer, options) {
27728
28092
  async function parseXlsx(buffer, options) {
27729
28093
  try {
27730
28094
  const { markdown, blocks, metadata, warnings } = await parseXlsxDocument(buffer, options);
27731
- return { success: true, fileType: "xlsx", markdown, blocks, metadata, warnings };
28095
+ return { success: true, fileType: "xlsx", markdown, blocks, metadata, warnings, pageCount: metadata?.pageCount };
27732
28096
  } catch (err) {
27733
28097
  return { success: false, fileType: "xlsx", error: sanitizeError(err), code: classifyError(err) };
27734
28098
  }
@@ -27736,7 +28100,7 @@ async function parseXlsx(buffer, options) {
27736
28100
  async function parseXls(buffer, options) {
27737
28101
  try {
27738
28102
  const { markdown, blocks, metadata, warnings } = await parseXlsDocument(buffer, options);
27739
- return { success: true, fileType: "xls", markdown, blocks, metadata, warnings };
28103
+ return { success: true, fileType: "xls", markdown, blocks, metadata, warnings, pageCount: metadata?.pageCount };
27740
28104
  } catch (err) {
27741
28105
  return { success: false, fileType: "xls", error: sanitizeError(err), code: classifyError(err) };
27742
28106
  }
@@ -27744,7 +28108,7 @@ async function parseXls(buffer, options) {
27744
28108
  async function parseDocx(buffer, options) {
27745
28109
  try {
27746
28110
  const { markdown, blocks, metadata, outline, warnings, images } = await parseDocxDocument(buffer, options);
27747
- return { success: true, fileType: "docx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0 };
28111
+ return { success: true, fileType: "docx", markdown, blocks, metadata, outline, warnings, images: images?.length ? images : void 0, pageCount: metadata?.pageCount };
27748
28112
  } catch (err) {
27749
28113
  return { success: false, fileType: "docx", error: sanitizeError(err), code: classifyError(err) };
27750
28114
  }
@@ -27752,7 +28116,7 @@ async function parseDocx(buffer, options) {
27752
28116
  async function parseHwpml(buffer, options) {
27753
28117
  try {
27754
28118
  const { markdown, blocks, metadata, outline, warnings } = parseHwpmlDocument(buffer, options);
27755
- return { success: true, fileType: "hwpml", markdown, blocks, metadata, outline, warnings };
28119
+ return { success: true, fileType: "hwpml", markdown, blocks, metadata, outline, warnings, pageCount: metadata?.pageCount };
27756
28120
  } catch (err) {
27757
28121
  return { success: false, fileType: "hwpml", error: sanitizeError(err), code: classifyError(err) };
27758
28122
  }
@@ -27798,9 +28162,13 @@ async function fillForm(input, values, outputFormat = "markdown") {
27798
28162
  }
27799
28163
 
27800
28164
  export {
27801
- PRESET_ALIAS,
27802
- normalizeGongmunPreset,
27803
- incompatibleGongmunWarnings,
28165
+ isZipFile,
28166
+ isHwpxFile,
28167
+ isOldHwpFile,
28168
+ isPdfFile,
28169
+ detectFormat,
28170
+ detectOle2Format,
28171
+ detectZipFormat,
27804
28172
  extractHwpxMetadataOnly,
27805
28173
  extractHwp5MetadataOnly,
27806
28174
  formatFillValue,
@@ -27811,10 +28179,18 @@ export {
27811
28179
  inferFieldType,
27812
28180
  extractFormSchema,
27813
28181
  fillFormFields,
28182
+ extractClickHereFields,
27814
28183
  fillHwpx,
28184
+ PRESET_ALIAS,
28185
+ normalizeGongmunPreset,
28186
+ incompatibleGongmunWarnings,
27815
28187
  markdownToHwpx,
27816
28188
  compare,
27817
28189
  diffBlocks,
28190
+ BUILTIN_TEMPLATES,
28191
+ resolveBuiltinTemplate,
28192
+ readBuiltinTemplate,
28193
+ readBuiltinTemplateSample,
27818
28194
  hwpxToProfile,
27819
28195
  isKnownFont,
27820
28196
  unknownFontWarnings,
@@ -27841,4 +28217,4 @@ export {
27841
28217
  parseHwpml,
27842
28218
  fillForm
27843
28219
  };
27844
- //# sourceMappingURL=chunk-G3DMDTZI.js.map
28220
+ //# sourceMappingURL=chunk-DRZUJ2RK.js.map