kordoc 4.2.8 → 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/{-SCTT2TBZ.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-IJUAWDOS.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-JP5MNHUK.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-WP4VD5T7.js → chunk-7MFO4TKP.js} +4 -3
  14. package/dist/chunk-7MFO4TKP.js.map +1 -0
  15. package/dist/{chunk-CELJIGBQ.cjs → chunk-AL34DGS5.cjs} +6 -6
  16. package/dist/chunk-AL34DGS5.cjs.map +1 -0
  17. package/dist/{chunk-FS3XQ3ZQ.js → chunk-ARXNKC5D.js} +3 -3
  18. package/dist/chunk-ARXNKC5D.js.map +1 -0
  19. package/dist/{chunk-2BTKLDNX.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-7GEBDS6B.js → chunk-DRQUAYNT.js} +4 -3
  23. package/dist/chunk-DRQUAYNT.js.map +1 -0
  24. package/dist/{chunk-YNAPK4H6.js → chunk-DRZUJ2RK.js} +1521 -1144
  25. package/dist/chunk-DRZUJ2RK.js.map +1 -0
  26. package/dist/{chunk-GKSFNCVP.js → chunk-EFVAY5VE.js} +71 -29
  27. package/dist/chunk-EFVAY5VE.js.map +1 -0
  28. package/dist/{chunk-UKEZ46VL.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-4FTXKIGT.js → chunk-GSMPTZC7.js} +4 -4
  32. package/dist/chunk-GSMPTZC7.js.map +1 -0
  33. package/dist/{chunk-OQNFGKRY.cjs → chunk-JFV7QT5T.cjs} +4 -3
  34. package/dist/chunk-JFV7QT5T.cjs.map +1 -0
  35. package/dist/{chunk-ROKHZE76.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-55QIELGV.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-COHHXQSP.js → chunk-SPKBIFSK.js} +2 -2
  45. package/dist/chunk-SPKBIFSK.js.map +1 -0
  46. package/dist/{chunk-QZCAIFUW.js → chunk-US4WFSIQ.js} +4 -4
  47. package/dist/chunk-US4WFSIQ.js.map +1 -0
  48. package/dist/{chunk-EOVJ27WC.js → chunk-VXMLENCV.js} +15 -11
  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-URKX77GB.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-ANSXR5KJ.js → image-ocr-G2LYTEUH.js} +8 -6
  68. package/dist/image-ocr-G2LYTEUH.js.map +1 -0
  69. package/dist/{image-ocr-ORATZW73.js → image-ocr-R44RYDBU.js} +10 -8
  70. package/dist/image-ocr-R44RYDBU.js.map +1 -0
  71. package/dist/{image-ocr-GHQ6YKJH.cjs → image-ocr-XEJFXH6M.cjs} +13 -11
  72. package/dist/image-ocr-XEJFXH6M.cjs.map +1 -0
  73. package/dist/index.cjs +2017 -1868
  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 +2139 -1990
  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-BPVTYM5K.js → parser-2G245MMN.js} +6 -6
  85. package/dist/parser-2G245MMN.js.map +1 -0
  86. package/dist/{parser-GZDOZCHM.js → parser-W3POOYQQ.js} +8 -8
  87. package/dist/parser-W3POOYQQ.js.map +1 -0
  88. package/dist/{parser-KTNIUF2N.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-I2TH44EO.js → profile-io-72R3B7KO.js} +3 -3
  95. package/dist/{rasterize-QBA43YAQ.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-7V6X7LHV.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-2BTKLDNX.cjs.map +0 -1
  110. package/dist/chunk-4FTXKIGT.js.map +0 -1
  111. package/dist/chunk-55QIELGV.js.map +0 -1
  112. package/dist/chunk-5RPYBC2Q.js.map +0 -1
  113. package/dist/chunk-7FCNOKOR.js.map +0 -1
  114. package/dist/chunk-7GEBDS6B.js.map +0 -1
  115. package/dist/chunk-CELJIGBQ.cjs.map +0 -1
  116. package/dist/chunk-COHHXQSP.js.map +0 -1
  117. package/dist/chunk-DZIXKL7E.js.map +0 -1
  118. package/dist/chunk-EOVJ27WC.js.map +0 -1
  119. package/dist/chunk-FS3XQ3ZQ.js.map +0 -1
  120. package/dist/chunk-GEPINOL6.js.map +0 -1
  121. package/dist/chunk-GKSFNCVP.js.map +0 -1
  122. package/dist/chunk-H7OWEYH2.cjs.map +0 -1
  123. package/dist/chunk-IJUAWDOS.js.map +0 -1
  124. package/dist/chunk-J2RZ444H.js.map +0 -1
  125. package/dist/chunk-JP5MNHUK.js.map +0 -1
  126. package/dist/chunk-LM7C6HI2.js.map +0 -1
  127. package/dist/chunk-NNDVFNMJ.js.map +0 -1
  128. package/dist/chunk-OQNFGKRY.cjs.map +0 -1
  129. package/dist/chunk-QZCAIFUW.js.map +0 -1
  130. package/dist/chunk-ROKHZE76.cjs.map +0 -1
  131. package/dist/chunk-TCJRGFYY.js +0 -280
  132. package/dist/chunk-TCJRGFYY.js.map +0 -1
  133. package/dist/chunk-UKEZ46VL.js.map +0 -1
  134. package/dist/chunk-URKX77GB.js.map +0 -1
  135. package/dist/chunk-WP4VD5T7.js.map +0 -1
  136. package/dist/chunk-YNAPK4H6.js.map +0 -1
  137. package/dist/chunks-KKYJDJT7.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-ANSXR5KJ.js.map +0 -1
  143. package/dist/image-ocr-GHQ6YKJH.cjs.map +0 -1
  144. package/dist/image-ocr-ORATZW73.js.map +0 -1
  145. package/dist/parser-BPVTYM5K.js.map +0 -1
  146. package/dist/parser-GZDOZCHM.js.map +0 -1
  147. package/dist/parser-KTNIUF2N.cjs.map +0 -1
  148. package/dist/pdf-ocr-6XKLXZTF.cjs +0 -13
  149. package/dist/pdf-ocr-6XKLXZTF.cjs.map +0 -1
  150. package/dist/pdf-ocr-HWPNMKKC.js +0 -12
  151. package/dist/pdf-ocr-W2PA2K3U.js +0 -15
  152. package/dist/rasterize-QBA43YAQ.js.map +0 -1
  153. package/dist/render-MDJDKKNH.js +0 -10
  154. package/dist/seal-Z52JR2GX.js +0 -10
  155. package/dist/seal-Z52JR2GX.js.map +0 -1
  156. package/dist/setup-GUQT5ELJ.js.map +0 -1
  157. package/dist/watch-7V6X7LHV.js.map +0 -1
  158. /package/dist/{-SCTT2TBZ.js.map → -DYHT3HWH.js.map} +0 -0
  159. /package/dist/{chunks-KKYJDJT7.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-HWPNMKKC.js.map → pdf-ocr-U7MEGNBZ.js.map} +0 -0
  163. /package/dist/{pdf-ocr-W2PA2K3U.js.map → profile-io-72R3B7KO.js.map} +0 -0
  164. /package/dist/{profile-io-I2TH44EO.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-MDJDKKNH.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-55QIELGV.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-4FTXKIGT.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-EOVJ27WC.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-GKSFNCVP.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-7GEBDS6B.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
@@ -17066,9 +16827,8 @@ function decodeJohab(ch) {
17066
16827
  const jongIdx = ch & 31;
17067
16828
  const cho = CHO_MAP[choIdx];
17068
16829
  const jung = JUNG_MAP[jungIdx];
17069
- let jong = JONG_MAP[jongIdx];
17070
- if (cho !== -1 && jung !== -1) {
17071
- if (jong === -1) jong = 0;
16830
+ const jong = JONG_MAP[jongIdx];
16831
+ if (cho !== -1 && jung !== -1 && jong !== -1) {
17072
16832
  return 44032 + cho * 588 + jung * 28 + jong;
17073
16833
  }
17074
16834
  const hit = lookupSymbol(ch);
@@ -17237,8 +16997,6 @@ var LINE_INFO_SIZE = 14;
17237
16997
  var INLINE_CHAR_SHAPE_SIZE = 31;
17238
16998
  var SIMPLE_CTRL = /* @__PURE__ */ new Map([
17239
16999
  [9, { extraBytes: 6, extraHchar: 3, emit: " " }],
17240
- [7, { extraBytes: 6, extraHchar: 3, emit: "\uFFFC" }],
17241
- [8, { extraBytes: 6, extraHchar: 3, emit: "\uFFFC" }],
17242
17000
  [18, { extraBytes: 6, extraHchar: 3, emit: " " }],
17243
17001
  // AutoNumber → 공백 (HWP5 패턴)
17244
17002
  [19, { extraBytes: 6, extraHchar: 3, emit: "\uFFFC" }],
@@ -17247,7 +17005,8 @@ var SIMPLE_CTRL = /* @__PURE__ */ new Map([
17247
17005
  [22, { extraBytes: 22, extraHchar: 11, emit: "\uFFFC" }],
17248
17006
  [23, { extraBytes: 8, extraHchar: 4, emit: "\uFFFC" }],
17249
17007
  [24, { extraBytes: 4, extraHchar: 2, emit: "-" }],
17250
- [25, { extraBytes: 4, extraHchar: 2, emit: "-" }],
17008
+ [25, { extraBytes: 4, extraHchar: 2, emit: null }],
17009
+ // 차례 표식 — 비가시 (rhwp #2765)
17251
17010
  [26, { extraBytes: 244, extraHchar: 122, emit: "\uFFFC" }],
17252
17011
  [28, { extraBytes: 62, extraHchar: 31, emit: "\uFFFC" }],
17253
17012
  [30, { extraBytes: 2, extraHchar: 1, emit: "\xA0" }],
@@ -17257,9 +17016,7 @@ function parseHwp3Document(buffer, _options) {
17257
17016
  const headReader = new Reader(Buffer.from(buffer));
17258
17017
  const header = readHeader(headReader);
17259
17018
  if (header.encrypted !== 0) {
17260
- 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.");
17261
- e.code = "ENCRYPTED";
17262
- 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.");
17263
17020
  }
17264
17021
  headReader.skip(header.infoBlockLength);
17265
17022
  const tail = headReader.readToEnd();
@@ -17270,10 +17027,10 @@ function parseHwp3Document(buffer, _options) {
17270
17027
  body = inflateRawSync3(tail, { maxOutputLength: MAX_DECOMPRESS_SIZE3 });
17271
17028
  } catch (err) {
17272
17029
  if (err?.code === "ERR_BUFFER_TOO_LARGE") {
17273
- 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`);
17274
17031
  }
17275
17032
  const msg2 = err instanceof Error ? err.message : String(err);
17276
- throw new Error(`HWP3 \uC555\uCD95 \uD574\uC81C \uC2E4\uD328: ${msg2}`);
17033
+ throw new KordocError(`HWP3 \uC555\uCD95 \uD574\uC81C \uC2E4\uD328: ${msg2}`);
17277
17034
  }
17278
17035
  } else {
17279
17036
  body = tail;
@@ -17391,9 +17148,6 @@ function parseCharStream(reader, charCount, ctx) {
17391
17148
  case 11:
17392
17149
  parsePicture(reader, ctx);
17393
17150
  break;
17394
- case 12:
17395
- reader.skip(84);
17396
- break;
17397
17151
  case 14:
17398
17152
  reader.skip(84);
17399
17153
  break;
@@ -17418,6 +17172,12 @@ function parseCharStream(reader, charCount, ctx) {
17418
17172
  case 6:
17419
17173
  reader.skip(34);
17420
17174
  break;
17175
+ case 7:
17176
+ reader.skip(76);
17177
+ break;
17178
+ case 8:
17179
+ reader.skip(88);
17180
+ break;
17421
17181
  case 29:
17422
17182
  if (headerVal1 < 1e6) reader.skip(headerVal1);
17423
17183
  break;
@@ -17471,7 +17231,7 @@ function isDistributionSentinel(markdown) {
17471
17231
  }
17472
17232
 
17473
17233
  // src/xlsx/parser.ts
17474
- import JSZip3 from "jszip";
17234
+ import JSZip4 from "jszip";
17475
17235
  import { DOMParser } from "@xmldom/xmldom";
17476
17236
  var MAX_SHEETS = 100;
17477
17237
  var MAX_DECOMPRESS_SIZE4 = 100 * 1024 * 1024;
@@ -17756,7 +17516,7 @@ function sheetToBlocks(sheetName, grid, merges, maxRow, maxCol, sheetIndex, keep
17756
17516
  }
17757
17517
  async function parseXlsxDocument(buffer, options) {
17758
17518
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE4);
17759
- const zip = await JSZip3.loadAsync(buffer);
17519
+ const zip = await JSZip4.loadAsync(buffer);
17760
17520
  const warnings = [];
17761
17521
  const workbookFile = zip.file("xl/workbook.xml");
17762
17522
  if (!workbookFile) {
@@ -17777,6 +17537,10 @@ async function parseXlsxDocument(buffer, options) {
17777
17537
  try {
17778
17538
  dateXfs = parseStyleDateXfs(await stylesFile.async("text"));
17779
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
+ });
17780
17544
  }
17781
17545
  }
17782
17546
  let relsMap = /* @__PURE__ */ new Map();
@@ -18473,17 +18237,7 @@ async function parseXlsDocument(buffer, options) {
18473
18237
  const globals = processGlobals(records);
18474
18238
  const warnings = [];
18475
18239
  if (globals.encrypted) {
18476
- return {
18477
- markdown: "",
18478
- blocks: [],
18479
- metadata: { pageCount: globals.sheets.length },
18480
- warnings: [
18481
- {
18482
- message: "XLS \uD30C\uC77C\uC774 \uC554\uD638\uD654\uB418\uC5B4 \uC788\uC5B4 \uD30C\uC2F1\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
18483
- code: "PARTIAL_PARSE"
18484
- }
18485
- ]
18486
- };
18240
+ throw new KordocError("XLS \uD30C\uC77C\uC774 \uC554\uD638\uD654\uB418\uC5B4 \uC788\uC5B4 \uD30C\uC2F1\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
18487
18241
  }
18488
18242
  const convertNum = globals.dateXfs.size > 0 ? (n, ixfe) => {
18489
18243
  const kind = globals.dateXfs.get(ixfe);
@@ -18542,41 +18296,13 @@ async function parseXlsDocument(buffer, options) {
18542
18296
  }
18543
18297
 
18544
18298
  // src/docx/parser.ts
18545
- import JSZip4 from "jszip";
18299
+ import JSZip5 from "jszip";
18546
18300
  import { DOMParser as DOMParser2 } from "@xmldom/xmldom";
18547
18301
 
18548
18302
  // src/docx/equation.ts
18549
- function lname(el) {
18550
- return el.localName || el.tagName?.replace(/^[^:]+:/, "") || "";
18551
- }
18552
- function kids(parent, name) {
18553
- const out = [];
18554
- const nodes = parent.childNodes;
18555
- if (!nodes) return out;
18556
- for (let i = 0; i < nodes.length; i++) {
18557
- const n = nodes[i];
18558
- if (n.nodeType !== 1) continue;
18559
- const el = n;
18560
- if (lname(el) === name) out.push(el);
18561
- }
18562
- return out;
18563
- }
18564
- function firstKid(parent, name) {
18565
- const list = kids(parent, name);
18566
- return list[0] ?? null;
18567
- }
18568
- function eachChild(parent) {
18569
- const out = [];
18570
- const nodes = parent.childNodes;
18571
- if (!nodes) return out;
18572
- for (let i = 0; i < nodes.length; i++) {
18573
- if (nodes[i].nodeType === 1) out.push(nodes[i]);
18574
- }
18575
- return out;
18576
- }
18577
18303
  function runToLatex(r) {
18578
18304
  let out = "";
18579
- for (const t of kids(r, "t")) out += t.textContent ?? "";
18305
+ for (const t of childrenByLocalName(r, "t")) out += t.textContent ?? "";
18580
18306
  return out;
18581
18307
  }
18582
18308
  var FUNC_NAMES = /* @__PURE__ */ new Set([
@@ -18697,13 +18423,13 @@ function grp(body) {
18697
18423
  }
18698
18424
  function childrenToLatex(parent) {
18699
18425
  let out = "";
18700
- for (const ch of eachChild(parent)) {
18426
+ for (const ch of elementChildren(parent)) {
18701
18427
  out += nodeToLatex(ch);
18702
18428
  }
18703
18429
  return out;
18704
18430
  }
18705
18431
  function nodeToLatex(el) {
18706
- const tag = lname(el);
18432
+ const tag = localName(el);
18707
18433
  switch (tag) {
18708
18434
  case "r":
18709
18435
  return runToLatex(el);
@@ -18719,33 +18445,33 @@ function nodeToLatex(el) {
18719
18445
  return childrenToLatex(el);
18720
18446
  // 분수
18721
18447
  case "f": {
18722
- const n = firstKid(el, "num");
18723
- const d = firstKid(el, "den");
18448
+ const n = findChildByLocalName(el, "num");
18449
+ const d = findChildByLocalName(el, "den");
18724
18450
  const num2 = n ? childrenToLatex(n) : "";
18725
18451
  const den = d ? childrenToLatex(d) : "";
18726
18452
  return "\\frac" + grp(num2) + grp(den);
18727
18453
  }
18728
18454
  // 첨자
18729
18455
  case "sSup": {
18730
- const e = firstKid(el, "e");
18731
- const sup = firstKid(el, "sup");
18456
+ const e = findChildByLocalName(el, "e");
18457
+ const sup = findChildByLocalName(el, "sup");
18732
18458
  return grp(e ? childrenToLatex(e) : "") + "^" + grp(sup ? childrenToLatex(sup) : "");
18733
18459
  }
18734
18460
  case "sSub": {
18735
- const e = firstKid(el, "e");
18736
- const sub = firstKid(el, "sub");
18461
+ const e = findChildByLocalName(el, "e");
18462
+ const sub = findChildByLocalName(el, "sub");
18737
18463
  return grp(e ? childrenToLatex(e) : "") + "_" + grp(sub ? childrenToLatex(sub) : "");
18738
18464
  }
18739
18465
  case "sSubSup": {
18740
- const e = firstKid(el, "e");
18741
- const sub = firstKid(el, "sub");
18742
- const sup = firstKid(el, "sup");
18466
+ const e = findChildByLocalName(el, "e");
18467
+ const sub = findChildByLocalName(el, "sub");
18468
+ const sup = findChildByLocalName(el, "sup");
18743
18469
  return grp(e ? childrenToLatex(e) : "") + "_" + grp(sub ? childrenToLatex(sub) : "") + "^" + grp(sup ? childrenToLatex(sup) : "");
18744
18470
  }
18745
18471
  case "sPre": {
18746
- const sub = firstKid(el, "sub");
18747
- const sup = firstKid(el, "sup");
18748
- const e = firstKid(el, "e");
18472
+ const sub = findChildByLocalName(el, "sub");
18473
+ const sup = findChildByLocalName(el, "sup");
18474
+ const e = findChildByLocalName(el, "e");
18749
18475
  const preSub = sub ? grp(childrenToLatex(sub)) : "{}";
18750
18476
  const preSup = sup ? grp(childrenToLatex(sup)) : "{}";
18751
18477
  const body = e ? childrenToLatex(e) : "";
@@ -18753,13 +18479,13 @@ function nodeToLatex(el) {
18753
18479
  }
18754
18480
  // 근호
18755
18481
  case "rad": {
18756
- const deg = firstKid(el, "deg");
18757
- const e = firstKid(el, "e");
18482
+ const deg = findChildByLocalName(el, "deg");
18483
+ const e = findChildByLocalName(el, "e");
18758
18484
  const body = e ? childrenToLatex(e) : "";
18759
- const radPr = firstKid(el, "radPr");
18485
+ const radPr = findChildByLocalName(el, "radPr");
18760
18486
  let hide = false;
18761
18487
  if (radPr) {
18762
- const degHide = firstKid(radPr, "degHide");
18488
+ const degHide = findChildByLocalName(radPr, "degHide");
18763
18489
  if (degHide) {
18764
18490
  const val = degHide.getAttribute("m:val") ?? degHide.getAttribute("val");
18765
18491
  hide = val === "1" || val === "on" || val === "true";
@@ -18770,13 +18496,13 @@ function nodeToLatex(el) {
18770
18496
  }
18771
18497
  // n-ary 연산자 (sum, prod, int, …)
18772
18498
  case "nary": {
18773
- const naryPr = firstKid(el, "naryPr");
18499
+ const naryPr = findChildByLocalName(el, "naryPr");
18774
18500
  let op = "\\int";
18775
18501
  let subHide = false;
18776
18502
  let supHide = false;
18777
18503
  let limLoc = "";
18778
18504
  if (naryPr) {
18779
- const chr = firstKid(naryPr, "chr");
18505
+ const chr = findChildByLocalName(naryPr, "chr");
18780
18506
  if (chr) {
18781
18507
  const v = chr.getAttribute("m:val") ?? chr.getAttribute("val") ?? "";
18782
18508
  if (v && NARY_MAP[v]) op = NARY_MAP[v];
@@ -18784,16 +18510,16 @@ function nodeToLatex(el) {
18784
18510
  } else {
18785
18511
  op = "\\int";
18786
18512
  }
18787
- const sh = firstKid(naryPr, "subHide");
18788
- const ph = firstKid(naryPr, "supHide");
18513
+ const sh = findChildByLocalName(naryPr, "subHide");
18514
+ const ph = findChildByLocalName(naryPr, "supHide");
18789
18515
  if (sh) subHide = onOffVal(sh.getAttribute("m:val") ?? sh.getAttribute("val"));
18790
18516
  if (ph) supHide = onOffVal(ph.getAttribute("m:val") ?? ph.getAttribute("val"));
18791
- const ll = firstKid(naryPr, "limLoc");
18517
+ const ll = findChildByLocalName(naryPr, "limLoc");
18792
18518
  if (ll) limLoc = ll.getAttribute("m:val") ?? ll.getAttribute("val") ?? "";
18793
18519
  }
18794
- const sub = firstKid(el, "sub");
18795
- const sup = firstKid(el, "sup");
18796
- const e = firstKid(el, "e");
18520
+ const sub = findChildByLocalName(el, "sub");
18521
+ const sup = findChildByLocalName(el, "sup");
18522
+ const e = findChildByLocalName(el, "e");
18797
18523
  const subStr = !subHide && sub ? childrenToLatex(sub) : "";
18798
18524
  const supStr = !supHide && sup ? childrenToLatex(sup) : "";
18799
18525
  const body = e ? childrenToLatex(e) : "";
@@ -18809,27 +18535,27 @@ function nodeToLatex(el) {
18809
18535
  }
18810
18536
  // 괄호 (delimiter)
18811
18537
  case "d": {
18812
- const dPr = firstKid(el, "dPr");
18538
+ const dPr = findChildByLocalName(el, "dPr");
18813
18539
  let beg = "(";
18814
18540
  let end = ")";
18815
18541
  let sep = ",";
18816
18542
  if (dPr) {
18817
- const begChr = firstKid(dPr, "begChr");
18818
- const endChr = firstKid(dPr, "endChr");
18819
- const sepChr = firstKid(dPr, "sepChr");
18543
+ const begChr = findChildByLocalName(dPr, "begChr");
18544
+ const endChr = findChildByLocalName(dPr, "endChr");
18545
+ const sepChr = findChildByLocalName(dPr, "sepChr");
18820
18546
  if (begChr) beg = begChr.getAttribute("m:val") ?? begChr.getAttribute("val") ?? beg;
18821
18547
  if (endChr) end = endChr.getAttribute("m:val") ?? endChr.getAttribute("val") ?? end;
18822
18548
  if (sepChr) sep = sepChr.getAttribute("m:val") ?? sepChr.getAttribute("val") ?? sep;
18823
18549
  }
18824
- const items = kids(el, "e").map(childrenToLatex);
18550
+ const items = childrenByLocalName(el, "e").map(childrenToLatex);
18825
18551
  const body = items.join(sep);
18826
18552
  return "\\left" + mapDelim(beg, true) + body + "\\right" + mapDelim(end, false);
18827
18553
  }
18828
18554
  // 행렬
18829
18555
  case "m": {
18830
18556
  const rows = [];
18831
- for (const mr of kids(el, "mr")) {
18832
- const cells = kids(mr, "e").map(childrenToLatex);
18557
+ for (const mr of childrenByLocalName(el, "mr")) {
18558
+ const cells = childrenByLocalName(mr, "e").map(childrenToLatex);
18833
18559
  rows.push(cells.join(" & "));
18834
18560
  }
18835
18561
  return "\\begin{matrix}" + rows.join(" \\\\ ") + "\\end{matrix}";
@@ -18839,8 +18565,8 @@ function nodeToLatex(el) {
18839
18565
  return childrenToLatex(el);
18840
18566
  // 함수 적용 (sin, cos, log …)
18841
18567
  case "func": {
18842
- const fn = firstKid(el, "fName");
18843
- const e = firstKid(el, "e");
18568
+ const fn = findChildByLocalName(el, "fName");
18569
+ const e = findChildByLocalName(el, "e");
18844
18570
  const fnStr = fn ? childrenToLatex(fn).trim() : "";
18845
18571
  const body = e ? childrenToLatex(e) : "";
18846
18572
  const fnLatex = FUNC_NAMES.has(fnStr) ? "\\" + fnStr : fnStr;
@@ -18848,42 +18574,42 @@ function nodeToLatex(el) {
18848
18574
  }
18849
18575
  // 악센트 (hat/bar/vec/…)
18850
18576
  case "acc": {
18851
- const accPr = firstKid(el, "accPr");
18577
+ const accPr = findChildByLocalName(el, "accPr");
18852
18578
  let chr = "";
18853
18579
  if (accPr) {
18854
- const chrEl = firstKid(accPr, "chr");
18580
+ const chrEl = findChildByLocalName(accPr, "chr");
18855
18581
  if (chrEl) chr = chrEl.getAttribute("m:val") ?? chrEl.getAttribute("val") ?? "";
18856
18582
  }
18857
18583
  if (!chr) chr = "\u0302";
18858
- const e = firstKid(el, "e");
18584
+ const e = findChildByLocalName(el, "e");
18859
18585
  const body = e ? childrenToLatex(e) : "";
18860
18586
  const cmd = ACCENT_MAP[chr] ?? "\\hat";
18861
18587
  return cmd + grp(body);
18862
18588
  }
18863
18589
  // bar (위/아래 줄)
18864
18590
  case "bar": {
18865
- const barPr = firstKid(el, "barPr");
18591
+ const barPr = findChildByLocalName(el, "barPr");
18866
18592
  let pos = "top";
18867
18593
  if (barPr) {
18868
- const posEl = firstKid(barPr, "pos");
18594
+ const posEl = findChildByLocalName(barPr, "pos");
18869
18595
  if (posEl) pos = posEl.getAttribute("m:val") ?? posEl.getAttribute("val") ?? pos;
18870
18596
  }
18871
- const e = firstKid(el, "e");
18597
+ const e = findChildByLocalName(el, "e");
18872
18598
  const body = e ? childrenToLatex(e) : "";
18873
18599
  return (pos === "bot" ? "\\underline" : "\\overline") + grp(body);
18874
18600
  }
18875
18601
  // lim 위/아래
18876
18602
  case "limLow": {
18877
- const e = firstKid(el, "e");
18878
- const lim = firstKid(el, "lim");
18603
+ const e = findChildByLocalName(el, "e");
18604
+ const lim = findChildByLocalName(el, "lim");
18879
18605
  const base = e ? childrenToLatex(e).trim() : "";
18880
18606
  const below = lim ? childrenToLatex(lim) : "";
18881
18607
  if (FUNC_NAMES.has(base)) return "\\" + base + "_" + grp(below);
18882
18608
  return base + "_" + grp(below);
18883
18609
  }
18884
18610
  case "limUpp": {
18885
- const e = firstKid(el, "e");
18886
- const lim = firstKid(el, "lim");
18611
+ const e = findChildByLocalName(el, "e");
18612
+ const lim = findChildByLocalName(el, "lim");
18887
18613
  const base = e ? childrenToLatex(e).trim() : "";
18888
18614
  const above = lim ? childrenToLatex(lim) : "";
18889
18615
  if (FUNC_NAMES.has(base)) return "\\" + base + "^" + grp(above);
@@ -18891,7 +18617,7 @@ function nodeToLatex(el) {
18891
18617
  }
18892
18618
  // group character (over/underset 비슷)
18893
18619
  case "groupChr":
18894
- return childrenToLatex(firstKid(el, "e") ?? el);
18620
+ return childrenToLatex(findChildByLocalName(el, "e") ?? el);
18895
18621
  // box/borderBox/phantom/eqArr/… 는 자식 본문만 유지
18896
18622
  case "borderBox":
18897
18623
  case "phant":
@@ -18921,7 +18647,7 @@ function nodeToLatex(el) {
18921
18647
  }
18922
18648
  }
18923
18649
  function isOmmlRoot(el) {
18924
- const t = lname(el);
18650
+ const t = localName(el);
18925
18651
  return t === "oMath" || t === "oMathPara";
18926
18652
  }
18927
18653
  function ommlElementToLatex(el) {
@@ -18930,7 +18656,7 @@ function ommlElementToLatex(el) {
18930
18656
  return raw.replace(/\s+/g, " ").trim();
18931
18657
  }
18932
18658
  function isDisplayMath(el) {
18933
- return lname(el) === "oMathPara";
18659
+ return localName(el) === "oMathPara";
18934
18660
  }
18935
18661
 
18936
18662
  // src/docx/parser.ts
@@ -19445,7 +19171,7 @@ function emitParagraphImages(p, imageMap, linked, out) {
19445
19171
  }
19446
19172
  async function parseDocxDocument(buffer, options) {
19447
19173
  precheckZipSize(buffer, MAX_DECOMPRESS_SIZE5);
19448
- const zip = await JSZip4.loadAsync(buffer);
19174
+ const zip = await JSZip5.loadAsync(buffer);
19449
19175
  const warnings = [];
19450
19176
  const docFile = zip.file("word/document.xml");
19451
19177
  if (!docFile) {
@@ -19560,13 +19286,12 @@ async function parseDocxDocument(buffer, options) {
19560
19286
 
19561
19287
  // src/hwpml/parser.ts
19562
19288
  import { DOMParser as DOMParser3 } from "@xmldom/xmldom";
19563
- var MAX_XML_DEPTH2 = 200;
19564
19289
  var MAX_TABLE_ROWS = 5e3;
19565
19290
  var MAX_TABLE_COLS = 500;
19566
19291
  var MAX_HWPML_BYTES = 50 * 1024 * 1024;
19567
19292
  function parseHwpmlDocument(buffer, options) {
19568
19293
  if (buffer.byteLength > MAX_HWPML_BYTES) {
19569
- 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)`);
19570
19295
  }
19571
19296
  const text = new TextDecoder("utf-8").decode(buffer).replace(/^\uFEFF/, "");
19572
19297
  const normalized = text.replace(/&nbsp;/g, "&#160;");
@@ -19583,17 +19308,17 @@ function parseHwpmlDocument(buffer, options) {
19583
19308
  }
19584
19309
  const root = doc.documentElement;
19585
19310
  const metadata = {};
19586
- const docSummary = findChild(root, "DOCSUMMARY");
19311
+ const docSummary = findChildByLocalName(root, "DOCSUMMARY");
19587
19312
  if (docSummary) {
19588
- const title = findChild(docSummary, "TITLE");
19589
- const author = findChild(docSummary, "AUTHOR");
19590
- const date = findChild(docSummary, "DATE");
19591
- if (title) metadata.title = textContent(title).trim();
19592
- if (author) metadata.author = textContent(author).trim();
19593
- 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;
19594
19319
  }
19595
19320
  const paraShapeMap = buildParaShapeMap(root);
19596
- const body = findChild(root, "BODY");
19321
+ const body = findChildByLocalName(root, "BODY");
19597
19322
  if (!body) {
19598
19323
  return { markdown: "", blocks: [], metadata, warnings };
19599
19324
  }
@@ -19621,11 +19346,11 @@ function parseHwpmlDocument(buffer, options) {
19621
19346
  }
19622
19347
  function buildParaShapeMap(root) {
19623
19348
  const map = /* @__PURE__ */ new Map();
19624
- const head = findChild(root, "HEAD");
19349
+ const head = findChildByLocalName(root, "HEAD");
19625
19350
  if (!head) return map;
19626
- const mappingTable = findChild(head, "MAPPINGTABLE");
19351
+ const mappingTable = findChildByLocalName(head, "MAPPINGTABLE");
19627
19352
  if (!mappingTable) return map;
19628
- const paraShapeList = findChild(mappingTable, "PARASHAPELIST");
19353
+ const paraShapeList = findChildByLocalName(mappingTable, "PARASHAPELIST");
19629
19354
  if (!paraShapeList) return map;
19630
19355
  const children = paraShapeList.childNodes;
19631
19356
  for (let i = 0; i < children.length; i++) {
@@ -19647,7 +19372,7 @@ function parseSection2(section, blocks, paraShapeMap, sectionNum, warnings, keep
19647
19372
  walkContent(section, blocks, paraShapeMap, sectionNum, warnings, false, keep);
19648
19373
  }
19649
19374
  function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderFooter, keep, depth = 0) {
19650
- if (depth > MAX_XML_DEPTH2) return;
19375
+ if (depth > MAX_XML_DEPTH) return;
19651
19376
  const children = node.childNodes;
19652
19377
  for (let i = 0; i < children.length; i++) {
19653
19378
  const el = children[i];
@@ -19677,7 +19402,7 @@ function walkContent(node, blocks, paraShapeMap, sectionNum, warnings, inHeaderF
19677
19402
  }
19678
19403
  }
19679
19404
  function walkTablesInP(node, blocks, paraShapeMap, sectionNum, warnings, keep, depth = 0) {
19680
- if (depth > MAX_XML_DEPTH2) return;
19405
+ if (depth > MAX_XML_DEPTH) return;
19681
19406
  const children = node.childNodes;
19682
19407
  for (let i = 0; i < children.length; i++) {
19683
19408
  const el = children[i];
@@ -19708,14 +19433,14 @@ function extractParagraphText(p) {
19708
19433
  return parts.join("").trim();
19709
19434
  }
19710
19435
  function collectCharText(node, parts, depth = 0) {
19711
- if (depth > MAX_XML_DEPTH2) return;
19436
+ if (depth > MAX_XML_DEPTH) return;
19712
19437
  const children = node.childNodes;
19713
19438
  for (let i = 0; i < children.length; i++) {
19714
19439
  const el = children[i];
19715
19440
  if (el.nodeType !== 1) continue;
19716
19441
  const tag = localName(el);
19717
19442
  if (tag === "CHAR") {
19718
- const t = textContent(el);
19443
+ const t = rawTextContent(el);
19719
19444
  if (t) parts.push(t);
19720
19445
  } else if (tag === "TABLE" || tag === "PICTURE" || tag === "SHAPEOBJECT") {
19721
19446
  } else if (tag === "AUTONUM") {
@@ -19724,14 +19449,28 @@ function collectCharText(node, parts, depth = 0) {
19724
19449
  }
19725
19450
  }
19726
19451
  }
19727
- 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) {
19728
19466
  const cells = [];
19467
+ const structured = [];
19729
19468
  const rowCount = parseInt(el.getAttribute("RowCount") ?? "0", 10);
19730
19469
  const colCount = parseInt(el.getAttribute("ColCount") ?? "0", 10);
19731
- if (isNaN(rowCount) || isNaN(colCount) || rowCount === 0 || colCount === 0) return;
19470
+ if (isNaN(rowCount) || isNaN(colCount) || rowCount === 0 || colCount === 0) return null;
19732
19471
  if (rowCount > MAX_TABLE_ROWS || colCount > MAX_TABLE_COLS) {
19733
19472
  warnings.push({ message: `\uD14C\uC774\uBE14 \uD06C\uAE30 \uCD08\uACFC (${rowCount}x${colCount}) \u2014 \uC2A4\uD0B5`, code: "TRUNCATED_TABLE" });
19734
- return;
19473
+ return null;
19735
19474
  }
19736
19475
  const children = el.childNodes;
19737
19476
  for (let i = 0; i < children.length; i++) {
@@ -19746,28 +19485,106 @@ function parseTable2(el, blocks, paraShapeMap, sectionNum, warnings, keep) {
19746
19485
  const colSpan = Math.min(Math.max(1, parseInt(cellEl.getAttribute("ColSpan") ?? "1", 10) || 1), MAX_TABLE_COLS);
19747
19486
  const rowSpan = Math.min(Math.max(1, parseInt(cellEl.getAttribute("RowSpan") ?? "1", 10) || 1), MAX_TABLE_ROWS);
19748
19487
  const cellText = extractCellText(cellEl);
19749
- 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
+ }
19750
19495
  }
19751
19496
  }
19752
- if (cells.length === 0) return;
19497
+ if (cells.length === 0) return null;
19753
19498
  const cellRows = Array.from({ length: rowCount }, () => []);
19754
19499
  for (const cell2 of cells) {
19755
19500
  const r = Math.min(Math.max(cell2.rowAddr ?? 0, 0), rowCount - 1);
19756
19501
  cellRows[r].push(cell2);
19757
19502
  }
19758
19503
  const table2 = buildTable(cellRows, { keepAnchoredEmptyCols: keep });
19759
- const caption = extractShapeCaption(el);
19760
- if (caption.text && caption.before) {
19761
- 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
+ }
19762
19523
  }
19763
- blocks.push({ type: "table", table: table2, pageNumber: sectionNum });
19764
- if (caption.text && !caption.before) {
19765
- 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;
19766
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 });
19767
19584
  }
19768
19585
  function extractShapeCaption(tableEl) {
19769
- const shape = findChild(tableEl, "SHAPEOBJECT");
19770
- const caption = shape && findChild(shape, "CAPTION");
19586
+ const shape = findChildByLocalName(tableEl, "SHAPEOBJECT");
19587
+ const caption = shape && findChildByLocalName(shape, "CAPTION");
19771
19588
  if (!caption) return { text: "", before: false };
19772
19589
  const parts = [];
19773
19590
  collectCellText2(caption, parts, 0);
@@ -19812,30 +19629,6 @@ function collectNestedTableText(node, parts, depth) {
19812
19629
  collectNestedTableText(el, parts, depth + 1);
19813
19630
  }
19814
19631
  }
19815
- function localName(el) {
19816
- return (el.tagName || el.localName || "").replace(/^[^:]+:/, "");
19817
- }
19818
- function findChild(parent, tag) {
19819
- const children = parent.childNodes;
19820
- for (let i = 0; i < children.length; i++) {
19821
- const el = children[i];
19822
- if (el.nodeType === 1 && localName(el) === tag) return el;
19823
- }
19824
- return null;
19825
- }
19826
- function textContent(el) {
19827
- const children = el.childNodes;
19828
- const parts = [];
19829
- for (let i = 0; i < children.length; i++) {
19830
- const node = children[i];
19831
- if (node.nodeType === 3) {
19832
- parts.push(node.nodeValue || "");
19833
- } else if (node.nodeType === 1) {
19834
- parts.push(textContent(node));
19835
- }
19836
- }
19837
- return parts.join("");
19838
- }
19839
19632
  function countSections(body) {
19840
19633
  let count = 0;
19841
19634
  const children = body.childNodes;
@@ -20585,10 +20378,136 @@ function fillInlineFields(text, values, filled, matchedLabels, blockedLabels) {
20585
20378
  }
20586
20379
 
20587
20380
  // src/form/filler-hwpx.ts
20588
- 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
20589
20508
  async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20590
20509
  const u8 = new Uint8Array(hwpxBuffer);
20591
- const zip = await JSZip5.loadAsync(hwpxBuffer);
20510
+ const zip = await JSZip7.loadAsync(hwpxBuffer);
20592
20511
  const sectionPaths = Object.keys(zip.files).filter((name) => /[Ss]ection\d+\.xml$/i.test(name)).sort();
20593
20512
  if (sectionPaths.length === 0) {
20594
20513
  throw new KordocError("HWPX\uC5D0\uC11C \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -20602,8 +20521,23 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20602
20521
  const succeededKeys = /* @__PURE__ */ new Set();
20603
20522
  const replacements = /* @__PURE__ */ new Map();
20604
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
+ }
20605
20539
  for (let si = 0; si < sectionPaths.length; si++) {
20606
- const xml = await zip.file(sectionPaths[si]).async("text");
20540
+ const xml = sectionXmls.get(sectionPaths[si]);
20607
20541
  const scan = scanSectionXml(xml, si);
20608
20542
  const ledger = /* @__PURE__ */ new Map();
20609
20543
  const led = (p) => {
@@ -20785,94 +20719,460 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
20785
20719
  }
20786
20720
  }
20787
20721
  }
20788
- for (const para2 of [...scan.bodyParagraphs, ...scan.excludedParagraphs]) {
20789
- const existing = ledger.get(para2);
20790
- if (existing?.fullText !== void 0) continue;
20791
- const text = matchText(para2);
20792
- const segments = scanInlineSegments(text);
20793
- const matches = segments.map((seg) => matchInlineSegment(seg, cursor, blockedLabels));
20794
- for (let i = 0; i < segments.length; i++) {
20795
- const seg = segments[i];
20796
- const matched = matches[i];
20797
- if (matched === void 0) continue;
20798
- const matchKey = matched.key;
20799
- const newValue = cursor.consume(matchKey);
20800
- if (newValue === void 0) continue;
20801
- const ve = clampSegmentEnd(text, seg, segments[i + 1], matches[i + 1]?.viaExt ?? false);
20802
- const replacement = seg.valueStart === ve ? padInsertion(text, seg.valueStart, newValue) : newValue;
20803
- const l = led(para2);
20804
- l.ranges.push({ start: seg.valueStart, end: ve, replacement });
20805
- matchedLabels.add(matchKey);
20806
- l.filledIdx.push(filled.length);
20807
- l.matchKeys.push(matchKey);
20808
- filled.push({ label: matched.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20809
- }
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");
20810
21023
  }
20811
- const splices = [];
20812
- for (const [para2, l] of ledger) {
20813
- let paraSplices = null;
20814
- if (l.fullText !== void 0) {
20815
- paraSplices = buildParagraphSplices(para2, l.fullText, xml);
20816
- } else if (l.ranges.length > 0) {
20817
- const sorted = [...l.ranges].sort((a, b) => a.start - b.start || a.end - b.end);
20818
- const merged = [];
20819
- for (const r of sorted) {
20820
- const prev = merged[merged.length - 1];
20821
- if (prev && r.start < prev.end) continue;
20822
- merged.push(r);
20823
- }
20824
- if (paraTText(para2, xml) !== null) {
20825
- const precise = [];
20826
- let ok = true;
20827
- for (const r of merged) {
20828
- const sp = buildRangeSplices(para2, xml, r.start, r.end, r.replacement);
20829
- if (!sp) {
20830
- ok = false;
20831
- break;
20832
- }
20833
- precise.push(...sp);
20834
- }
20835
- paraSplices = ok ? precise : null;
20836
- } else if (paraTextPureT(para2, xml)) {
20837
- let text = para2.text;
20838
- for (let k = merged.length - 1; k >= 0; k--) {
20839
- const r = merged[k];
20840
- text = text.slice(0, r.start) + r.replacement + text.slice(r.end);
20841
- }
20842
- paraSplices = buildParagraphSplices(para2, text, xml);
20843
- } else {
20844
- paraSplices = null;
20845
- }
20846
- }
20847
- if (paraSplices === null) {
20848
- for (const idx of l.filledIdx) filled[idx] = null;
20849
- for (const k of l.matchKeys) failedKeys.add(k);
20850
- continue;
20851
- }
20852
- for (const k of l.matchKeys) succeededKeys.add(k);
20853
- 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");
20854
21026
  }
20855
- if (splices.length > 0) {
20856
- splices.push(...allLinesegRemovalSplices(xml));
20857
- 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);
20858
21031
  }
20859
21032
  }
20860
- for (const k of failedKeys) {
20861
- 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");
20862
21035
  }
20863
- const cleanFilled = filled.filter((f) => f !== null);
20864
- const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
20865
- 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";
20866
21065
  return {
20867
- buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
20868
- filled: cleanFilled,
20869
- unmatched,
20870
- ...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
20871
21102
  };
20872
21103
  }
20873
-
20874
- // src/hwpx/generator.ts
20875
- 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
+ };
20876
21176
 
20877
21177
  // src/hwpx/gen-ids.ts
20878
21178
  var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
@@ -24239,7 +24539,7 @@ async function markdownToHwpx(markdown, options) {
24239
24539
  }
24240
24540
  }
24241
24541
  const extraBorderFills = [...remap?.borderFillXmls ?? [], ...bfReg?.emit() ?? []];
24242
- const zip = new JSZip6();
24542
+ const zip = new JSZip8();
24243
24543
  zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
24244
24544
  zip.file("META-INF/container.xml", generateContainerXml());
24245
24545
  zip.file("Contents/content.hpf", generateManifest(chartParts, images.manifestItems(), gongmun ? "gongmun" : "default"));
@@ -24401,8 +24701,55 @@ function diffTableCells(a, b) {
24401
24701
  return result;
24402
24702
  }
24403
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
+
24404
24751
  // src/hwpx/extract-profile.ts
24405
- import JSZip7 from "jszip";
24752
+ import JSZip9 from "jszip";
24406
24753
  function elemsByLocal(root, name) {
24407
24754
  const all = root.getElementsByTagName("*");
24408
24755
  const out = [];
@@ -24605,7 +24952,7 @@ function pick(map, keys) {
24605
24952
  }
24606
24953
  async function hwpxToProfile(input) {
24607
24954
  const buf = input instanceof ArrayBuffer ? input : toArrayBuffer(input);
24608
- const zip = await JSZip7.loadAsync(buf);
24955
+ const zip = await JSZip9.loadAsync(buf);
24609
24956
  const parser = createXmlParser();
24610
24957
  const headerFile = zip.file("Contents/header.xml") ?? zip.file(/[Hh]eader\.xml$/)?.[0];
24611
24958
  let headerXml = "<root/>";
@@ -24920,7 +25267,7 @@ function gongmunLintWarnings(text, limit = 10) {
24920
25267
  }
24921
25268
 
24922
25269
  // src/roundtrip/patcher.ts
24923
- import JSZip8 from "jszip";
25270
+ import JSZip10 from "jszip";
24924
25271
 
24925
25272
  // src/roundtrip/table-rows.ts
24926
25273
  var ROW_OBJECT_RE = /<(?:[A-Za-z0-9_]+:)?(?:tbl|pic|equation|ole|container|shape|drawingObject|drawText|video|chart|fieldBegin|fieldEnd|ctrl)\b/;
@@ -25567,7 +25914,7 @@ async function patchHwpx(original, editedMarkdown, options) {
25567
25914
  }
25568
25915
  let zip;
25569
25916
  try {
25570
- zip = await JSZip8.loadAsync(original);
25917
+ zip = await JSZip10.loadAsync(original);
25571
25918
  } catch {
25572
25919
  return { success: false, applied: 0, skipped, error: "ZIP \uB85C\uB4DC \uC2E4\uD328" };
25573
25920
  }
@@ -25927,6 +26274,8 @@ function replaceOleStream(file, path, newData) {
25927
26274
  }
25928
26275
  var Surgeon = class {
25929
26276
  buf;
26277
+ /** 논리 파일 길이 — buf는 성장 시 기하급수 capacity로 재할당되므로 buf.length와 다를 수 있음 */
26278
+ len;
25930
26279
  fat = [];
25931
26280
  /** FAT 배열을 구성하는 섹터 번호들 (DIFAT 순서) */
25932
26281
  fatSectors = [];
@@ -25946,6 +26295,7 @@ var Surgeon = class {
25946
26295
  }
25947
26296
  const padded = Math.ceil((file.length - SECTOR) / SECTOR) * SECTOR + SECTOR;
25948
26297
  this.buf = Buffer.alloc(padded);
26298
+ this.len = padded;
25949
26299
  file.copy(this.buf);
25950
26300
  this.loadFat();
25951
26301
  this.loadMiniFat();
@@ -26000,7 +26350,7 @@ var Surgeon = class {
26000
26350
  // ── 헬퍼 ──
26001
26351
  sectorOffset(n) {
26002
26352
  const off = SECTOR + n * SECTOR;
26003
- 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}`);
26004
26354
  return off;
26005
26355
  }
26006
26356
  chain(start) {
@@ -26058,26 +26408,40 @@ var Surgeon = class {
26058
26408
  const out = [];
26059
26409
  for (let i = 0; i < this.fat.length && out.length < n; i++) {
26060
26410
  if (this.fat[i] !== FREESECT) continue;
26061
- if (SECTOR + (i + 1) * SECTOR > this.buf.length) continue;
26411
+ if (SECTOR + (i + 1) * SECTOR > this.len) continue;
26062
26412
  this.fat[i] = ENDOFCHAIN;
26063
26413
  const off = this.sectorOffset(i);
26064
26414
  this.buf.fill(0, off, off + SECTOR);
26065
26415
  out.push(i);
26066
26416
  }
26067
26417
  while (out.length < n) {
26068
- this.ensureFatCapacity((this.buf.length - SECTOR) / SECTOR + 2);
26069
- const idx = (this.buf.length - SECTOR) / SECTOR;
26070
- this.buf = Buffer.concat([this.buf, Buffer.alloc(SECTOR)]);
26418
+ this.ensureFatCapacity((this.len - SECTOR) / SECTOR + 2);
26419
+ const idx = this.appendSector();
26071
26420
  this.fat[idx] = ENDOFCHAIN;
26072
26421
  out.push(idx);
26073
26422
  }
26074
26423
  return out;
26075
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
+ }
26076
26441
  /** FAT 배열이 sectorCount개 엔트리를 담도록 확장 (FAT 섹터 추가 + DIFAT 갱신) */
26077
26442
  ensureFatCapacity(sectorCount) {
26078
26443
  while (this.fat.length < sectorCount) {
26079
- const idx = (this.buf.length - SECTOR) / SECTOR;
26080
- this.buf = Buffer.concat([this.buf, Buffer.alloc(SECTOR)]);
26444
+ const idx = this.appendSector();
26081
26445
  for (let i = 0; i < 128; i++) this.fat.push(FREESECT);
26082
26446
  this.fat[idx] = FATSECT;
26083
26447
  this.fatSectors.push(idx);
@@ -26200,7 +26564,7 @@ var Surgeon = class {
26200
26564
  finish() {
26201
26565
  this.wipeFreedSectors();
26202
26566
  this.flushFat();
26203
- return this.buf;
26567
+ return this.len === this.buf.length ? this.buf : this.buf.subarray(0, this.len);
26204
26568
  }
26205
26569
  /** 해제 후 재할당되지 않고 남은 FREESECT 섹터의 바이트를 0으로 채움 (데이터 remanence 제거) */
26206
26570
  wipeFreedSectors() {
@@ -27018,7 +27382,7 @@ function stageParaPatch(scan, para2, newPlain, skip) {
27018
27382
  }
27019
27383
 
27020
27384
  // src/validate.ts
27021
- import JSZip9 from "jszip";
27385
+ import JSZip11 from "jszip";
27022
27386
  import { DOMParser as DOMParser4 } from "@xmldom/xmldom";
27023
27387
  var REQUIRED_FILES = [
27024
27388
  "mimetype",
@@ -27036,7 +27400,7 @@ async function validateHwpx(buffer) {
27036
27400
  const issues = [];
27037
27401
  let zip;
27038
27402
  try {
27039
- zip = await JSZip9.loadAsync(buffer);
27403
+ zip = await JSZip11.loadAsync(buffer);
27040
27404
  } catch (err) {
27041
27405
  return {
27042
27406
  ok: false,
@@ -27104,10 +27468,10 @@ async function validateHwpx(buffer) {
27104
27468
  }
27105
27469
 
27106
27470
  // src/roundtrip/session.ts
27107
- import JSZip10 from "jszip";
27471
+ import JSZip12 from "jszip";
27108
27472
  async function buildState(bytes) {
27109
27473
  const parsed = await parseHwpxDocument(u8ToArrayBuffer2(bytes));
27110
- const zip = await JSZip10.loadAsync(bytes);
27474
+ const zip = await JSZip12.loadAsync(bytes);
27111
27475
  const sectionPaths = await resolveSectionEntryNames(zip);
27112
27476
  if (sectionPaths.length === 0) {
27113
27477
  throw new Error("HWPX \uC139\uC158 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4");
@@ -27449,7 +27813,7 @@ async function patchHwpxBlocks(original, edits, options) {
27449
27813
  }
27450
27814
 
27451
27815
  // src/print/renderer.ts
27452
- import { existsSync } from "fs";
27816
+ import { existsSync as existsSync2 } from "fs";
27453
27817
  import MarkdownIt from "markdown-it";
27454
27818
  var PRESETS = {
27455
27819
  default: `
@@ -27588,7 +27952,7 @@ function findChromiumPath() {
27588
27952
  const linux = ["/usr/bin/google-chrome", "/usr/bin/chromium-browser", "/usr/bin/chromium"];
27589
27953
  const candidates = process.platform === "win32" ? win : process.platform === "darwin" ? mac : linux;
27590
27954
  for (const p of candidates) {
27591
- if (p && existsSync(p)) return p;
27955
+ if (p && existsSync2(p)) return p;
27592
27956
  }
27593
27957
  return null;
27594
27958
  }
@@ -27648,7 +28012,7 @@ async function parse(input, options) {
27648
28012
  }
27649
28013
  async function parseImage(buffer, options) {
27650
28014
  try {
27651
- const { parseImageDocument } = await import("./image-ocr-ORATZW73.js");
28015
+ const { parseImageDocument } = await import("./image-ocr-R44RYDBU.js");
27652
28016
  const { blocks, warnings } = await parseImageDocument(buffer, options);
27653
28017
  return {
27654
28018
  success: true,
@@ -27665,7 +28029,7 @@ async function parseImage(buffer, options) {
27665
28029
  async function parseHwp3(buffer, options) {
27666
28030
  try {
27667
28031
  const { markdown, blocks, metadata, outline, warnings } = parseHwp3Document(buffer, options);
27668
- return { success: true, fileType: "hwp3", markdown, blocks, metadata, outline, warnings };
28032
+ return { success: true, fileType: "hwp3", markdown, blocks, metadata, outline, warnings, pageCount: metadata?.pageCount };
27669
28033
  } catch (err) {
27670
28034
  return { success: false, fileType: "hwp3", error: sanitizeError(err), code: classifyError(err) };
27671
28035
  }
@@ -27673,7 +28037,7 @@ async function parseHwp3(buffer, options) {
27673
28037
  async function parseHwpx(buffer, options) {
27674
28038
  try {
27675
28039
  const { markdown, blocks, metadata, outline, warnings, images } = await parseHwpxDocument(buffer, options);
27676
- 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 };
27677
28041
  } catch (err) {
27678
28042
  return { success: false, fileType: "hwpx", error: sanitizeError(err), code: classifyError(err) };
27679
28043
  }
@@ -27692,13 +28056,14 @@ async function parseHwp(buffer, options) {
27692
28056
  markdown: com.markdown,
27693
28057
  blocks: com.blocks,
27694
28058
  metadata: com.metadata,
27695
- warnings: com.warnings
28059
+ warnings: com.warnings,
28060
+ pageCount
27696
28061
  };
27697
28062
  }
27698
28063
  } catch {
27699
28064
  }
27700
28065
  }
27701
- 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 };
27702
28067
  } catch (err) {
27703
28068
  return { success: false, fileType: "hwp", error: sanitizeError(err), code: classifyError(err) };
27704
28069
  }
@@ -27706,7 +28071,7 @@ async function parseHwp(buffer, options) {
27706
28071
  async function parsePdf(buffer, options) {
27707
28072
  let parsePdfDocument;
27708
28073
  try {
27709
- const mod = await import("./parser-GZDOZCHM.js");
28074
+ const mod = await import("./parser-W3POOYQQ.js");
27710
28075
  parsePdfDocument = mod.parsePdfDocument;
27711
28076
  } catch {
27712
28077
  return {
@@ -27718,7 +28083,7 @@ async function parsePdf(buffer, options) {
27718
28083
  }
27719
28084
  try {
27720
28085
  const { markdown, blocks, metadata, outline, warnings, isImageBased, pageQuality, qualitySummary, images } = await parsePdfDocument(buffer, options);
27721
- 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 };
27722
28087
  } catch (err) {
27723
28088
  const isImageBased = err instanceof Error && "isImageBased" in err ? true : void 0;
27724
28089
  return { success: false, fileType: "pdf", error: sanitizeError(err), code: classifyError(err), isImageBased };
@@ -27727,7 +28092,7 @@ async function parsePdf(buffer, options) {
27727
28092
  async function parseXlsx(buffer, options) {
27728
28093
  try {
27729
28094
  const { markdown, blocks, metadata, warnings } = await parseXlsxDocument(buffer, options);
27730
- return { success: true, fileType: "xlsx", markdown, blocks, metadata, warnings };
28095
+ return { success: true, fileType: "xlsx", markdown, blocks, metadata, warnings, pageCount: metadata?.pageCount };
27731
28096
  } catch (err) {
27732
28097
  return { success: false, fileType: "xlsx", error: sanitizeError(err), code: classifyError(err) };
27733
28098
  }
@@ -27735,7 +28100,7 @@ async function parseXlsx(buffer, options) {
27735
28100
  async function parseXls(buffer, options) {
27736
28101
  try {
27737
28102
  const { markdown, blocks, metadata, warnings } = await parseXlsDocument(buffer, options);
27738
- return { success: true, fileType: "xls", markdown, blocks, metadata, warnings };
28103
+ return { success: true, fileType: "xls", markdown, blocks, metadata, warnings, pageCount: metadata?.pageCount };
27739
28104
  } catch (err) {
27740
28105
  return { success: false, fileType: "xls", error: sanitizeError(err), code: classifyError(err) };
27741
28106
  }
@@ -27743,7 +28108,7 @@ async function parseXls(buffer, options) {
27743
28108
  async function parseDocx(buffer, options) {
27744
28109
  try {
27745
28110
  const { markdown, blocks, metadata, outline, warnings, images } = await parseDocxDocument(buffer, options);
27746
- 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 };
27747
28112
  } catch (err) {
27748
28113
  return { success: false, fileType: "docx", error: sanitizeError(err), code: classifyError(err) };
27749
28114
  }
@@ -27751,7 +28116,7 @@ async function parseDocx(buffer, options) {
27751
28116
  async function parseHwpml(buffer, options) {
27752
28117
  try {
27753
28118
  const { markdown, blocks, metadata, outline, warnings } = parseHwpmlDocument(buffer, options);
27754
- return { success: true, fileType: "hwpml", markdown, blocks, metadata, outline, warnings };
28119
+ return { success: true, fileType: "hwpml", markdown, blocks, metadata, outline, warnings, pageCount: metadata?.pageCount };
27755
28120
  } catch (err) {
27756
28121
  return { success: false, fileType: "hwpml", error: sanitizeError(err), code: classifyError(err) };
27757
28122
  }
@@ -27797,9 +28162,13 @@ async function fillForm(input, values, outputFormat = "markdown") {
27797
28162
  }
27798
28163
 
27799
28164
  export {
27800
- PRESET_ALIAS,
27801
- normalizeGongmunPreset,
27802
- incompatibleGongmunWarnings,
28165
+ isZipFile,
28166
+ isHwpxFile,
28167
+ isOldHwpFile,
28168
+ isPdfFile,
28169
+ detectFormat,
28170
+ detectOle2Format,
28171
+ detectZipFormat,
27803
28172
  extractHwpxMetadataOnly,
27804
28173
  extractHwp5MetadataOnly,
27805
28174
  formatFillValue,
@@ -27810,10 +28179,18 @@ export {
27810
28179
  inferFieldType,
27811
28180
  extractFormSchema,
27812
28181
  fillFormFields,
28182
+ extractClickHereFields,
27813
28183
  fillHwpx,
28184
+ PRESET_ALIAS,
28185
+ normalizeGongmunPreset,
28186
+ incompatibleGongmunWarnings,
27814
28187
  markdownToHwpx,
27815
28188
  compare,
27816
28189
  diffBlocks,
28190
+ BUILTIN_TEMPLATES,
28191
+ resolveBuiltinTemplate,
28192
+ readBuiltinTemplate,
28193
+ readBuiltinTemplateSample,
27817
28194
  hwpxToProfile,
27818
28195
  isKnownFont,
27819
28196
  unknownFontWarnings,
@@ -27840,4 +28217,4 @@ export {
27840
28217
  parseHwpml,
27841
28218
  fillForm
27842
28219
  };
27843
- //# sourceMappingURL=chunk-YNAPK4H6.js.map
28220
+ //# sourceMappingURL=chunk-DRZUJ2RK.js.map