kordoc 4.0.2 → 4.0.4

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 (84) hide show
  1. package/README.md +800 -768
  2. package/dist/{-XMKVL3NX.js → -Q4ZCVQFI.js} +10 -8
  3. package/dist/chunk-2RIOXYGA.js +92 -0
  4. package/dist/chunk-2RIOXYGA.js.map +1 -0
  5. package/dist/chunk-5RPYBC2Q.js +88 -0
  6. package/dist/chunk-5RPYBC2Q.js.map +1 -0
  7. package/dist/chunk-DCZVOIEO.cjs.map +1 -1
  8. package/dist/{chunk-IKBIIEVJ.js → chunk-DKMKJMR6.js} +5 -4
  9. package/dist/chunk-DKMKJMR6.js.map +1 -0
  10. package/dist/chunk-GS7T56RP.cjs.map +1 -1
  11. package/dist/{chunk-STMLZDKR.js → chunk-KA5AN73R.js} +2 -2
  12. package/dist/chunk-KA5AN73R.js.map +1 -0
  13. package/dist/{chunk-3IYQYBUF.js → chunk-KSVLQ5BC.js} +33 -4
  14. package/dist/chunk-KSVLQ5BC.js.map +1 -0
  15. package/dist/{chunk-5CJGKKMZ.js → chunk-MEPHGCPQ.js} +1 -1
  16. package/dist/chunk-MEPHGCPQ.js.map +1 -0
  17. package/dist/{chunk-AHWRAGXN.js → chunk-MLNHZN32.js} +1876 -965
  18. package/dist/chunk-MLNHZN32.js.map +1 -0
  19. package/dist/chunk-MOL7MDBG.js +0 -0
  20. package/dist/{chunk-WN7LZHII.cjs → chunk-NIW7CZYX.cjs} +37 -8
  21. package/dist/chunk-NIW7CZYX.cjs.map +1 -0
  22. package/dist/{chunk-6SD4X5PT.js → chunk-S3KGEEBQ.js} +76 -25
  23. package/dist/chunk-S3KGEEBQ.js.map +1 -0
  24. package/dist/{chunk-67RANGFQ.js → chunk-TXNRIVWA.js} +3 -3
  25. package/dist/chunk-TXNRIVWA.js.map +1 -0
  26. package/dist/{chunk-6YIYFWO2.js → chunk-ZJXR4PWY.js} +33 -4
  27. package/dist/chunk-ZJXR4PWY.js.map +1 -0
  28. package/dist/cli.js +120 -73
  29. package/dist/cli.js.map +1 -1
  30. package/dist/{detect-PJZMUL2Z.js → detect-RI2MQ33K.js} +2 -2
  31. package/dist/formula-5NKVS2LR.cjs.map +1 -1
  32. package/dist/formula-JCNF43NE.js +0 -0
  33. package/dist/index.cjs +21139 -20176
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +52 -5
  36. package/dist/index.d.ts +52 -5
  37. package/dist/index.js +20969 -20006
  38. package/dist/index.js.map +1 -1
  39. package/dist/mcp.js +113 -90
  40. package/dist/mcp.js.map +1 -1
  41. package/dist/page-range-737B4EZW.js +0 -0
  42. package/dist/page-range-P7SDW6LR.cjs.map +1 -1
  43. package/dist/{parser-6AYYGX6J.cjs → parser-2ZUTCU2G.cjs} +108 -19
  44. package/dist/parser-2ZUTCU2G.cjs.map +1 -0
  45. package/dist/{parser-3HM2VYKK.js → parser-46JGSTPR.js} +97 -8
  46. package/dist/parser-46JGSTPR.js.map +1 -0
  47. package/dist/{parser-2Q2YKEJG.js → parser-TLCC67W6.js} +96 -7
  48. package/dist/parser-TLCC67W6.js.map +1 -0
  49. package/dist/profile-io-MRVIJ5T4.js +11 -0
  50. package/dist/{provider-H4WWSPOV.js → provider-4ZJKV3DC.js} +1 -1
  51. package/dist/provider-4ZJKV3DC.js.map +1 -0
  52. package/dist/{provider-7H4CPZYS.js → provider-AKROB7WQ.js} +1 -1
  53. package/dist/provider-AKROB7WQ.js.map +1 -0
  54. package/dist/{provider-5K7O3Z2O.cjs → provider-G4C2V2PD.cjs} +1 -1
  55. package/dist/provider-G4C2V2PD.cjs.map +1 -0
  56. package/dist/render-TTOG6CHW.js +10 -0
  57. package/dist/seal-NMLZ3M3K.js +10 -0
  58. package/dist/seal-NMLZ3M3K.js.map +1 -0
  59. package/dist/setup-57FB3LSP.js +0 -0
  60. package/dist/{watch-6OHF727O.js → watch-NZNVJ4YD.js} +8 -8
  61. package/dist/watch-NZNVJ4YD.js.map +1 -0
  62. package/package.json +4 -4
  63. package/dist/chunk-3IYQYBUF.js.map +0 -1
  64. package/dist/chunk-5CJGKKMZ.js.map +0 -1
  65. package/dist/chunk-67RANGFQ.js.map +0 -1
  66. package/dist/chunk-6SD4X5PT.js.map +0 -1
  67. package/dist/chunk-6YIYFWO2.js.map +0 -1
  68. package/dist/chunk-AHWRAGXN.js.map +0 -1
  69. package/dist/chunk-IKBIIEVJ.js.map +0 -1
  70. package/dist/chunk-STMLZDKR.js.map +0 -1
  71. package/dist/chunk-WN7LZHII.cjs.map +0 -1
  72. package/dist/parser-2Q2YKEJG.js.map +0 -1
  73. package/dist/parser-3HM2VYKK.js.map +0 -1
  74. package/dist/parser-6AYYGX6J.cjs.map +0 -1
  75. package/dist/provider-5K7O3Z2O.cjs.map +0 -1
  76. package/dist/provider-7H4CPZYS.js.map +0 -1
  77. package/dist/provider-H4WWSPOV.js.map +0 -1
  78. package/dist/render-W4VCJE6N.js +0 -10
  79. package/dist/seal-DRPTNHOP.js +0 -10
  80. package/dist/watch-6OHF727O.js.map +0 -1
  81. /package/dist/{-XMKVL3NX.js.map → -Q4ZCVQFI.js.map} +0 -0
  82. /package/dist/{detect-PJZMUL2Z.js.map → detect-RI2MQ33K.js.map} +0 -0
  83. /package/dist/{render-W4VCJE6N.js.map → profile-io-MRVIJ5T4.js.map} +0 -0
  84. /package/dist/{seal-DRPTNHOP.js.map → render-TTOG6CHW.js.map} +0 -0
@@ -11,13 +11,13 @@ import {
11
11
  dedupeRunningHeaders,
12
12
  flattenLayoutTables,
13
13
  mapPuaText
14
- } from "./chunk-6YIYFWO2.js";
14
+ } from "./chunk-ZJXR4PWY.js";
15
15
  import {
16
16
  detectFormat,
17
17
  detectOle2Format,
18
18
  detectZipFormat,
19
19
  parseLenientCfb
20
- } from "./chunk-5CJGKKMZ.js";
20
+ } from "./chunk-MEPHGCPQ.js";
21
21
  import {
22
22
  parsePageRange
23
23
  } from "./chunk-MOL7MDBG.js";
@@ -32,14 +32,14 @@ import {
32
32
  paraTextPureT,
33
33
  patchZipEntries,
34
34
  scanSectionXml
35
- } from "./chunk-67RANGFQ.js";
35
+ } from "./chunk-TXNRIVWA.js";
36
36
  import {
37
37
  SPACE_EM_FIXED,
38
38
  charWidthEm1000,
39
39
  fitRatioForFewerLines,
40
40
  measureTextWidth,
41
41
  simulateWrap
42
- } from "./chunk-6SD4X5PT.js";
42
+ } from "./chunk-S3KGEEBQ.js";
43
43
  import {
44
44
  MAX_DECOMPRESS_SIZE,
45
45
  MAX_XML_DEPTH,
@@ -50,7 +50,7 @@ import {
50
50
  createXmlParser,
51
51
  extractTextFromNode,
52
52
  findChildByLocalName
53
- } from "./chunk-IKBIIEVJ.js";
53
+ } from "./chunk-DKMKJMR6.js";
54
54
  import {
55
55
  KordocError,
56
56
  classifyError,
@@ -61,7 +61,7 @@ import {
61
61
  sanitizeHref,
62
62
  stripDtd,
63
63
  toArrayBuffer
64
- } from "./chunk-STMLZDKR.js";
64
+ } from "./chunk-KA5AN73R.js";
65
65
 
66
66
  // src/index.ts
67
67
  import { readFile } from "fs/promises";
@@ -183,7 +183,8 @@ async function extractHwpxStyles(zip, decompressed) {
183
183
  styles: /* @__PURE__ */ new Map(),
184
184
  numberings: /* @__PURE__ */ new Map(),
185
185
  bullets: /* @__PURE__ */ new Map(),
186
- paraHeadings: /* @__PURE__ */ new Map()
186
+ paraHeadings: /* @__PURE__ */ new Map(),
187
+ paraIndents: /* @__PURE__ */ new Map()
187
188
  };
188
189
  const headerPaths = ["Contents/header.xml", "header.xml", "Contents/head.xml", "head.xml"];
189
190
  for (const hp of headerPaths) {
@@ -199,12 +200,13 @@ async function extractHwpxStyles(zip, decompressed) {
199
200
  const parser = createXmlParser();
200
201
  const doc = parser.parseFromString(stripDtd(xml), "text/xml");
201
202
  if (!doc.documentElement) continue;
202
- parseCharProperties(doc, result.charProperties);
203
- parseStyleElements(doc, result.styles);
204
203
  const domDoc = doc;
204
+ parseCharProperties(domDoc, result.charProperties);
205
+ parseStyleElements(domDoc, result.styles);
205
206
  parseNumberings(domDoc, result.numberings);
206
207
  parseBullets(domDoc, result.bullets);
207
208
  parseParaHeadings(domDoc, result.paraHeadings);
209
+ parseParaIndents(domDoc, result.paraIndents);
208
210
  break;
209
211
  } catch {
210
212
  continue;
@@ -232,6 +234,14 @@ function parseCharProperties(doc, map) {
232
234
  if (bold === "true" || bold === "1") prop.bold = true;
233
235
  const italic = el.getAttribute("italic");
234
236
  if (italic === "true" || italic === "1") prop.italic = true;
237
+ const kids2 = el.childNodes;
238
+ for (let j = 0; j < (kids2?.length ?? 0); j++) {
239
+ const k = kids2[j];
240
+ if (k.nodeType !== 1) continue;
241
+ const localTag = (k.tagName || "").replace(/^[^:]+:/, "");
242
+ if (localTag === "bold") prop.bold = true;
243
+ else if (localTag === "italic") prop.italic = true;
244
+ }
235
245
  const fontFaces = el.getElementsByTagName("*");
236
246
  for (let j = 0; j < fontFaces.length; j++) {
237
247
  const ff = fontFaces[j];
@@ -304,6 +314,28 @@ function parseBullets(doc, map) {
304
314
  if (map.size > 0) break;
305
315
  }
306
316
  }
317
+ function parseParaIndents(doc, map) {
318
+ const tagNames = ["hh:paraPr", "paraPr"];
319
+ for (const tagName of tagNames) {
320
+ const elements = doc.getElementsByTagName(tagName);
321
+ for (let i = 0; i < elements.length; i++) {
322
+ const el = elements[i];
323
+ const id = el.getAttribute("id") || "";
324
+ if (!id) continue;
325
+ const margin = findChildByLocalName(el, "margin");
326
+ if (!margin) continue;
327
+ const readHu = (name) => {
328
+ const child = findChildByLocalName(margin, name);
329
+ const v = parseInt(child?.getAttribute("value") || child?.textContent || "", 10);
330
+ return Number.isFinite(v) ? v : 0;
331
+ };
332
+ const left = readHu("left");
333
+ const intent = readHu("intent");
334
+ if (left !== 0 || intent !== 0) map.set(id, { left, intent });
335
+ }
336
+ if (map.size > 0) break;
337
+ }
338
+ }
307
339
  function parseParaHeadings(doc, map) {
308
340
  const tagNames = ["hh:paraPr", "paraPr"];
309
341
  for (const tagName of tagNames) {
@@ -832,95 +864,479 @@ function hmlToLatex(hmlEqStr) {
832
864
  return out;
833
865
  }
834
866
 
835
- // src/hwpx/para-heading.ts
836
- var HANGUL_SYLLABLE_SEQ = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
837
- var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
838
- function toRoman(n) {
839
- if (n <= 0 || n >= 4e3) return String(n);
840
- const table2 = [
841
- [1e3, "M"],
842
- [900, "CM"],
843
- [500, "D"],
844
- [400, "CD"],
845
- [100, "C"],
846
- [90, "XC"],
847
- [50, "L"],
848
- [40, "XL"],
849
- [10, "X"],
850
- [9, "IX"],
851
- [5, "V"],
852
- [4, "IV"],
853
- [1, "I"]
854
- ];
855
- let out = "";
856
- for (const [v, s] of table2) {
857
- while (n >= v) {
858
- out += s;
859
- n -= v;
860
- }
867
+ // src/hwpx/geometry.ts
868
+ var A4_W_HU = 59528;
869
+ var A4_H_HU = 84188;
870
+ var HU_PER_MM = 7200 / 25.4;
871
+ function mmToHwpunit(mm) {
872
+ return Math.round(mm * 7200 / 25.4);
873
+ }
874
+ var BODY_WIDTH_20MM = mmToHwpunit(170);
875
+ var COVER_MEASURED_W = 48180;
876
+ var DATA_TABLE_ID_BASE = 1e3;
877
+ var CHART_TABLE_ID_BASE = 91e5;
878
+ var GJ_TABLE_ID_BASE = 92e5;
879
+ var EXTRA_TABLE_ID_BASE = 93e5;
880
+ var PIC_ID_BASE = 94e5;
881
+
882
+ // src/hwpx/gaejosik.ts
883
+ var GAEJOSIK_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
884
+ function gaejosikMarker(depth, bullet2 = "\u25CB") {
885
+ if (depth === 1) return bullet2;
886
+ return GAEJOSIK_BULLETS[Math.min(depth, GAEJOSIK_BULLETS.length - 1)];
887
+ }
888
+ function gaejosikSizes(bodyHeight, ov = {}) {
889
+ const s = bodyHeight / 1500;
890
+ const r = (v, pt) => pt ? Math.round(pt * 100) : Math.round(v * s);
891
+ return {
892
+ /** □ 대항목 — HY헤드라인M 16pt */
893
+ dae: r(1600, ov.dae),
894
+ /** ※ 참고 — 한양중고딕 13pt */
895
+ cham: r(1300, ov.cham),
896
+ /** 장 헤더(로마숫자·제목) 17pt */
897
+ chapter: r(1700, ov.chapter),
898
+ /** 표지 제목 30pt */
899
+ coverTitle: r(3e3, ov.coverTitle),
900
+ /** 표지 날짜·기관명 25pt */
901
+ coverSub: r(2500, ov.coverSub),
902
+ /** 목차 라벨 "목 차" 28pt */
903
+ tocLabel: r(2800, ov.tocLabel),
904
+ /** 목차 로마숫자 21pt(한양신명조) */
905
+ tocRoman: r(2100, ov.tocRoman),
906
+ /** 목차 항목 18pt */
907
+ tocItem: r(1800, ov.tocItem),
908
+ /** 표 셀 — 맑은 고딕 12pt (실측: 정부 양식 표) */
909
+ table: r(1200, ov.table),
910
+ /** 표지 장식 바 셀 빈 문단 — 6pt (실측: 바 높이 818 안에 줄높이 수납) */
911
+ bar: r(600),
912
+ /** 본문 첫 페이지 제목 박스 — HY헤드라인M 22pt (실측: GT3 표④) */
913
+ bodyTitle: r(2200, ov.bodyTitle)
914
+ };
915
+ }
916
+ function gaejosikSpaceBefore(depth, bodyHeight) {
917
+ const s = bodyHeight / 1500;
918
+ const ui = [3e3, 2e3, 1200, 600][Math.min(depth, 3)];
919
+ return Math.round(ui * s);
920
+ }
921
+ function gaejosikLevelIndent(depth, bodyHeight, sizes = {}, bullet2 = "\u25CB") {
922
+ const lefts = [0, 1, 1.5, 2];
923
+ const left = depth <= 3 ? Math.round(lefts[depth] * bodyHeight) : Math.round((2 + (depth - 3) * 0.5) * bodyHeight);
924
+ const h = depth === 0 ? gaejosikSizes(bodyHeight, sizes).dae : bodyHeight;
925
+ return { left, indent: -markerWidth(gaejosikMarker(depth, bullet2), h) };
926
+ }
927
+ function gaejosikChamIndent(bodyHeight, sizes = {}) {
928
+ const cham = gaejosikSizes(bodyHeight, sizes).cham;
929
+ return { left: Math.round(2.5 * cham), indent: -markerWidth("\u203B", cham) };
930
+ }
931
+ function gaejosikTocItemIndent(bodyHeight, sizes = {}) {
932
+ const sz = gaejosikSizes(bodyHeight, sizes);
933
+ const hang = markerWidth("\u2167.", sz.tocRoman);
934
+ return { left: 2e3 + hang, indent: -hang };
935
+ }
936
+ function formatGaejosikDate(d) {
937
+ return `${d.getFullYear()}. ${d.getMonth() + 1}. ${d.getDate()}.`;
938
+ }
939
+ function stripChapterNumber(title) {
940
+ return title.replace(/^\s*[□○ㅇㆍ-]\s+/u, "").replace(/^\s*(?:\d+(?:\.\d+)*|[Ⅰ-Ⅻ]+)\s*[.)]\s*/u, "").trim();
941
+ }
942
+ var GAEJOSIK_COLORS = {
943
+ /** 장헤더 로마숫자 셀·표지 진한 바 음영 */
944
+ primary: "#193AAA",
945
+ /** 장헤더 셀 테두리 */
946
+ border: "#006699",
947
+ /** 표지 연한 바 */
948
+ accent: "#A0B4E6",
949
+ /** 장헤더 제목 셀 음영 */
950
+ titleFill: "#F2F2F2",
951
+ /** 장헤더 제목 셀 상하선 */
952
+ titleLine: "#A6A6A6",
953
+ /** 목차 박스 테두리 */
954
+ tocBorder: "#514BAC",
955
+ /** 목차 배너 라벤더 스트라이프 (실측: GT3 표②) */
956
+ tocStripe: "#E0E5FA"
957
+ };
958
+ var GAEJOSIK_BASE_WIDTH = BODY_WIDTH_20MM;
959
+ var CHAPTER_GEOM = { numW: 3327, gapW: 848, titleW: 43513, rowH: 2832 };
960
+ var COVER_GEOM = {
961
+ totalW: COVER_MEASURED_W,
962
+ topDarkW: 38219,
963
+ topLightW: 9961,
964
+ barH: 818,
965
+ titleH: 11060,
966
+ botDarkW: 10466,
967
+ botLightW: 37714,
968
+ botBarH: 812
969
+ };
970
+ var TOC_GEOM = { boxW: 46492, boxH: 57840 };
971
+ function chapterGeom(bodyHeight, ov = {}, bodyWidth = GAEJOSIK_BASE_WIDTH) {
972
+ const s = gaejosikSizes(bodyHeight, ov).chapter / 1700;
973
+ const ws = bodyWidth / GAEJOSIK_BASE_WIDTH;
974
+ const numW = Math.round(CHAPTER_GEOM.numW * ws);
975
+ const gapW = Math.round(CHAPTER_GEOM.gapW * ws);
976
+ const titleW = Math.round((CHAPTER_GEOM.numW + CHAPTER_GEOM.gapW + CHAPTER_GEOM.titleW) * ws) - numW - gapW;
977
+ return { numW, gapW, titleW, rowH: Math.round(CHAPTER_GEOM.rowH * s) };
978
+ }
979
+ function coverGeom(bodyHeight, ov = {}, bodyWidth = GAEJOSIK_BASE_WIDTH) {
980
+ const s = gaejosikSizes(bodyHeight, ov).coverTitle / 3e3;
981
+ const w = (v) => Math.round(v * bodyWidth / GAEJOSIK_BASE_WIDTH);
982
+ const totalW = w(COVER_GEOM.totalW);
983
+ return {
984
+ ...COVER_GEOM,
985
+ totalW,
986
+ topDarkW: w(COVER_GEOM.topDarkW),
987
+ topLightW: totalW - w(COVER_GEOM.topDarkW),
988
+ botDarkW: w(COVER_GEOM.botDarkW),
989
+ botLightW: totalW - w(COVER_GEOM.botDarkW),
990
+ titleH: Math.round(COVER_GEOM.titleH * s)
991
+ };
992
+ }
993
+ var TOC_BANNER_GEOM = {
994
+ colWs: [565, 565, 1414, 13191, 1414, 565, 565],
995
+ h: 3931
996
+ };
997
+ function tocBannerGeom(bodyHeight, ov = {}) {
998
+ const s = gaejosikSizes(bodyHeight, ov).tocLabel / 2800;
999
+ return { colWs: TOC_BANNER_GEOM.colWs, h: Math.round(TOC_BANNER_GEOM.h * s) };
1000
+ }
1001
+ var BODY_TITLE_GEOM = { barH: 600, titleH: 3566 };
1002
+ function bodyTitleGeom(bodyHeight, ov = {}) {
1003
+ const s = gaejosikSizes(bodyHeight, ov).bodyTitle / 2200;
1004
+ return { barH: BODY_TITLE_GEOM.barH, titleH: Math.round(BODY_TITLE_GEOM.titleH * s) };
1005
+ }
1006
+
1007
+ // src/hwpx/gongmun.ts
1008
+ var OFFICIAL_MARGINS = { top: 20, bottom: 15, left: 20, right: 15 };
1009
+ var GAEJOSIK_MARGINS = { top: 15, bottom: 15, left: 20, right: 20 };
1010
+ var GAEJOSIK_HEADER_FOOTER = 4251;
1011
+ var PRESET_DEFAULTS = {
1012
+ // 서울시 실결재 104건 중 12pt 64건(13pt 30, 15pt 5) — 지배값을 기본으로 사용.
1013
+ official: { bodyPt: 12, lineSpacing: 160, numbering: "standard" },
1014
+ report: { bodyPt: 15, lineSpacing: 160, numbering: "report" },
1015
+ // 실측 추진계획안: □ → ㅇ → * 계층.
1016
+ plan: { bodyPt: 15, lineSpacing: 160, numbering: "report" },
1017
+ notice: { bodyPt: 15, lineSpacing: 160, numbering: "standard" },
1018
+ minutes: { bodyPt: 14, lineSpacing: 130, numbering: "standard" },
1019
+ gaejosik: { bodyPt: 15, lineSpacing: 160, numbering: "gaejosik" },
1020
+ // 보도자료 — 실측(국토부 실물): 본문 바탕 14pt 160%, □→ㅇ→*(각주) 부호
1021
+ press: { bodyPt: 14, lineSpacing: 160, numbering: "report" }
1022
+ };
1023
+ var PRESET_ALIAS = {
1024
+ official: "official",
1025
+ \uAE30\uC548\uBB38: "official",
1026
+ \uC2DC\uD589\uBB38: "official",
1027
+ \uACF5\uBB38: "official",
1028
+ \uACF5\uBB38\uC11C: "official",
1029
+ report: "report",
1030
+ \uBCF4\uACE0\uC11C: "report",
1031
+ plan: "plan",
1032
+ \uACC4\uD68D\uC11C: "plan",
1033
+ \uACC4\uD68D: "plan",
1034
+ notice: "notice",
1035
+ \uD1B5\uC9C0: "notice",
1036
+ \uC54C\uB9BC: "notice",
1037
+ \uC548\uB0B4: "notice",
1038
+ minutes: "minutes",
1039
+ \uD68C\uC758\uB85D: "minutes",
1040
+ gaejosik: "gaejosik",
1041
+ \uAC1C\uC870\uC2DD: "gaejosik",
1042
+ \uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
1043
+ \uC815\uBD80\uBCF4\uACE0\uC11C: "gaejosik",
1044
+ \uC815\uBD80\uD45C\uC900\uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
1045
+ press: "press",
1046
+ \uBCF4\uB3C4\uC790\uB8CC: "press"
1047
+ };
1048
+ function normalizeGongmunPreset(preset) {
1049
+ if (!preset) return "official";
1050
+ return PRESET_ALIAS[preset.trim()] ?? "official";
1051
+ }
1052
+ function usesReportFonts(preset) {
1053
+ return preset === "gaejosik" || preset === "report" || preset === "plan";
1054
+ }
1055
+ function usesAsteriskThird(preset) {
1056
+ return preset === "plan" || preset === "press";
1057
+ }
1058
+ function needsGaejosikAssets(gongmun) {
1059
+ return usesReportFonts(gongmun.preset) || gongmun.cover !== null || gongmun.toc;
1060
+ }
1061
+ function assertFiniteRange(name, value, min, max) {
1062
+ if (!Number.isFinite(value) || value < min || value > max) {
1063
+ throw new KordocError(`${name} must be a finite number between ${min} and ${max}`);
861
1064
  }
862
- return out;
863
1065
  }
864
- function formatHeadNumber(n, numFormat) {
865
- if (n <= 0) n = 1;
866
- switch (numFormat) {
867
- case "DIGIT":
868
- return String(n);
869
- case "CIRCLED_DIGIT":
870
- return n <= 20 ? String.fromCodePoint(9312 + n - 1) : `(${n})`;
871
- case "HANGUL_SYLLABLE":
872
- return HANGUL_SYLLABLE_SEQ[(n - 1) % HANGUL_SYLLABLE_SEQ.length];
873
- case "CIRCLED_HANGUL_SYLLABLE":
874
- return n <= 14 ? String.fromCodePoint(12910 + n - 1) : HANGUL_SYLLABLE_SEQ[(n - 1) % 14];
875
- case "HANGUL_JAMO":
876
- return HANGUL_JAMO_SEQ[(n - 1) % HANGUL_JAMO_SEQ.length];
877
- case "CIRCLED_HANGUL_JAMO":
878
- return n <= 14 ? String.fromCodePoint(12896 + n - 1) : HANGUL_JAMO_SEQ[(n - 1) % 14];
879
- case "LATIN_CAPITAL":
880
- return String.fromCharCode(65 + (n - 1) % 26);
881
- case "LATIN_SMALL":
882
- return String.fromCharCode(97 + (n - 1) % 26);
883
- case "CIRCLED_LATIN_CAPITAL":
884
- return n <= 26 ? String.fromCodePoint(9398 + n - 1) : String.fromCharCode(65 + (n - 1) % 26);
885
- case "CIRCLED_LATIN_SMALL":
886
- return n <= 26 ? String.fromCodePoint(9424 + n - 1) : String.fromCharCode(97 + (n - 1) % 26);
887
- case "ROMAN_CAPITAL":
888
- return toRoman(n);
889
- case "ROMAN_SMALL":
890
- return toRoman(n).toLowerCase();
891
- default:
892
- return String(n);
1066
+ function validateGongmunOptions(opts) {
1067
+ if (opts.bodyPt !== void 0) assertFiniteRange("bodyPt", opts.bodyPt, 6, 40);
1068
+ if (opts.lineSpacing !== void 0) assertFiniteRange("lineSpacing", opts.lineSpacing, 50, 300);
1069
+ if (typeof opts.autoFit === "object" && opts.autoFit.minRatio !== void 0) {
1070
+ assertFiniteRange("autoFit.minRatio", opts.autoFit.minRatio, 50, 99);
1071
+ }
1072
+ if (opts.margins) {
1073
+ for (const side of ["top", "bottom", "left", "right"]) {
1074
+ const value = opts.margins[side];
1075
+ assertFiniteRange(`margins.${side}`, value, 0, 200);
1076
+ }
1077
+ if (opts.margins.left + opts.margins.right >= 210) {
1078
+ throw new KordocError("margins.left + margins.right must be less than 210");
1079
+ }
1080
+ if (opts.margins.top + opts.margins.bottom >= 297) {
1081
+ throw new KordocError("margins.top + margins.bottom must be less than 297");
1082
+ }
1083
+ }
1084
+ if (opts.sizes) {
1085
+ for (const [key, value] of Object.entries(opts.sizes)) {
1086
+ if (value !== void 0) assertFiniteRange(`sizes.${key}`, value, 6, 60);
1087
+ }
1088
+ }
1089
+ if (opts.approval && opts.approval.length > 6) {
1090
+ throw new KordocError("approval must contain at most 6 labels");
893
1091
  }
894
1092
  }
895
- function resolveParaHeading(paraEl, ctx) {
896
- const sm = ctx.styleMap;
897
- if (!sm) return null;
898
- const styleId = paraEl.getAttribute("styleIDRef");
899
- let styleHeading;
900
- if (styleId && styleId !== "0") {
901
- const st = sm.styles.get(styleId);
902
- const m = st && /^개요\s*([1-6])$/.exec(st.name);
903
- if (m) styleHeading = Math.min(parseInt(m[1], 10), 6);
1093
+ function incompatibleGongmunWarnings(opts) {
1094
+ const warns = [];
1095
+ const preset = normalizeGongmunPreset(opts.preset);
1096
+ 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`);
1097
+ 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`);
1098
+ 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`);
1099
+ 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`);
1100
+ if (preset === "press" && (opts.cover === true || typeof opts.cover === "object" || opts.toc === true)) {
1101
+ 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");
904
1102
  }
905
- const prId = paraEl.getAttribute("paraPrIDRef");
906
- if (!prId) return styleHeading ? { headingLevel: styleHeading } : null;
907
- const ref = sm.paraHeadings.get(prId);
908
- if (!ref) return styleHeading ? { headingLevel: styleHeading } : null;
909
- if (ref.type === "BULLET") {
910
- const char = sm.bullets.get(ref.idRef);
911
- return char ? { prefix: char } : null;
1103
+ if (opts.sizes && Object.keys(opts.sizes).length > 0 && !usesReportFonts(preset)) {
1104
+ 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`);
912
1105
  }
913
- const numId = ref.type === "OUTLINE" ? ctx.outlineNumId || "1" : ref.idRef;
914
- const level = Math.min(ref.level + 1, 10);
915
- const headingLevel = ref.type === "OUTLINE" ? Math.min(ref.level + 1, 6) : void 0;
916
- const numDef = sm.numberings.get(numId);
917
- if (!numDef) return headingLevel ? { headingLevel } : null;
918
- let counters = ctx.shared.numState.get(numId);
919
- if (!counters) {
920
- counters = new Array(11).fill(0);
921
- ctx.shared.numState.set(numId, counters);
1106
+ if (opts.suppressSingle && (opts.numbering ?? PRESET_DEFAULTS[preset].numbering) !== "standard") {
1107
+ 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`);
922
1108
  }
923
- const head = numDef.heads.get(level);
1109
+ return warns;
1110
+ }
1111
+ function resolveGongmun(opts) {
1112
+ validateGongmunOptions(opts);
1113
+ const preset = normalizeGongmunPreset(opts.preset);
1114
+ const d = PRESET_DEFAULTS[preset];
1115
+ const bodyPt = opts.bodyPt ?? d.bodyPt;
1116
+ const autoFitMinRatio = opts.autoFit === false ? null : typeof opts.autoFit === "object" ? Math.min(Math.max(opts.autoFit.minRatio ?? 90, 50), 99) : 90;
1117
+ const coverOn = opts.cover !== void 0 ? opts.cover !== false : preset === "gaejosik";
1118
+ const coverOpts = typeof opts.cover === "object" ? opts.cover : {};
1119
+ const gaejosik = preset === "gaejosik";
1120
+ const reportFamily = gaejosik || preset === "report" || preset === "plan" || preset === "notice" || preset === "press";
1121
+ return {
1122
+ preset,
1123
+ bodyFont: opts.bodyFont ?? "myeongjo",
1124
+ bodyHeight: Math.round(bodyPt * 100),
1125
+ lineSpacing: opts.lineSpacing ?? d.lineSpacing,
1126
+ numbering: opts.numbering ?? d.numbering,
1127
+ margins: opts.margins ?? (reportFamily ? GAEJOSIK_MARGINS : OFFICIAL_MARGINS),
1128
+ centerTitle: opts.centerTitle ?? true,
1129
+ autoFitMinRatio,
1130
+ // 보도자료는 머리박스가 1페이지 최상단을 차지하는 서식이라 표지·목차와 양립 불가 —
1131
+ // 켜면 머리박스가 표지에 얹히고 25pt 제목·부제가 유실된다 (docHead 프리셋 게이팅과 동일 관례)
1132
+ cover: coverOn && preset !== "press" ? { date: coverOpts.date ?? null, org: coverOpts.org ?? "" } : null,
1133
+ toc: preset !== "press" && (opts.toc ?? gaejosik),
1134
+ fonts: opts.fonts ?? {},
1135
+ sizes: opts.sizes ?? {},
1136
+ // 쪽번호 — 보고서 계열 관행(실측: 2_보고서 양식·추진계획·공고문 전부 하단 중앙)
1137
+ pageNumbers: opts.pageNumbers ?? (gaejosik || preset === "report" || preset === "plan"),
1138
+ // 머리말·꼬리말 — 실측: 보고서 계열 15mm(GT3·t2·춘천·브라더), 공고·보도 10mm,
1139
+ // 기안문 0(실결재 41/60건 h0/f0)
1140
+ headerFooter: usesReportFonts(preset) ? GAEJOSIK_HEADER_FOOTER : preset === "notice" || preset === "press" ? 2835 : 0,
1141
+ // "끝." — 기안문 규정(본문 끝 2타+"끝."). 그 외는 opt-in
1142
+ endMark: opts.endMark ?? preset === "official",
1143
+ approval: opts.approval && opts.approval.length > 0 ? opts.approval : null,
1144
+ // 본문 제목박스 — 실측(GT3·GT12): 목차 뒤 본문 시작에 제목 반복. 표지 켜진 개조식 기본
1145
+ bodyTitleBox: opts.bodyTitleBox ?? (gaejosik && coverOn),
1146
+ // h2 말머리 — 실측: 보고서 양식 □ 대항목(QA-2), 공고문 아라비아("1. 사업개요", 바이오헬스 실측)
1147
+ h2Marker: opts.h2Marker ?? (preset === "report" || preset === "plan" ? "box" : preset === "notice" ? "number" : "none"),
1148
+ // 2단계 부호 — 실결재 기안문·공고문 ㅇ 지배(60건 중 ㅇ134:○5), 보고서 양식 계열 ○
1149
+ bullet2: opts.bullet2 ?? (preset === "plan" || preset === "notice" || preset === "press" ? "\u3147" : "\u25CB"),
1150
+ // 단일 형제 부호 생략 — 규정이지만 부호 없는 계단이 실무 눈에 어색 (실무자 QA)
1151
+ suppressSingle: opts.suppressSingle ?? false,
1152
+ docHead: preset === "official" && opts.docHead ? opts.docHead : null,
1153
+ docFoot: preset === "official" && opts.docFoot ? opts.docFoot : null,
1154
+ reportInfo: opts.reportInfo?.trim() || null,
1155
+ noticeHead: preset === "notice" && opts.noticeHead ? opts.noticeHead : null,
1156
+ // 보도자료 머리박스는 프리셋 자체가 요구 — 옵션 미지정이어도 빈 객체
1157
+ press: preset === "press" ? opts.press ?? {} : null
1158
+ };
1159
+ }
1160
+ var HANGUL_INITIALS = [0, 2, 3, 5, 6, 7, 9, 11, 12, 14, 15, 16, 17, 18];
1161
+ var HANGUL_MEDIALS = [0, 4, 8, 13, 18, 20];
1162
+ function hangulOrdinal(n) {
1163
+ const cols = HANGUL_INITIALS.length;
1164
+ const vowel = HANGUL_MEDIALS[Math.min(Math.floor(n / cols), HANGUL_MEDIALS.length - 1)];
1165
+ const init = HANGUL_INITIALS[n % cols];
1166
+ return String.fromCodePoint(44032 + init * 588 + vowel * 28);
1167
+ }
1168
+ function circledNumber(n) {
1169
+ if (n < 20) return String.fromCodePoint(9312 + n);
1170
+ if (n < 35) return String.fromCodePoint(12881 + (n - 20));
1171
+ if (n < 50) return String.fromCodePoint(12977 + (n - 35));
1172
+ return `(${n + 1})`;
1173
+ }
1174
+ function circledHangul(n) {
1175
+ return n < 14 ? String.fromCodePoint(12910 + n) : hangulOrdinal(n);
1176
+ }
1177
+ var REPORT_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
1178
+ var ASTERISK_BULLETS = ["\u25A1", "\u25CB", "*", "\u318D"];
1179
+ function standardMarker(depth, n) {
1180
+ switch (depth) {
1181
+ case 0:
1182
+ return `${n + 1}.`;
1183
+ case 1:
1184
+ return `${hangulOrdinal(n)}.`;
1185
+ case 2:
1186
+ return `${n + 1})`;
1187
+ case 3:
1188
+ return `${hangulOrdinal(n)})`;
1189
+ case 4:
1190
+ return `(${n + 1})`;
1191
+ case 5:
1192
+ return `(${hangulOrdinal(n)})`;
1193
+ case 6:
1194
+ return circledNumber(n);
1195
+ case 7:
1196
+ return circledHangul(n);
1197
+ default:
1198
+ return circledHangul(n);
1199
+ }
1200
+ }
1201
+ function reportMarker(depth, bullet2 = "\u25CB", asteriskThird = false) {
1202
+ const bullets = asteriskThird ? ASTERISK_BULLETS : REPORT_BULLETS;
1203
+ const m = bullets[Math.min(depth, bullets.length - 1)];
1204
+ return depth === 1 ? bullet2 : m;
1205
+ }
1206
+ function markerWidth(marker, bodyHeight) {
1207
+ let em = SPACE_EM_FIXED;
1208
+ for (const c of marker) em += charWidthEm1000(c.codePointAt(0));
1209
+ return Math.round(em / 1e3 * bodyHeight);
1210
+ }
1211
+ function levelIndent(depth, bodyHeight, numbering, sizes = {}, bullet2 = "\u25CB", asteriskThird = false) {
1212
+ if (numbering === "gaejosik") return gaejosikLevelIndent(depth, bodyHeight, sizes, bullet2);
1213
+ const marker = numbering === "report" ? reportMarker(depth, bullet2, asteriskThird) : standardMarker(depth, 0);
1214
+ return { left: Math.round(depth * bodyHeight), indent: -markerWidth(marker, bodyHeight) };
1215
+ }
1216
+ function computeSuppression(depths) {
1217
+ const counts = /* @__PURE__ */ new Map();
1218
+ const keys = [];
1219
+ const path = [];
1220
+ for (const depth of depths) {
1221
+ path.length = depth + 1;
1222
+ path[depth] = (path[depth] ?? 0) + 1;
1223
+ const parentKey = path.slice(0, depth).join(".") + "|" + depth;
1224
+ keys.push(parentKey);
1225
+ counts.set(parentKey, (counts.get(parentKey) ?? 0) + 1);
1226
+ }
1227
+ return keys.map((k) => (counts.get(k) ?? 0) <= 1);
1228
+ }
1229
+ var GongmunNumberer = class {
1230
+ constructor(numbering, bullet2 = "\u25CB", asteriskThird = false) {
1231
+ this.numbering = numbering;
1232
+ this.bullet2 = bullet2;
1233
+ this.asteriskThird = asteriskThird;
1234
+ }
1235
+ counts = [];
1236
+ /** depth 항목 하나에 대한 마커. suppress=true면 빈 문자열(부호 없음) */
1237
+ next(depth, suppress) {
1238
+ this.counts.length = depth + 1;
1239
+ const n = this.counts[depth] ?? 0;
1240
+ this.counts[depth] = n + 1;
1241
+ if (suppress) return "";
1242
+ if (this.numbering === "gaejosik") return gaejosikMarker(depth, this.bullet2);
1243
+ return this.numbering === "report" ? reportMarker(depth, this.bullet2, this.asteriskThird) : standardMarker(depth, n);
1244
+ }
1245
+ reset() {
1246
+ this.counts = [];
1247
+ }
1248
+ };
1249
+
1250
+ // src/hwpx/para-heading.ts
1251
+ var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
1252
+ function toRoman(n) {
1253
+ if (n <= 0 || n >= 4e3) return String(n);
1254
+ const table2 = [
1255
+ [1e3, "M"],
1256
+ [900, "CM"],
1257
+ [500, "D"],
1258
+ [400, "CD"],
1259
+ [100, "C"],
1260
+ [90, "XC"],
1261
+ [50, "L"],
1262
+ [40, "XL"],
1263
+ [10, "X"],
1264
+ [9, "IX"],
1265
+ [5, "V"],
1266
+ [4, "IV"],
1267
+ [1, "I"]
1268
+ ];
1269
+ let out = "";
1270
+ for (const [v, s] of table2) {
1271
+ while (n >= v) {
1272
+ out += s;
1273
+ n -= v;
1274
+ }
1275
+ }
1276
+ return out;
1277
+ }
1278
+ function formatHeadNumber(n, numFormat) {
1279
+ if (n <= 0) n = 1;
1280
+ switch (numFormat) {
1281
+ case "DIGIT":
1282
+ return String(n);
1283
+ // 서수 시퀀스는 생성기(gongmun.ts)와 단일 소스 (v4.0.5) — 종전 mod-14 순환은
1284
+ // 15번째 형제를 다시 '가'로 만들어 한글 실렌더(가→하→거→너 단모음 연속)와 어긋났다
1285
+ case "CIRCLED_DIGIT":
1286
+ return n <= 50 ? circledNumber(n - 1) : `(${n})`;
1287
+ case "HANGUL_SYLLABLE":
1288
+ return hangulOrdinal(n - 1);
1289
+ case "CIRCLED_HANGUL_SYLLABLE":
1290
+ return n <= 14 ? circledHangul(n - 1) : hangulOrdinal(n - 1);
1291
+ case "HANGUL_JAMO":
1292
+ return HANGUL_JAMO_SEQ[(n - 1) % HANGUL_JAMO_SEQ.length];
1293
+ case "CIRCLED_HANGUL_JAMO":
1294
+ return n <= 14 ? String.fromCodePoint(12896 + n - 1) : HANGUL_JAMO_SEQ[(n - 1) % 14];
1295
+ case "LATIN_CAPITAL":
1296
+ return String.fromCharCode(65 + (n - 1) % 26);
1297
+ case "LATIN_SMALL":
1298
+ return String.fromCharCode(97 + (n - 1) % 26);
1299
+ case "CIRCLED_LATIN_CAPITAL":
1300
+ return n <= 26 ? String.fromCodePoint(9398 + n - 1) : String.fromCharCode(65 + (n - 1) % 26);
1301
+ case "CIRCLED_LATIN_SMALL":
1302
+ return n <= 26 ? String.fromCodePoint(9424 + n - 1) : String.fromCharCode(97 + (n - 1) % 26);
1303
+ case "ROMAN_CAPITAL":
1304
+ return toRoman(n);
1305
+ case "ROMAN_SMALL":
1306
+ return toRoman(n).toLowerCase();
1307
+ default:
1308
+ return String(n);
1309
+ }
1310
+ }
1311
+ function resolveParaHeading(paraEl, ctx) {
1312
+ const sm = ctx.styleMap;
1313
+ if (!sm) return null;
1314
+ const styleId = paraEl.getAttribute("styleIDRef");
1315
+ let styleHeading;
1316
+ if (styleId && styleId !== "0") {
1317
+ const st = sm.styles.get(styleId);
1318
+ const m = st && /^개요\s*([1-6])$/.exec(st.name);
1319
+ if (m) styleHeading = Math.min(parseInt(m[1], 10), 6);
1320
+ }
1321
+ const prId = paraEl.getAttribute("paraPrIDRef");
1322
+ if (!prId) return styleHeading ? { headingLevel: styleHeading } : null;
1323
+ const ref = sm.paraHeadings.get(prId);
1324
+ if (!ref) return styleHeading ? { headingLevel: styleHeading } : null;
1325
+ if (ref.type === "BULLET") {
1326
+ const char = sm.bullets.get(ref.idRef);
1327
+ return char ? { prefix: char } : null;
1328
+ }
1329
+ const numId = ref.type === "OUTLINE" ? ctx.outlineNumId || "1" : ref.idRef;
1330
+ const level = Math.min(ref.level + 1, 10);
1331
+ const headingLevel = ref.type === "OUTLINE" ? Math.min(ref.level + 1, 6) : void 0;
1332
+ const numDef = sm.numberings.get(numId);
1333
+ if (!numDef) return headingLevel ? { headingLevel } : null;
1334
+ let counters = ctx.shared.numState.get(numId);
1335
+ if (!counters) {
1336
+ counters = new Array(11).fill(0);
1337
+ ctx.shared.numState.set(numId, counters);
1338
+ }
1339
+ const head = numDef.heads.get(level);
924
1340
  counters[level] = counters[level] === 0 ? head?.start ?? 1 : counters[level] + 1;
925
1341
  for (let l = level + 1; l <= 10; l++) counters[l] = 0;
926
1342
  const fmtText = head ? head.text.trim() : `^${level}.`;
@@ -963,7 +1379,7 @@ function buildTableWithCellMeta(state) {
963
1379
  for (const row of state.rows) {
964
1380
  for (const src of row) {
965
1381
  flatIdx++;
966
- const needsBlocks = src.hasStructure && src.blocks && src.blocks.length > 0;
1382
+ const needsBlocks = !!src.blocks && src.blocks.length > 0 && (src.hasStructure || src.blocks.some((b) => b.spans));
967
1383
  if (!needsBlocks && !src.isHeader) continue;
968
1384
  let target;
969
1385
  const trimmed = src.text.trim();
@@ -1062,6 +1478,15 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1062
1478
  const localTag = tag.replace(/^[^:]+:/, "");
1063
1479
  switch (localTag) {
1064
1480
  case "tbl": {
1481
+ if (!tableCtx) {
1482
+ const chan = kordocTableChannel(el, ctx);
1483
+ if (chan) {
1484
+ if (chan.kind === "heading" && chan.text) {
1485
+ blocks.push({ type: "heading", level: chan.level, text: chan.text, pageNumber: ctx.sectionNum });
1486
+ }
1487
+ break;
1488
+ }
1489
+ }
1065
1490
  if (tableCtx) tableStack.push(tableCtx);
1066
1491
  const newTable = { rows: [], currentRow: [], cell: null };
1067
1492
  walkSection(el, blocks, newTable, tableStack, ctx, depth + 1);
@@ -1069,12 +1494,17 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1069
1494
  break;
1070
1495
  }
1071
1496
  // 표/도표 캡션 — IRTable.caption으로 보존 (v3.0, 기존 무음 드롭 수정)
1072
- case "caption":
1073
- if (tableCtx) {
1074
- const capText = collectSubListText(el, ctx);
1075
- if (capText) tableCtx.caption = (tableCtx.caption ? tableCtx.caption + "\n" : "") + capText;
1497
+ case "caption": {
1498
+ const capText = collectSubListText(el, ctx);
1499
+ if (capText) {
1500
+ if (tableCtx) {
1501
+ tableCtx.caption = (tableCtx.caption ? tableCtx.caption + "\n" : "") + capText;
1502
+ } else {
1503
+ blocks.push({ type: "paragraph", text: capText, pageNumber: ctx.sectionNum });
1504
+ }
1076
1505
  }
1077
1506
  break;
1507
+ }
1078
1508
  case "tr":
1079
1509
  if (tableCtx) {
1080
1510
  tableCtx.currentRow = [];
@@ -1116,8 +1546,8 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1116
1546
  const { text: rawText, href, footnote, style } = extractParagraphInfo(el, ctx.styleMap, ctx);
1117
1547
  let text = rawText;
1118
1548
  let headingLevel;
1549
+ const ph = resolveParaHeading(el, ctx);
1119
1550
  if (text) {
1120
- const ph = resolveParaHeading(el, ctx);
1121
1551
  if (ph?.prefix) text = ph.prefix + " " + text;
1122
1552
  headingLevel = ph?.headingLevel;
1123
1553
  }
@@ -1126,13 +1556,40 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1126
1556
  const cell2 = tableCtx.cell;
1127
1557
  if (footnote) text += ` (\uC8FC: ${footnote})`;
1128
1558
  cell2.text += (cell2.text ? "\n" : "") + text;
1129
- (cell2.blocks ??= []).push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
1559
+ const cellBlock = { type: "paragraph", text, pageNumber: ctx.sectionNum };
1560
+ if (ctx.shared.kordocLayout === "default" && !ph?.prefix) {
1561
+ const spans = extractRunSpans(el, ctx);
1562
+ if (spans && spans.map((s) => s.text).join("").replace(/[ \t]+/g, " ").trim() === text) {
1563
+ cellBlock.spans = spans;
1564
+ }
1565
+ }
1566
+ ;
1567
+ (cell2.blocks ??= []).push(cellBlock);
1130
1568
  } else if (!tableCtx) {
1569
+ if (/^─{10,}$/.test(text)) {
1570
+ blocks.push({ type: "separator", pageNumber: ctx.sectionNum });
1571
+ tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1572
+ break;
1573
+ }
1131
1574
  const block = { type: headingLevel ? "heading" : "paragraph", text, pageNumber: ctx.sectionNum };
1132
1575
  if (headingLevel) block.level = headingLevel;
1133
1576
  if (style) block.style = style;
1134
1577
  if (href) block.href = href;
1135
1578
  if (footnote) block.footnoteText = footnote;
1579
+ const ind = ctx.styleMap?.paraIndents.get(el.getAttribute("paraPrIDRef") ?? "");
1580
+ if (ind) {
1581
+ const eff = ind.left + Math.max(0, ind.intent);
1582
+ if (eff > 0) block.indent = eff;
1583
+ }
1584
+ if (!headingLevel && ctx.shared.kordocLayout === "default") {
1585
+ if (!ph?.prefix) {
1586
+ const spans = extractRunSpans(el, ctx);
1587
+ if (spans && spans.map((s) => s.text).join("").replace(/[ \t]+/g, " ").trim() === text) {
1588
+ block.spans = spans;
1589
+ }
1590
+ }
1591
+ if (el.getAttribute("paraPrIDRef") === KORDOC_PARA_QUOTE) block.quote = true;
1592
+ }
1136
1593
  blocks.push(block);
1137
1594
  } else {
1138
1595
  blocks.push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
@@ -1214,6 +1671,34 @@ function mergeBlocksIntoCell(cell2, sink) {
1214
1671
  }
1215
1672
  }
1216
1673
  }
1674
+ function kordocTableChannel(tblEl, ctx) {
1675
+ const found = findKordocMarkedCell(tblEl, 0);
1676
+ if (!found) return null;
1677
+ const m = found.name.match(/^__kordoc_(?:h([1-6])|(toc|skip))$/);
1678
+ if (!m) return null;
1679
+ if (m[2]) return { kind: "skip" };
1680
+ const text = collectSubListText(found.cell, ctx).trim();
1681
+ return { kind: "heading", level: Number(m[1]), text };
1682
+ }
1683
+ function findKordocMarkedCell(el, depth) {
1684
+ if (depth > 6) return null;
1685
+ const children = el.childNodes;
1686
+ if (!children) return null;
1687
+ for (let i = 0; i < children.length; i++) {
1688
+ const ch = children[i];
1689
+ if (ch.nodeType !== 1) continue;
1690
+ const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1691
+ if (tag === "tc") {
1692
+ const name = ch.getAttribute("name") || "";
1693
+ if (name.startsWith("__kordoc_")) return { cell: ch, name };
1694
+ continue;
1695
+ }
1696
+ if (tag === "tbl" && depth > 0) continue;
1697
+ const found = findKordocMarkedCell(ch, depth + 1);
1698
+ if (found) return found;
1699
+ }
1700
+ return null;
1701
+ }
1217
1702
  function collectSubListText(el, ctx, depth = 0) {
1218
1703
  if (depth > 10) return "";
1219
1704
  const parts = [];
@@ -1239,7 +1724,7 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth =
1239
1724
  if (depth > MAX_XML_DEPTH) return tableCtx;
1240
1725
  const children = node.childNodes;
1241
1726
  if (!children) return tableCtx;
1242
- const walkChildren = (parent, d) => {
1727
+ const walkChildren = (parent, d, inShape = false) => {
1243
1728
  if (d > MAX_XML_DEPTH) return;
1244
1729
  const kids2 = parent.childNodes;
1245
1730
  if (!kids2) return;
@@ -1249,10 +1734,26 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth =
1249
1734
  const tag = el.tagName || el.localName || "";
1250
1735
  const localTag = tag.replace(/^[^:]+:/, "");
1251
1736
  if (localTag === "tbl") {
1737
+ if (!tableCtx) {
1738
+ const chan = kordocTableChannel(el, ctx);
1739
+ if (chan) {
1740
+ if (chan.kind === "heading" && chan.text) {
1741
+ blocks.push({ type: "heading", level: chan.level, text: chan.text, pageNumber: ctx.sectionNum });
1742
+ }
1743
+ continue;
1744
+ }
1745
+ }
1252
1746
  if (tableCtx) tableStack.push(tableCtx);
1253
1747
  const newTable = { rows: [], currentRow: [], cell: null };
1254
1748
  walkSection(el, blocks, newTable, tableStack, ctx, d + 1);
1255
1749
  tableCtx = completeTable(newTable, tableStack, blocks, ctx);
1750
+ } else if (localTag === "caption" && !inShape) {
1751
+ const capText = collectSubListText(el, ctx);
1752
+ if (capText) {
1753
+ if (tableCtx?.cell) mergeBlocksIntoCell(tableCtx.cell, [{ type: "paragraph", text: capText, pageNumber: ctx.sectionNum }]);
1754
+ else if (tableCtx) tableCtx.caption = (tableCtx.caption ? tableCtx.caption + "\n" : "") + capText;
1755
+ else blocks.push({ type: "paragraph", text: capText, pageNumber: ctx.sectionNum });
1756
+ }
1256
1757
  } else if (localTag === "pic" || localTag === "shape" || localTag === "drawingObject") {
1257
1758
  if (tableCtx?.cell) {
1258
1759
  const sink = [];
@@ -1269,8 +1770,10 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth =
1269
1770
  } else {
1270
1771
  extractDrawTextBlocks(el, blocks, ctx);
1271
1772
  }
1272
- } else if (localTag === "r" || localTag === "run" || localTag === "ctrl" || localTag === "rect" || localTag === "ellipse" || localTag === "polygon" || localTag === "line" || localTag === "arc" || localTag === "curve" || localTag === "connectLine" || localTag === "container") {
1273
- walkChildren(el, d + 1);
1773
+ } else if (localTag === "r" || localTag === "run" || localTag === "ctrl") {
1774
+ walkChildren(el, d + 1, inShape);
1775
+ } else if (localTag === "rect" || localTag === "ellipse" || localTag === "polygon" || localTag === "line" || localTag === "arc" || localTag === "curve" || localTag === "connectLine" || localTag === "container") {
1776
+ walkChildren(el, d + 1, true);
1274
1777
  }
1275
1778
  }
1276
1779
  };
@@ -1406,6 +1909,9 @@ function extractParagraphInfo(para2, styleMap, ctx) {
1406
1909
  case "indexmark":
1407
1910
  case "colPr":
1408
1911
  break;
1912
+ // 캡션 — walkParagraphChildren의 caption 분기가 보존하므로 손실 경고 대상 아님
1913
+ case "caption":
1914
+ break;
1409
1915
  // 미지원 요소 — 텍스트를 가졌으면 무음 손실 대신 경고
1410
1916
  default: {
1411
1917
  if (ctx?.warnings && extractTextFromNode(k)) {
@@ -1570,6 +2076,55 @@ function extractParagraphInfo(para2, styleMap, ctx) {
1570
2076
  }
1571
2077
  return { text: cleanText, href, footnote, style };
1572
2078
  }
2079
+ var KORDOC_CHAR_CODE = "4";
2080
+ var KORDOC_PARA_QUOTE = "6";
2081
+ function extractRunSpans(para2, ctx) {
2082
+ const styleMap = ctx.styleMap;
2083
+ if (!styleMap) return null;
2084
+ const spans = [];
2085
+ let styled = false;
2086
+ const kids2 = para2.childNodes;
2087
+ if (!kids2) return null;
2088
+ for (let i = 0; i < kids2.length; i++) {
2089
+ const child = kids2[i];
2090
+ if (child.nodeType !== 1) continue;
2091
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
2092
+ if (tag === "linesegarray") continue;
2093
+ if (tag !== "run" && tag !== "r") return null;
2094
+ let text = "";
2095
+ const rkids = child.childNodes;
2096
+ for (let j = 0; j < (rkids?.length ?? 0); j++) {
2097
+ const rc = rkids[j];
2098
+ if (rc.nodeType !== 1) continue;
2099
+ const rtag = (rc.tagName || rc.localName || "").replace(/^[^:]+:/, "");
2100
+ if (rtag === "t") {
2101
+ const tkids = rc.childNodes;
2102
+ for (let k = 0; k < (tkids?.length ?? 0); k++) {
2103
+ const tk = tkids[k];
2104
+ if (tk.nodeType === 3) text += tk.textContent || "";
2105
+ else if (tk.nodeType === 1) return null;
2106
+ }
2107
+ } else if (rtag === "secPr" || rtag === "colPr") {
2108
+ } else if (rtag === "ctrl") {
2109
+ if (extractTextFromNode(rc)) return null;
2110
+ } else {
2111
+ return null;
2112
+ }
2113
+ }
2114
+ if (!text) continue;
2115
+ const prId = child.getAttribute("charPrIDRef") ?? "";
2116
+ const cp = styleMap.charProperties.get(prId);
2117
+ const span = { text };
2118
+ if (prId === KORDOC_CHAR_CODE) span.code = true;
2119
+ else {
2120
+ if (cp?.bold) span.bold = true;
2121
+ if (cp?.italic) span.italic = true;
2122
+ }
2123
+ if (span.bold || span.italic || span.code) styled = true;
2124
+ spans.push(span);
2125
+ }
2126
+ return styled && spans.length > 0 ? spans : null;
2127
+ }
1573
2128
 
1574
2129
  // src/hwpx/images.ts
1575
2130
  function imageExtToMime(ext) {
@@ -1759,9 +2314,20 @@ function extractFromBrokenZip(buffer) {
1759
2314
  const markdown = blocksToMarkdown(blocks);
1760
2315
  return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
1761
2316
  }
1762
- async function resolveSectionPaths(zip) {
1763
- const manifestPaths = ["Contents/content.hpf", "content.hpf"];
1764
- for (const mp of manifestPaths) {
2317
+ async function readKordocLayout(zip) {
2318
+ const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
2319
+ if (!file) return null;
2320
+ try {
2321
+ const xml = await file.async("text");
2322
+ if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
2323
+ return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
2324
+ } catch {
2325
+ return null;
2326
+ }
2327
+ }
2328
+ async function resolveSectionPaths(zip) {
2329
+ const manifestPaths = ["Contents/content.hpf", "content.hpf"];
2330
+ for (const mp of manifestPaths) {
1765
2331
  const mpLower = mp.toLowerCase();
1766
2332
  const file = zip.file(mp) || Object.values(zip.files).find((f) => f.name.toLowerCase() === mpLower) || null;
1767
2333
  if (!file) continue;
@@ -1889,6 +2455,7 @@ async function parseHwpxDocument(buffer, options) {
1889
2455
  const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
1890
2456
  const blocks = [];
1891
2457
  const shared = createSectionShared();
2458
+ shared.kordocLayout = await readKordocLayout(zip);
1892
2459
  let parsedSections = 0;
1893
2460
  for (let si = 0; si < sectionPaths.length; si++) {
1894
2461
  if (pageFilter && !pageFilter.has(si + 1)) continue;
@@ -19591,452 +20158,160 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
19591
20158
  row: rowIdx,
19592
20159
  col: colIdx,
19593
20160
  key: matchKey
19594
- });
19595
- } else {
19596
- const paras = valueCell.paragraphs;
19597
- if (paras.length === 0) continue;
19598
- const newValue = cursor.consume(matchKey);
19599
- if (newValue === void 0) continue;
19600
- const l0 = led(paras[0]);
19601
- l0.fullText = newValue;
19602
- l0.ranges = [];
19603
- l0.filledIdx.push(filled.length);
19604
- l0.matchKeys.push(matchKey);
19605
- for (let k = 1; k < paras.length; k++) {
19606
- const lk = led(paras[k]);
19607
- lk.fullText = "";
19608
- lk.ranges = [];
19609
- }
19610
- matchedLabels.add(matchKey);
19611
- filled.push({
19612
- label: labelText.trim().replace(/[::]\s*$/, ""),
19613
- value: newValue,
19614
- row: rowIdx,
19615
- col: colIdx,
19616
- key: matchKey
19617
- });
19618
- }
19619
- }
19620
- }
19621
- if (table2.rows.length >= 2) {
19622
- const headerCells = table2.rows[0];
19623
- const allLabels = headerCells.length > 0 && headerCells.every((cell2) => {
19624
- const t = cellLabelText(cell2).trim();
19625
- return t.length > 0 && t.length <= 20 && isLabelCell(t);
19626
- });
19627
- if (allLabels) {
19628
- for (let rowIdx = 1; rowIdx < table2.rows.length; rowIdx++) {
19629
- const dataCells = table2.rows[rowIdx];
19630
- for (let colIdx = 0; colIdx < Math.min(headerCells.length, dataCells.length); colIdx++) {
19631
- const headerLabel = normalizeLabel(cellLabelText(headerCells[colIdx]));
19632
- if (blockedLabels?.has(headerLabel)) continue;
19633
- const matchKey = findMatchingKey(headerLabel, cursor);
19634
- if (matchKey === void 0) continue;
19635
- if (!cursor.isArray(matchKey) && matchedLabels.has(matchKey)) continue;
19636
- const newValue = cursor.consume(matchKey);
19637
- if (newValue === void 0) continue;
19638
- const paras = dataCells[colIdx].paragraphs;
19639
- if (paras.length === 0) continue;
19640
- const l0 = led(paras[0]);
19641
- l0.fullText = newValue;
19642
- l0.ranges = [];
19643
- l0.filledIdx.push(filled.length);
19644
- l0.matchKeys.push(matchKey);
19645
- for (let k = 1; k < paras.length; k++) {
19646
- const lk = led(paras[k]);
19647
- lk.fullText = "";
19648
- lk.ranges = [];
19649
- }
19650
- matchedLabels.add(matchKey);
19651
- filled.push({
19652
- label: cellLabelText(headerCells[colIdx]).trim(),
19653
- value: newValue,
19654
- row: rowIdx,
19655
- col: colIdx,
19656
- key: matchKey
19657
- });
19658
- }
19659
- }
19660
- }
19661
- }
19662
- }
19663
- for (const para2 of [...scan.bodyParagraphs, ...scan.excludedParagraphs]) {
19664
- const existing = ledger.get(para2);
19665
- if (existing?.fullText !== void 0) continue;
19666
- const text = matchText(para2);
19667
- for (const seg of scanInlineSegments(text)) {
19668
- const nlabel = normalizeLabel(seg.label);
19669
- if (blockedLabels?.has(nlabel)) continue;
19670
- const matchKey = findMatchingKey(nlabel, cursor);
19671
- if (matchKey === void 0) continue;
19672
- const newValue = cursor.consume(matchKey);
19673
- if (newValue === void 0) continue;
19674
- const replacement = seg.valueStart === seg.valueEnd ? padInsertion(text, seg.valueStart, newValue) : newValue;
19675
- const l = led(para2);
19676
- l.ranges.push({ start: seg.valueStart, end: seg.valueEnd, replacement });
19677
- matchedLabels.add(matchKey);
19678
- l.filledIdx.push(filled.length);
19679
- l.matchKeys.push(matchKey);
19680
- filled.push({ label: seg.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
19681
- }
19682
- }
19683
- const splices = [];
19684
- for (const [para2, l] of ledger) {
19685
- let paraSplices = null;
19686
- if (l.fullText !== void 0) {
19687
- paraSplices = buildParagraphSplices(para2, l.fullText, xml);
19688
- } else if (l.ranges.length > 0) {
19689
- const sorted = [...l.ranges].sort((a, b) => a.start - b.start || a.end - b.end);
19690
- const merged = [];
19691
- for (const r of sorted) {
19692
- const prev = merged[merged.length - 1];
19693
- if (prev && r.start < prev.end) continue;
19694
- merged.push(r);
19695
- }
19696
- if (paraTText(para2, xml) !== null) {
19697
- const precise = [];
19698
- let ok = true;
19699
- for (const r of merged) {
19700
- const sp = buildRangeSplices(para2, xml, r.start, r.end, r.replacement);
19701
- if (!sp) {
19702
- ok = false;
19703
- break;
19704
- }
19705
- precise.push(...sp);
19706
- }
19707
- paraSplices = ok ? precise : null;
19708
- } else if (paraTextPureT(para2, xml)) {
19709
- let text = para2.text;
19710
- for (let k = merged.length - 1; k >= 0; k--) {
19711
- const r = merged[k];
19712
- text = text.slice(0, r.start) + r.replacement + text.slice(r.end);
19713
- }
19714
- paraSplices = buildParagraphSplices(para2, text, xml);
19715
- } else {
19716
- paraSplices = null;
19717
- }
19718
- }
19719
- if (paraSplices === null) {
19720
- for (const idx of l.filledIdx) filled[idx] = null;
19721
- for (const k of l.matchKeys) failedKeys.add(k);
19722
- continue;
19723
- }
19724
- for (const k of l.matchKeys) succeededKeys.add(k);
19725
- splices.push(...paraSplices);
19726
- }
19727
- if (splices.length > 0) {
19728
- splices.push(...allLinesegRemovalSplices(xml));
19729
- replacements.set(sectionPaths[si], encoder.encode(applySplices(xml, splices)));
19730
- }
19731
- }
19732
- for (const k of failedKeys) {
19733
- if (!succeededKeys.has(k)) matchedLabels.delete(k);
19734
- }
19735
- const cleanFilled = filled.filter((f) => f !== null);
19736
- const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
19737
- const out = replacements.size > 0 ? patchZipEntries(u8, replacements) : new Uint8Array(u8);
19738
- return {
19739
- buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
19740
- filled: cleanFilled,
19741
- unmatched
19742
- };
19743
- }
19744
-
19745
- // src/hwpx/generator.ts
19746
- import JSZip6 from "jszip";
19747
-
19748
- // src/hwpx/gaejosik.ts
19749
- var GAEJOSIK_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
19750
- function gaejosikMarker(depth, bullet2 = "\u25CB") {
19751
- if (depth === 1) return bullet2;
19752
- return GAEJOSIK_BULLETS[Math.min(depth, GAEJOSIK_BULLETS.length - 1)];
19753
- }
19754
- function gaejosikSizes(bodyHeight, ov = {}) {
19755
- const s = bodyHeight / 1500;
19756
- const r = (v, pt) => pt ? Math.round(pt * 100) : Math.round(v * s);
19757
- return {
19758
- /** □ 대항목 — HY헤드라인M 16pt */
19759
- dae: r(1600, ov.dae),
19760
- /** ※ 참고 — 한양중고딕 13pt */
19761
- cham: r(1300, ov.cham),
19762
- /** 장 헤더(로마숫자·제목) 17pt */
19763
- chapter: r(1700, ov.chapter),
19764
- /** 표지 제목 30pt */
19765
- coverTitle: r(3e3, ov.coverTitle),
19766
- /** 표지 날짜·기관명 25pt */
19767
- coverSub: r(2500, ov.coverSub),
19768
- /** 목차 라벨 "목 차" 28pt */
19769
- tocLabel: r(2800, ov.tocLabel),
19770
- /** 목차 로마숫자 21pt(한양신명조) */
19771
- tocRoman: r(2100, ov.tocRoman),
19772
- /** 목차 항목 18pt */
19773
- tocItem: r(1800, ov.tocItem),
19774
- /** 표 셀 — 맑은 고딕 12pt (실측: 정부 양식 표) */
19775
- table: r(1200, ov.table),
19776
- /** 표지 장식 바 셀 빈 문단 — 6pt (실측: 바 높이 818 안에 줄높이 수납) */
19777
- bar: r(600),
19778
- /** 본문 첫 페이지 제목 박스 — HY헤드라인M 22pt (실측: GT3 표④) */
19779
- bodyTitle: r(2200, ov.bodyTitle)
19780
- };
19781
- }
19782
- function gaejosikSpaceBefore(depth, bodyHeight) {
19783
- const s = bodyHeight / 1500;
19784
- const ui = [3e3, 2e3, 1200, 600][Math.min(depth, 3)];
19785
- return Math.round(ui * s);
19786
- }
19787
- function gaejosikLevelIndent(depth, bodyHeight, sizes = {}, bullet2 = "\u25CB") {
19788
- const lefts = [0, 1, 1.5, 2];
19789
- const left = depth <= 3 ? Math.round(lefts[depth] * bodyHeight) : Math.round((2 + (depth - 3) * 0.5) * bodyHeight);
19790
- const h = depth === 0 ? gaejosikSizes(bodyHeight, sizes).dae : bodyHeight;
19791
- return { left, indent: -markerWidth(gaejosikMarker(depth, bullet2), h) };
19792
- }
19793
- function gaejosikChamIndent(bodyHeight, sizes = {}) {
19794
- const cham = gaejosikSizes(bodyHeight, sizes).cham;
19795
- return { left: Math.round(2.5 * cham), indent: -markerWidth("\u203B", cham) };
19796
- }
19797
- function gaejosikTocItemIndent(bodyHeight, sizes = {}) {
19798
- const sz = gaejosikSizes(bodyHeight, sizes);
19799
- const hang = markerWidth("\u2167.", sz.tocRoman);
19800
- return { left: 2e3 + hang, indent: -hang };
19801
- }
19802
- function formatGaejosikDate(d) {
19803
- return `${d.getFullYear()}. ${d.getMonth() + 1}. ${d.getDate()}.`;
19804
- }
19805
- function stripChapterNumber(title) {
19806
- return title.replace(/^\s*(?:\d+|[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ]+)\s*[.)]\s*/u, "").trim();
19807
- }
19808
- var GAEJOSIK_COLORS = {
19809
- /** 장헤더 로마숫자 셀·표지 진한 바 음영 */
19810
- primary: "#193AAA",
19811
- /** 장헤더 셀 테두리 */
19812
- border: "#006699",
19813
- /** 표지 연한 바 */
19814
- accent: "#A0B4E6",
19815
- /** 장헤더 제목 셀 음영 */
19816
- titleFill: "#F2F2F2",
19817
- /** 장헤더 제목 셀 상하선 */
19818
- titleLine: "#A6A6A6",
19819
- /** 목차 박스 테두리 */
19820
- tocBorder: "#514BAC",
19821
- /** 목차 배너 라벤더 스트라이프 (실측: GT3 표②) */
19822
- tocStripe: "#E0E5FA"
19823
- };
19824
- var CHAPTER_GEOM = { numW: 3327, gapW: 848, titleW: 43513, rowH: 2832 };
19825
- var COVER_GEOM = {
19826
- totalW: 48180,
19827
- topDarkW: 38219,
19828
- topLightW: 9961,
19829
- barH: 818,
19830
- titleH: 11060,
19831
- botDarkW: 10466,
19832
- botLightW: 37714,
19833
- botBarH: 812
19834
- };
19835
- var TOC_GEOM = { boxW: 46492, boxH: 57840 };
19836
- function chapterGeom(bodyHeight, ov = {}) {
19837
- const s = gaejosikSizes(bodyHeight, ov).chapter / 1700;
19838
- return { ...CHAPTER_GEOM, rowH: Math.round(CHAPTER_GEOM.rowH * s) };
19839
- }
19840
- function coverGeom(bodyHeight, ov = {}) {
19841
- const s = gaejosikSizes(bodyHeight, ov).coverTitle / 3e3;
19842
- return { ...COVER_GEOM, titleH: Math.round(COVER_GEOM.titleH * s) };
19843
- }
19844
- var TOC_BANNER_GEOM = {
19845
- colWs: [565, 565, 1414, 13191, 1414, 565, 565],
19846
- h: 3931
19847
- };
19848
- function tocBannerGeom(bodyHeight, ov = {}) {
19849
- const s = gaejosikSizes(bodyHeight, ov).tocLabel / 2800;
19850
- return { colWs: TOC_BANNER_GEOM.colWs, h: Math.round(TOC_BANNER_GEOM.h * s) };
19851
- }
19852
- var BODY_TITLE_GEOM = { barH: 600, titleH: 3566 };
19853
- function bodyTitleGeom(bodyHeight, ov = {}) {
19854
- const s = gaejosikSizes(bodyHeight, ov).bodyTitle / 2200;
19855
- return { barH: BODY_TITLE_GEOM.barH, titleH: Math.round(BODY_TITLE_GEOM.titleH * s) };
19856
- }
19857
-
19858
- // src/hwpx/gongmun.ts
19859
- var OFFICIAL_MARGINS = { top: 20, bottom: 15, left: 20, right: 15 };
19860
- var GAEJOSIK_MARGINS = { top: 15, bottom: 15, left: 20, right: 20 };
19861
- var GAEJOSIK_HEADER_FOOTER = 4251;
19862
- var PRESET_DEFAULTS = {
19863
- official: { bodyPt: 15, lineSpacing: 160, numbering: "standard" },
19864
- report: { bodyPt: 15, lineSpacing: 160, numbering: "report" },
19865
- plan: { bodyPt: 15, lineSpacing: 160, numbering: "standard" },
19866
- notice: { bodyPt: 15, lineSpacing: 160, numbering: "standard" },
19867
- minutes: { bodyPt: 14, lineSpacing: 130, numbering: "standard" },
19868
- gaejosik: { bodyPt: 15, lineSpacing: 160, numbering: "gaejosik" },
19869
- // 보도자료 — 실측(국토부 실물): 본문 바탕 14pt 160%, □→ㅇ→*(각주) 부호
19870
- press: { bodyPt: 14, lineSpacing: 160, numbering: "report" }
19871
- };
19872
- var PRESET_ALIAS = {
19873
- official: "official",
19874
- \uAE30\uC548\uBB38: "official",
19875
- \uC2DC\uD589\uBB38: "official",
19876
- \uACF5\uBB38: "official",
19877
- \uACF5\uBB38\uC11C: "official",
19878
- report: "report",
19879
- \uBCF4\uACE0\uC11C: "report",
19880
- plan: "plan",
19881
- \uACC4\uD68D\uC11C: "plan",
19882
- \uACC4\uD68D: "plan",
19883
- notice: "notice",
19884
- \uD1B5\uC9C0: "notice",
19885
- \uC54C\uB9BC: "notice",
19886
- \uC548\uB0B4: "notice",
19887
- minutes: "minutes",
19888
- \uD68C\uC758\uB85D: "minutes",
19889
- gaejosik: "gaejosik",
19890
- \uAC1C\uC870\uC2DD: "gaejosik",
19891
- \uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
19892
- \uC815\uBD80\uBCF4\uACE0\uC11C: "gaejosik",
19893
- \uC815\uBD80\uD45C\uC900\uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
19894
- press: "press",
19895
- \uBCF4\uB3C4\uC790\uB8CC: "press"
19896
- };
19897
- function normalizeGongmunPreset(preset) {
19898
- if (!preset) return "official";
19899
- return PRESET_ALIAS[preset.trim()] ?? "official";
19900
- }
19901
- function usesReportFonts(preset) {
19902
- return preset === "gaejosik" || preset === "report" || preset === "plan";
19903
- }
19904
- function resolveGongmun(opts) {
19905
- const preset = normalizeGongmunPreset(opts.preset);
19906
- const d = PRESET_DEFAULTS[preset];
19907
- const bodyPt = opts.bodyPt ?? d.bodyPt;
19908
- const autoFitMinRatio = opts.autoFit === false ? null : typeof opts.autoFit === "object" ? Math.min(Math.max(opts.autoFit.minRatio ?? 90, 50), 99) : 90;
19909
- const coverOn = opts.cover !== void 0 ? opts.cover !== false : preset === "gaejosik";
19910
- const coverOpts = typeof opts.cover === "object" ? opts.cover : {};
19911
- const gaejosik = preset === "gaejosik";
19912
- const reportFamily = gaejosik || preset === "report" || preset === "plan" || preset === "notice" || preset === "press";
19913
- return {
19914
- preset,
19915
- bodyFont: opts.bodyFont ?? "myeongjo",
19916
- bodyHeight: Math.round(bodyPt * 100),
19917
- lineSpacing: opts.lineSpacing ?? d.lineSpacing,
19918
- numbering: opts.numbering ?? d.numbering,
19919
- margins: opts.margins ?? (reportFamily ? GAEJOSIK_MARGINS : OFFICIAL_MARGINS),
19920
- centerTitle: opts.centerTitle ?? true,
19921
- autoFitMinRatio,
19922
- cover: coverOn ? { date: coverOpts.date ?? null, org: coverOpts.org ?? "" } : null,
19923
- toc: opts.toc ?? gaejosik,
19924
- fonts: opts.fonts ?? {},
19925
- sizes: opts.sizes ?? {},
19926
- // 쪽번호 — 보고서 계열 관행(실측: 2_보고서 양식·추진계획·공고문 전부 하단 중앙)
19927
- pageNumbers: opts.pageNumbers ?? (gaejosik || preset === "report" || preset === "plan"),
19928
- // 머리말·꼬리말 — 실측: 보고서 계열 15mm(GT3·t2·춘천·브라더), 공고·보도 10mm,
19929
- // 기안문 0(실결재 41/60건 h0/f0)
19930
- headerFooter: usesReportFonts(preset) ? GAEJOSIK_HEADER_FOOTER : preset === "notice" || preset === "press" ? 2835 : 0,
19931
- // "끝." — 기안문 규정(본문 끝 2타+"끝."). 그 외는 opt-in
19932
- endMark: opts.endMark ?? preset === "official",
19933
- approval: opts.approval && opts.approval.length > 0 ? opts.approval : null,
19934
- // 본문 제목박스 — 실측(GT3·GT12): 목차 뒤 본문 시작에 제목 반복. 표지 켜진 개조식 기본
19935
- bodyTitleBox: opts.bodyTitleBox ?? (gaejosik && coverOn),
19936
- // h2 말머리 — 실측: 보고서 양식 □ 대항목(QA-2), 공고문 아라비아("1. 사업개요", 바이오헬스 실측)
19937
- h2Marker: opts.h2Marker ?? (preset === "report" || preset === "plan" ? "box" : preset === "notice" ? "number" : "none"),
19938
- // 2단계 부호 — 실결재 기안문·공고문 ㅇ 지배(60건 중 ㅇ134:○5), 보고서 양식 계열 ○
19939
- bullet2: opts.bullet2 ?? (preset === "notice" || preset === "press" ? "\u3147" : "\u25CB"),
19940
- // 단일 형제 부호 생략 — 규정이지만 부호 없는 계단이 실무 눈에 어색 (실무자 QA)
19941
- suppressSingle: opts.suppressSingle ?? false,
19942
- docHead: preset === "official" && opts.docHead ? opts.docHead : null,
19943
- docFoot: preset === "official" && opts.docFoot ? opts.docFoot : null,
19944
- reportInfo: opts.reportInfo?.trim() || null,
19945
- noticeHead: preset === "notice" && opts.noticeHead ? opts.noticeHead : null,
19946
- // 보도자료 머리박스는 프리셋 자체가 요구 — 옵션 미지정이어도 빈 객체
19947
- press: preset === "press" ? opts.press ?? {} : null
19948
- };
19949
- }
19950
- var HANGUL_INITIALS = [0, 2, 3, 5, 6, 7, 9, 11, 12, 14, 15, 16, 17, 18];
19951
- var HANGUL_MEDIALS = [0, 4, 8, 13, 18, 20];
19952
- function hangulOrdinal(n) {
19953
- const cols = HANGUL_INITIALS.length;
19954
- const vowel = HANGUL_MEDIALS[Math.min(Math.floor(n / cols), HANGUL_MEDIALS.length - 1)];
19955
- const init = HANGUL_INITIALS[n % cols];
19956
- return String.fromCodePoint(44032 + init * 588 + vowel * 28);
19957
- }
19958
- function circledNumber(n) {
19959
- return String.fromCodePoint(9312 + n % 20);
19960
- }
19961
- function circledHangul(n) {
19962
- return String.fromCodePoint(12910 + n % 14);
19963
- }
19964
- var REPORT_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
19965
- var PRESS_BULLETS = ["\u25A1", "\u3147", "*", "\u318D"];
19966
- function standardMarker(depth, n) {
19967
- switch (depth) {
19968
- case 0:
19969
- return `${n + 1}.`;
19970
- case 1:
19971
- return `${hangulOrdinal(n)}.`;
19972
- case 2:
19973
- return `${n + 1})`;
19974
- case 3:
19975
- return `${hangulOrdinal(n)})`;
19976
- case 4:
19977
- return `(${n + 1})`;
19978
- case 5:
19979
- return `(${hangulOrdinal(n)})`;
19980
- case 6:
19981
- return circledNumber(n);
19982
- case 7:
19983
- return circledHangul(n);
19984
- default:
19985
- return circledHangul(n);
19986
- }
19987
- }
19988
- function reportMarker(depth, bullet2 = "\u25CB", press = false) {
19989
- const bullets = press ? PRESS_BULLETS : REPORT_BULLETS;
19990
- const m = bullets[Math.min(depth, bullets.length - 1)];
19991
- return depth === 1 && !press ? bullet2 : m;
19992
- }
19993
- function markerWidth(marker, bodyHeight) {
19994
- let em = SPACE_EM_FIXED;
19995
- for (const c of marker) em += charWidthEm1000(c.codePointAt(0));
19996
- return Math.round(em / 1e3 * bodyHeight);
19997
- }
19998
- function levelIndent(depth, bodyHeight, numbering, sizes = {}, bullet2 = "\u25CB", press = false) {
19999
- if (numbering === "gaejosik") return gaejosikLevelIndent(depth, bodyHeight, sizes, bullet2);
20000
- const marker = numbering === "report" ? reportMarker(depth, bullet2, press) : standardMarker(depth, 0);
20001
- return { left: Math.round(depth * bodyHeight), indent: -markerWidth(marker, bodyHeight) };
20002
- }
20003
- function computeSuppression(depths) {
20004
- const counts = /* @__PURE__ */ new Map();
20005
- const keys = [];
20006
- const path = [];
20007
- for (const depth of depths) {
20008
- path.length = depth + 1;
20009
- path[depth] = (path[depth] ?? 0) + 1;
20010
- const parentKey = path.slice(0, depth).join(".") + "|" + depth;
20011
- keys.push(parentKey);
20012
- counts.set(parentKey, (counts.get(parentKey) ?? 0) + 1);
20013
- }
20014
- return keys.map((k) => (counts.get(k) ?? 0) <= 1);
20015
- }
20016
- var GongmunNumberer = class {
20017
- constructor(numbering, bullet2 = "\u25CB", press = false) {
20018
- this.numbering = numbering;
20019
- this.bullet2 = bullet2;
20020
- this.press = press;
20021
- }
20022
- counts = [];
20023
- /** depth 항목 하나에 대한 마커. suppress=true면 빈 문자열(부호 없음) */
20024
- next(depth, suppress) {
20025
- this.counts.length = depth + 1;
20026
- const n = this.counts[depth] ?? 0;
20027
- this.counts[depth] = n + 1;
20028
- if (suppress) return "";
20029
- if (this.numbering === "gaejosik") return gaejosikMarker(depth, this.bullet2);
20030
- return this.numbering === "report" ? reportMarker(depth, this.bullet2, this.press) : standardMarker(depth, n);
20161
+ });
20162
+ } else {
20163
+ const paras = valueCell.paragraphs;
20164
+ if (paras.length === 0) continue;
20165
+ const newValue = cursor.consume(matchKey);
20166
+ if (newValue === void 0) continue;
20167
+ const l0 = led(paras[0]);
20168
+ l0.fullText = newValue;
20169
+ l0.ranges = [];
20170
+ l0.filledIdx.push(filled.length);
20171
+ l0.matchKeys.push(matchKey);
20172
+ for (let k = 1; k < paras.length; k++) {
20173
+ const lk = led(paras[k]);
20174
+ lk.fullText = "";
20175
+ lk.ranges = [];
20176
+ }
20177
+ matchedLabels.add(matchKey);
20178
+ filled.push({
20179
+ label: labelText.trim().replace(/[::]\s*$/, ""),
20180
+ value: newValue,
20181
+ row: rowIdx,
20182
+ col: colIdx,
20183
+ key: matchKey
20184
+ });
20185
+ }
20186
+ }
20187
+ }
20188
+ if (table2.rows.length >= 2) {
20189
+ const headerCells = table2.rows[0];
20190
+ const allLabels = headerCells.length > 0 && headerCells.every((cell2) => {
20191
+ const t = cellLabelText(cell2).trim();
20192
+ return t.length > 0 && t.length <= 20 && isLabelCell(t);
20193
+ });
20194
+ if (allLabels) {
20195
+ for (let rowIdx = 1; rowIdx < table2.rows.length; rowIdx++) {
20196
+ const dataCells = table2.rows[rowIdx];
20197
+ for (let colIdx = 0; colIdx < Math.min(headerCells.length, dataCells.length); colIdx++) {
20198
+ const headerLabel = normalizeLabel(cellLabelText(headerCells[colIdx]));
20199
+ if (blockedLabels?.has(headerLabel)) continue;
20200
+ const matchKey = findMatchingKey(headerLabel, cursor);
20201
+ if (matchKey === void 0) continue;
20202
+ if (!cursor.isArray(matchKey) && matchedLabels.has(matchKey)) continue;
20203
+ const newValue = cursor.consume(matchKey);
20204
+ if (newValue === void 0) continue;
20205
+ const paras = dataCells[colIdx].paragraphs;
20206
+ if (paras.length === 0) continue;
20207
+ const l0 = led(paras[0]);
20208
+ l0.fullText = newValue;
20209
+ l0.ranges = [];
20210
+ l0.filledIdx.push(filled.length);
20211
+ l0.matchKeys.push(matchKey);
20212
+ for (let k = 1; k < paras.length; k++) {
20213
+ const lk = led(paras[k]);
20214
+ lk.fullText = "";
20215
+ lk.ranges = [];
20216
+ }
20217
+ matchedLabels.add(matchKey);
20218
+ filled.push({
20219
+ label: cellLabelText(headerCells[colIdx]).trim(),
20220
+ value: newValue,
20221
+ row: rowIdx,
20222
+ col: colIdx,
20223
+ key: matchKey
20224
+ });
20225
+ }
20226
+ }
20227
+ }
20228
+ }
20229
+ }
20230
+ for (const para2 of [...scan.bodyParagraphs, ...scan.excludedParagraphs]) {
20231
+ const existing = ledger.get(para2);
20232
+ if (existing?.fullText !== void 0) continue;
20233
+ const text = matchText(para2);
20234
+ for (const seg of scanInlineSegments(text)) {
20235
+ const nlabel = normalizeLabel(seg.label);
20236
+ if (blockedLabels?.has(nlabel)) continue;
20237
+ const matchKey = findMatchingKey(nlabel, cursor);
20238
+ if (matchKey === void 0) continue;
20239
+ const newValue = cursor.consume(matchKey);
20240
+ if (newValue === void 0) continue;
20241
+ const replacement = seg.valueStart === seg.valueEnd ? padInsertion(text, seg.valueStart, newValue) : newValue;
20242
+ const l = led(para2);
20243
+ l.ranges.push({ start: seg.valueStart, end: seg.valueEnd, replacement });
20244
+ matchedLabels.add(matchKey);
20245
+ l.filledIdx.push(filled.length);
20246
+ l.matchKeys.push(matchKey);
20247
+ filled.push({ label: seg.label.trim(), value: newValue, row: -1, col: -1, key: matchKey });
20248
+ }
20249
+ }
20250
+ const splices = [];
20251
+ for (const [para2, l] of ledger) {
20252
+ let paraSplices = null;
20253
+ if (l.fullText !== void 0) {
20254
+ paraSplices = buildParagraphSplices(para2, l.fullText, xml);
20255
+ } else if (l.ranges.length > 0) {
20256
+ const sorted = [...l.ranges].sort((a, b) => a.start - b.start || a.end - b.end);
20257
+ const merged = [];
20258
+ for (const r of sorted) {
20259
+ const prev = merged[merged.length - 1];
20260
+ if (prev && r.start < prev.end) continue;
20261
+ merged.push(r);
20262
+ }
20263
+ if (paraTText(para2, xml) !== null) {
20264
+ const precise = [];
20265
+ let ok = true;
20266
+ for (const r of merged) {
20267
+ const sp = buildRangeSplices(para2, xml, r.start, r.end, r.replacement);
20268
+ if (!sp) {
20269
+ ok = false;
20270
+ break;
20271
+ }
20272
+ precise.push(...sp);
20273
+ }
20274
+ paraSplices = ok ? precise : null;
20275
+ } else if (paraTextPureT(para2, xml)) {
20276
+ let text = para2.text;
20277
+ for (let k = merged.length - 1; k >= 0; k--) {
20278
+ const r = merged[k];
20279
+ text = text.slice(0, r.start) + r.replacement + text.slice(r.end);
20280
+ }
20281
+ paraSplices = buildParagraphSplices(para2, text, xml);
20282
+ } else {
20283
+ paraSplices = null;
20284
+ }
20285
+ }
20286
+ if (paraSplices === null) {
20287
+ for (const idx of l.filledIdx) filled[idx] = null;
20288
+ for (const k of l.matchKeys) failedKeys.add(k);
20289
+ continue;
20290
+ }
20291
+ for (const k of l.matchKeys) succeededKeys.add(k);
20292
+ splices.push(...paraSplices);
20293
+ }
20294
+ if (splices.length > 0) {
20295
+ splices.push(...allLinesegRemovalSplices(xml));
20296
+ replacements.set(sectionPaths[si], encoder.encode(applySplices(xml, splices)));
20297
+ }
20031
20298
  }
20032
- reset() {
20033
- this.counts = [];
20299
+ for (const k of failedKeys) {
20300
+ if (!succeededKeys.has(k)) matchedLabels.delete(k);
20034
20301
  }
20035
- };
20036
- function mmToHwpunit(mm) {
20037
- return Math.round(mm * 7200 / 25.4);
20302
+ const cleanFilled = filled.filter((f) => f !== null);
20303
+ const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
20304
+ const out = replacements.size > 0 ? patchZipEntries(u8, replacements) : new Uint8Array(u8);
20305
+ return {
20306
+ buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
20307
+ filled: cleanFilled,
20308
+ unmatched
20309
+ };
20038
20310
  }
20039
20311
 
20312
+ // src/hwpx/generator.ts
20313
+ import JSZip6 from "jszip";
20314
+
20040
20315
  // src/hwpx/gen-ids.ts
20041
20316
  var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
20042
20317
  var NS_PARA = "http://www.hancom.co.kr/hwpml/2011/paragraph";
@@ -20080,7 +20355,7 @@ function resolveTheme(theme) {
20080
20355
  };
20081
20356
  }
20082
20357
  function escapeXml(text) {
20083
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20358
+ return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20084
20359
  }
20085
20360
  function headingParaPrId(level) {
20086
20361
  if (level === 1) return PARA_H1;
@@ -20099,7 +20374,7 @@ function charPr(id, height, bold, italic, fontId = 0, textColor = DEFAULT_TEXT_C
20099
20374
  const italicAttr = italic ? ` italic="1"` : "";
20100
20375
  const effFont = fontId;
20101
20376
  const boldEl = bold ? `<hh:bold/>` : "";
20102
- return ` <hh:charPr id="${id}" height="${height}" textColor="${textColor}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
20377
+ return ` <hh:charPr id="${id}" height="${height}" textColor="${escapeXml(textColor)}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
20103
20378
  <hh:fontRef hangul="${effFont}" latin="${effFont}" hanja="${effFont}" japanese="${effFont}" other="${effFont}" symbol="${effFont}" user="${effFont}"/>
20104
20379
  <hh:ratio hangul="${ratioPct}" latin="${ratioPct}" hanja="${ratioPct}" japanese="100" other="100" symbol="100" user="100"/>
20105
20380
  <hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
@@ -20143,13 +20418,17 @@ var GJ_CHAR_TABLE = 22;
20143
20418
  var GJ_CHAR_TABLE_BOLD = 23;
20144
20419
  var GJ_CHAR_BAR = 24;
20145
20420
  var GJ_CHAR_BODY_TITLE = 25;
20146
- var GJ_CHAR_COUNT = 15;
20421
+ var GJ_CHAR_TITLE_BAR = 26;
20422
+ var GJ_CHAR_APPROVAL = 27;
20423
+ var GJ_CHAR_COUNT = GJ_CHAR_APPROVAL - GJ_CHAR_DAE + 1;
20147
20424
  var GJ_PARA_CHAM = 20;
20148
20425
  var GJ_PARA_COVER = 21;
20149
20426
  var GJ_PARA_TOC_ITEM = 22;
20150
20427
  var GJ_PARA_CHAPTER = 23;
20151
20428
  var GJ_PARA_BAR = 24;
20152
20429
  var GONGMUN_LIST_PLAIN_BASE = 25;
20430
+ var GONGMUN_PARA_APPROVAL = GONGMUN_LIST_PLAIN_BASE + GONGMUN_LIST_LEVELS;
20431
+ var GONGMUN_LIST_VARIANT_BASE = GONGMUN_PARA_APPROVAL + 1;
20153
20432
  var GJ_BF_CHAPTER_NUM = 3;
20154
20433
  var GJ_BF_CHAPTER_GAP = 4;
20155
20434
  var GJ_BF_CHAPTER_TITLE = 5;
@@ -20157,8 +20436,8 @@ var GJ_BF_BAR_DARK = 6;
20157
20436
  var GJ_BF_BAR_LIGHT = 7;
20158
20437
  var GJ_BF_TOC_BOX = 8;
20159
20438
  var GJ_BF_TOC_STRIPE = 9;
20160
- function gongmunTableHeaderBf(gaejosik) {
20161
- return gaejosik ? 10 : 3;
20439
+ function gongmunTableHeaderBf(richAssets) {
20440
+ return richAssets ? 10 : 3;
20162
20441
  }
20163
20442
  function borderFillEntry(id, b, fill) {
20164
20443
  const side = (name, v) => v ? ` <hh:${name} type="${v[2] ?? "SOLID"}" width="${v[0]}" color="${v[1]}"/>` : ` <hh:${name} type="NONE" width="0.1 mm" color="#000000"/>`;
@@ -20191,10 +20470,12 @@ function pageHidingCtrl(hideHeader = false) {
20191
20470
  var GONGMUN_TBL_CHAR = 11;
20192
20471
  var GONGMUN_TBL_CHAR_BOLD = 12;
20193
20472
  var GONGMUN_TBL_PT = 1200;
20194
- var CHAR_VARIANT_BASE = 11;
20195
- function charVariantBase(measured, isGongmun = true) {
20473
+ var GONGMUN_TITLE_BAR_CHAR = 13;
20474
+ var GONGMUN_APPROVAL_CHAR = 14;
20475
+ var CHAR_VARIANT_BASE = CHAR_QUOTE + 1;
20476
+ function charVariantBase(richAssets, isGongmun = true) {
20196
20477
  if (!isGongmun) return CHAR_VARIANT_BASE;
20197
- return measured ? 11 + GJ_CHAR_COUNT : 11 + 2;
20478
+ return richAssets ? CHAR_VARIANT_BASE + GJ_CHAR_COUNT : CHAR_VARIANT_BASE + (GONGMUN_APPROVAL_CHAR - GONGMUN_TBL_CHAR + 1);
20198
20479
  }
20199
20480
  var GONGMUN_BODY_RATIO = 95;
20200
20481
 
@@ -20227,6 +20508,7 @@ function parseMarkdownToBlocks(md2) {
20227
20508
  const lines = md2.replace(/\r\n?/g, "\n").split("\n");
20228
20509
  const blocks = [];
20229
20510
  let i = 0;
20511
+ const listStack = [];
20230
20512
  while (i < lines.length) {
20231
20513
  const line = lines[i];
20232
20514
  if (!line.trim()) {
@@ -20315,7 +20597,8 @@ function parseMarkdownToBlocks(md2) {
20315
20597
  const tableRows = [];
20316
20598
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
20317
20599
  const row = lines[i];
20318
- if (/^[\s|:\-]+$/.test(row)) {
20600
+ const sepCells = row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
20601
+ if (sepCells.every((c) => /^\s*:?-+:?\s*$/.test(c))) {
20319
20602
  i++;
20320
20603
  continue;
20321
20604
  }
@@ -20329,17 +20612,36 @@ function parseMarkdownToBlocks(md2) {
20329
20612
  if (line.trimStart().startsWith("> ")) {
20330
20613
  const quoteLines = [];
20331
20614
  while (i < lines.length && (lines[i].trimStart().startsWith("> ") || lines[i].trimStart().startsWith(">"))) {
20332
- quoteLines.push(lines[i].replace(/^>\s?/, ""));
20615
+ quoteLines.push(lines[i].replace(/^>\s?/, "").trim());
20333
20616
  i++;
20334
20617
  }
20618
+ let joined = [];
20335
20619
  for (const ql of quoteLines) {
20336
- blocks.push({ type: "blockquote", text: ql.trim() || "" });
20620
+ if (ql) {
20621
+ joined.push(ql);
20622
+ continue;
20623
+ }
20624
+ if (joined.length) blocks.push({ type: "blockquote", text: joined.join("\n") });
20625
+ joined = [];
20337
20626
  }
20627
+ if (joined.length) blocks.push({ type: "blockquote", text: joined.join("\n") });
20338
20628
  continue;
20339
20629
  }
20340
- const listMatch = line.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
20630
+ const listLine = line.replace(/^[\t ]+/, (ws) => ws.replace(/\t/g, " "));
20631
+ const listMatch = listLine.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
20341
20632
  if (listMatch) {
20342
- const indent = Math.floor(listMatch[1].length / 2);
20633
+ if (blocks.length === 0 || blocks[blocks.length - 1].type !== "list_item") listStack.length = 0;
20634
+ const phys = listMatch[1].length;
20635
+ let indent;
20636
+ if (listStack.length === 0) {
20637
+ indent = Math.floor(phys / 2);
20638
+ for (let d = 0; d < indent; d++) listStack.push(d * 2);
20639
+ listStack.push(phys);
20640
+ } else {
20641
+ while (listStack.length > 1 && phys < listStack[listStack.length - 1]) listStack.pop();
20642
+ if (phys > listStack[listStack.length - 1]) listStack.push(phys);
20643
+ indent = listStack.length - 1;
20644
+ }
20343
20645
  const ordered = /\d/.test(listMatch[2]);
20344
20646
  blocks.push({ type: "list_item", text: listMatch[3].trim(), ordered, indent, marker: listMatch[2] });
20345
20647
  i++;
@@ -20360,7 +20662,7 @@ function parseInlineMarkdown(text) {
20360
20662
  text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_, t, u) => t || u);
20361
20663
  text = text.replace(/~~([^~]+)~~/g, "$1");
20362
20664
  const spans = [];
20363
- const regex = /(`[^`]+`|\*{3}[^*]+\*{3}|\*{2}[^*]+\*{2}|\*[^*]+\*|_{2}[^_]+_{2}|_[^_]+_)/g;
20665
+ const regex = /(`[^`]+`|\*{3}[^*]+\*{3}|\*{2}[^*]+\*{2}|\*[^*]+\*|(?<![^\s\p{P}\p{S}\x00])(?<!_)_{2}(?=\S)[^_]+(?<=\S)_{2}(?!_)(?![^\s\p{P}\p{S}\x00])|(?<![^\s\p{P}\p{S}\x00])(?<!_)_(?=\S)[^_]+(?<=\S)_(?!_)(?![^\s\p{P}\p{S}\x00]))/gu;
20364
20666
  let lastIdx = 0;
20365
20667
  for (const match of text.matchAll(regex)) {
20366
20668
  const idx = match.index;
@@ -20426,14 +20728,20 @@ function generateContainerXml() {
20426
20728
  </ocf:rootfiles>
20427
20729
  </ocf:container>`;
20428
20730
  }
20429
- function generateManifest(chartParts = []) {
20731
+ function generateManifest(chartParts = [], imageItems = [], layout = "default") {
20430
20732
  const chartItems = chartParts.map((p, i) => `
20431
20733
  <opf:item id="chart${i + 1}" href="${p.name}" media-type="application/xml"/>`).join("");
20734
+ const imgItems = imageItems.map((x) => `
20735
+ ${x}`).join("");
20432
20736
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
20433
20737
  <opf:package xmlns:opf="${NS_OPF}" xmlns:hpf="${NS_HPF}" xmlns:hh="${NS_HEAD}">
20738
+ <opf:metadata>
20739
+ <opf:meta name="generator" content="kordoc"/>
20740
+ <opf:meta name="kordoc-layout" content="${escapeXml(layout)}"/>
20741
+ </opf:metadata>
20434
20742
  <opf:manifest>
20435
20743
  <opf:item id="header" href="Contents/header.xml" media-type="application/xml"/>
20436
- <opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/>${chartItems}
20744
+ <opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/>${chartItems}${imgItems}
20437
20745
  </opf:manifest>
20438
20746
  <opf:spine>
20439
20747
  <opf:itemref idref="header" linear="no"/>
@@ -20441,18 +20749,30 @@ function generateManifest(chartParts = []) {
20441
20749
  </opf:spine>
20442
20750
  </opf:package>`;
20443
20751
  }
20752
+ function assertSequentialIds(rows, label, startId) {
20753
+ for (let i = 0; i < rows.length; i++) {
20754
+ const m = rows[i].match(/\bid="(\d+)"/);
20755
+ const got = m ? Number(m[1]) : NaN;
20756
+ if (got !== startId + i) {
20757
+ throw new KordocError(
20758
+ `\uB0B4\uBD80 \uC624\uB958: ${label} id \uD30C\uD2F0\uC158 \uB4DC\uB9AC\uD504\uD2B8 \u2014 ${startId + i} \uC704\uCE58\uC5D0 id=${m ? m[1] : "?"} \uBC29\uCD9C. id \uC0C1\uC218(gen-ids)\uC640 \uC2E4\uC81C \uBC29\uCD9C \uC218\uAC00 \uC5B4\uAE0B\uB0AC\uB2E4 (GJ_CHAR_COUNT\uB958 \uCE74\uC6B4\uD2B8\xB7base \uC0B0\uC220 \uD655\uC778)`
20759
+ );
20760
+ }
20761
+ }
20762
+ }
20444
20763
  function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls = []) {
20445
20764
  const measured = !!gongmun && usesReportFonts(gongmun.preset);
20765
+ const richAssets = !!gongmun && needsGaejosikAssets(gongmun);
20446
20766
  let body = 1e3, code = 900, h1 = 1800, h2 = 1400, h3 = 1200, h4 = 1100;
20447
20767
  if (gongmun) {
20448
20768
  body = gongmun.bodyHeight;
20449
20769
  code = Math.max(body - 200, 900);
20450
- h1 = gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700;
20451
- h2 = 1600;
20770
+ h1 = Math.max(gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700, body);
20771
+ h2 = Math.max(1600, body);
20452
20772
  h3 = body;
20453
- h4 = Math.max(body - 100, 1300);
20773
+ h4 = Math.min(h3, Math.max(body - 100, 1300));
20454
20774
  }
20455
- const bodyFont = measured ? 4 : 0;
20775
+ const bodyFont = richAssets ? 4 : 0;
20456
20776
  const hFont = measured ? 3 : 1;
20457
20777
  const hBold = !measured;
20458
20778
  const bodyRatio = gongmun ? GONGMUN_BODY_RATIO : 100;
@@ -20469,13 +20789,15 @@ function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls
20469
20789
  charPr(CHAR_TABLE_HEADER, body, theme.tableHeaderBold, false, bodyFont, theme.tableHeader),
20470
20790
  charPr(CHAR_QUOTE, body, false, true, bodyFont, theme.quote)
20471
20791
  ];
20472
- if (gongmun && !measured) {
20792
+ if (gongmun && !richAssets) {
20473
20793
  rows.push(
20474
20794
  charPr(11, GONGMUN_TBL_PT, false, false, bodyFont, theme.body),
20475
- charPr(12, GONGMUN_TBL_PT, true, false, bodyFont, theme.body)
20795
+ charPr(12, GONGMUN_TBL_PT, true, false, bodyFont, theme.body),
20796
+ charPr(GONGMUN_TITLE_BAR_CHAR, 100, false, false, bodyFont, theme.body),
20797
+ charPr(GONGMUN_APPROVAL_CHAR, 1e3, false, false, bodyFont, theme.body)
20476
20798
  );
20477
20799
  }
20478
- if (measured) {
20800
+ if (richAssets) {
20479
20801
  const sz = gaejosikSizes(body, gongmun.sizes);
20480
20802
  rows.push(
20481
20803
  charPr(11, sz.dae, false, false, 3),
@@ -20506,8 +20828,18 @@ function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls
20506
20828
  // 표 셀 bold
20507
20829
  charPr(24, sz.bar, false, false, 4),
20508
20830
  // 표지 바 셀 빈 문단(6pt)
20509
- charPr(25, sz.bodyTitle, false, false, 3)
20831
+ charPr(25, sz.bodyTitle, false, false, 3),
20510
20832
  // 본문 제목박스(HY헤드라인M 22pt, 실측 GT3 표④)
20833
+ charPr(GJ_CHAR_TITLE_BAR, 100, false, false, 4),
20834
+ // 1페이지형 제목박스 바(1pt)
20835
+ charPr(GJ_CHAR_APPROVAL, 1e3, false, false, 7)
20836
+ // 결재란 라벨(맑은 고딕 10pt)
20837
+ );
20838
+ }
20839
+ const expectedBase = charVariantBase(richAssets, !!gongmun);
20840
+ if (rows.length !== expectedBase) {
20841
+ throw new KordocError(
20842
+ `\uB0B4\uBD80 \uC624\uB958: charPr \uAE30\uBCF8 \uBE14\uB85D \uBC29\uCD9C \uC218(${rows.length}) \u2260 charVariantBase(${expectedBase}) \u2014 gen-header \uBC29\uCD9C \uD589\uACFC gen-ids \uCE74\uC6B4\uD2B8 \uC0C1\uC218\uAC00 \uC5B4\uAE0B\uB0AC\uB2E4`
20511
20843
  );
20512
20844
  }
20513
20845
  for (const r of ratioVariants) {
@@ -20519,11 +20851,12 @@ function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls
20519
20851
  );
20520
20852
  }
20521
20853
  rows.push(...extraCharPrXmls);
20854
+ assertSequentialIds(rows, "charPr", 0);
20522
20855
  return `<hh:charProperties itemCnt="${rows.length}">
20523
20856
  ${rows.join("\n")}
20524
20857
  </hh:charProperties>`;
20525
20858
  }
20526
- function buildParaProperties(gongmun) {
20859
+ function buildParaProperties(gongmun, listIndentVariants = []) {
20527
20860
  if (!gongmun) {
20528
20861
  const base2 = [
20529
20862
  paraPr(0, { keepWord: true }),
@@ -20535,6 +20868,7 @@ function buildParaProperties(gongmun) {
20535
20868
  paraPr(6, { align: "LEFT", lineSpacing: 150, indent: 600, keepWord: true }),
20536
20869
  paraPr(7, { align: "LEFT", lineSpacing: 160, indent: 600, keepWord: true })
20537
20870
  ];
20871
+ assertSequentialIds(base2, "paraPr", 0);
20538
20872
  return `<hh:paraProperties itemCnt="${base2.length}">
20539
20873
  ${base2.join("\n")}
20540
20874
  </hh:paraProperties>`;
@@ -20553,7 +20887,7 @@ ${base2.join("\n")}
20553
20887
  paraPr(7, { align: "LEFT", lineSpacing: ls, indent: 600, keepWord: true })
20554
20888
  ];
20555
20889
  for (let d = 0; d < GONGMUN_LIST_LEVELS; d++) {
20556
- const { left, indent } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, gongmun.preset === "press");
20890
+ const { left, indent } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
20557
20891
  const sectionGap = gongmun.numbering === "gaejosik" || gongmun.numbering === "report" ? gaejosikSpaceBefore(d, gongmun.bodyHeight) : 0;
20558
20892
  const keepNext = (gongmun.numbering === "gaejosik" || gongmun.numbering === "report") && d === 0;
20559
20893
  base.push(paraPr(GONGMUN_LIST_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent, spaceBefore: sectionGap, keepWord: true, keepWithNext: keepNext }));
@@ -20576,10 +20910,19 @@ ${base2.join("\n")}
20576
20910
  );
20577
20911
  }
20578
20912
  for (let d = 0; d < GONGMUN_LIST_LEVELS; d++) {
20579
- const { left } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, gongmun.preset === "press");
20913
+ const { left } = levelIndent(d, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
20580
20914
  const sectionGap = gongmun.numbering === "gaejosik" || gongmun.numbering === "report" ? gaejosikSpaceBefore(d, gongmun.bodyHeight) : 0;
20581
20915
  base.push(paraPr(GONGMUN_LIST_PLAIN_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent: 0, spaceBefore: sectionGap, keepWord: true }));
20582
20916
  }
20917
+ base.push(paraPr(GONGMUN_PARA_APPROVAL, { align: "CENTER", lineSpacing: 100, keepWord: true }));
20918
+ for (let vi = 0; vi < listIndentVariants.length; vi++) {
20919
+ const { depth, widthHu } = listIndentVariants[vi];
20920
+ const { left } = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
20921
+ const sectionGap = gongmun.numbering === "gaejosik" || gongmun.numbering === "report" ? gaejosikSpaceBefore(depth, gongmun.bodyHeight) : 0;
20922
+ const keepNext = (gongmun.numbering === "gaejosik" || gongmun.numbering === "report") && depth === 0;
20923
+ base.push(paraPr(GONGMUN_LIST_VARIANT_BASE + vi, { align: "JUSTIFY", lineSpacing: ls, left, indent: -widthHu, spaceBefore: sectionGap, keepWord: true, keepWithNext: keepNext }));
20924
+ }
20925
+ assertSequentialIds(base, "paraPr", 0);
20583
20926
  return `<hh:paraProperties itemCnt="${base.length}">
20584
20927
  ${base.join("\n")}
20585
20928
  </hh:paraProperties>`;
@@ -20596,14 +20939,17 @@ ${heads}
20596
20939
  </hh:numberings>`;
20597
20940
  }
20598
20941
  function fontEntry(id, face, weight = 6) {
20599
- return ` <hh:font id="${id}" face="${face}" type="TTF" isEmbedded="0">
20942
+ return ` <hh:font id="${id}" face="${escapeXml(face)}" type="TTF" isEmbedded="0">
20600
20943
  <hh:typeInfo familyType="FCAT_GOTHIC" weight="${weight}" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20601
20944
  </hh:font>`;
20602
20945
  }
20603
- function buildFontFaces(gongmun, bodyFace) {
20604
- if (gongmun && usesReportFonts(gongmun.preset)) {
20946
+ function staticFontNext(gongmun) {
20947
+ return gongmun && needsGaejosikAssets(gongmun) ? 8 : 3;
20948
+ }
20949
+ function buildFontFaces(gongmun, bodyFace, extraFonts = []) {
20950
+ if (gongmun && needsGaejosikAssets(gongmun)) {
20605
20951
  const ov = gongmun.fonts;
20606
- const bodyDefault = gongmun.preset !== "gaejosik" && gongmun.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD734\uBA3C\uBA85\uC870";
20952
+ const bodyDefault = usesReportFonts(gongmun.preset) ? gongmun.preset !== "gaejosik" && gongmun.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD734\uBA3C\uBA85\uC870" : bodyFace;
20607
20953
  const faces = [
20608
20954
  "\uD568\uCD08\uB86C\uBC14\uD0D5",
20609
20955
  "\uD568\uCD08\uB86C\uB3CB\uC6C0",
@@ -20614,21 +20960,27 @@ function buildFontFaces(gongmun, bodyFace) {
20614
20960
  "\uD55C\uC591\uC2E0\uBA85\uC870",
20615
20961
  ov.table ?? "\uB9D1\uC740 \uACE0\uB515"
20616
20962
  ];
20617
- const list = faces.map((f, i) => fontEntry(i, f, i === 2 || i === 3 ? 9 : 6)).join("\n");
20963
+ if (faces.length !== staticFontNext(gongmun)) {
20964
+ throw new Error(`fontface \uC815\uC801 \uAC1C\uC218 \uB4DC\uB9AC\uD504\uD2B8: ${faces.length} \u2260 staticFontNext ${staticFontNext(gongmun)}`);
20965
+ }
20966
+ const list = faces.map((f, i) => fontEntry(i, f, i === 2 || i === 3 ? 9 : 6)).concat(extraFonts.map((f, i) => fontEntry(faces.length + i, f))).join("\n");
20618
20967
  const langs = ["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
20619
20968
  return `<hh:fontfaces itemCnt="${langs.length}">
20620
- ` + langs.map((l) => ` <hh:fontface lang="${l}" fontCnt="${faces.length}">
20969
+ ` + langs.map((l) => ` <hh:fontface lang="${l}" fontCnt="${faces.length + extraFonts.length}">
20621
20970
  ${list}
20622
20971
  </hh:fontface>`).join("\n") + `
20623
20972
  </hh:fontfaces>`;
20624
20973
  }
20974
+ const extraH = extraFonts.map((f, i) => fontEntry(3 + i, f)).join("\n");
20975
+ const extraBlock = extraH ? `
20976
+ ${extraH}` : "";
20625
20977
  return `<hh:fontfaces itemCnt="7">
20626
- <hh:fontface lang="HANGUL" fontCnt="3">
20978
+ <hh:fontface lang="HANGUL" fontCnt="${3 + extraFonts.length}">
20627
20979
  ${fontEntry(0, bodyFace)}
20628
20980
  ${fontEntry(1, "\uD568\uCD08\uB86C\uB3CB\uC6C0")}
20629
- ${fontEntry(2, "HY\uACAC\uACE0\uB515", 9)}
20981
+ ${fontEntry(2, "HY\uACAC\uACE0\uB515", 9)}${extraBlock}
20630
20982
  </hh:fontface>
20631
- <hh:fontface lang="LATIN" fontCnt="3">
20983
+ <hh:fontface lang="LATIN" fontCnt="${3 + extraFonts.length}">
20632
20984
  <hh:font id="0" face="Times New Roman" type="TTF" isEmbedded="0">
20633
20985
  <hh:typeInfo familyType="FCAT_OLDSTYLE" weight="5" proportion="4" contrast="2" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="4"/>
20634
20986
  </hh:font>
@@ -20637,7 +20989,7 @@ ${fontEntry(2, "HY\uACAC\uACE0\uB515", 9)}
20637
20989
  </hh:font>
20638
20990
  <hh:font id="2" face="Arial Black" type="TTF" isEmbedded="0">
20639
20991
  <hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
20640
- </hh:font>
20992
+ </hh:font>${extraBlock}
20641
20993
  </hh:fontface>
20642
20994
  <hh:fontface lang="HANJA" fontCnt="1">
20643
20995
  ${fontEntry(0, "\uD568\uCD08\uB86C\uBC14\uD0D5")}
@@ -20656,13 +21008,13 @@ ${fontEntry(0, "\uAD74\uB9BC")}
20656
21008
  </hh:fontface>
20657
21009
  </hh:fontfaces>`;
20658
21010
  }
20659
- function buildBorderFills(gongmun, extra = []) {
21011
+ function staticBorderFillItems(gongmun) {
20660
21012
  const thin = ["0.12 mm", "#000000"];
20661
21013
  const items = [
20662
21014
  borderFillEntry(1, {}),
20663
21015
  borderFillEntry(2, { l: thin, r: thin, t: thin, b: thin })
20664
21016
  ];
20665
- if (gongmun?.preset === "gaejosik") {
21017
+ if (gongmun && needsGaejosikAssets(gongmun)) {
20666
21018
  const c = GAEJOSIK_COLORS;
20667
21019
  const edge = ["0.12 mm", c.border];
20668
21020
  const grayLine = ["0.1 mm", c.titleLine];
@@ -20687,7 +21039,14 @@ function buildBorderFills(gongmun, extra = []) {
20687
21039
  if (gongmun) {
20688
21040
  items.push(borderFillEntry(items.length + 1, { l: thin, r: thin, t: thin, b: thin }, "#E6E6E6"));
20689
21041
  }
20690
- items.push(...extra);
21042
+ return items;
21043
+ }
21044
+ function staticBorderFillNext(gongmun) {
21045
+ return staticBorderFillItems(gongmun).length + 1;
21046
+ }
21047
+ function buildBorderFills(gongmun, extra = []) {
21048
+ const items = [...staticBorderFillItems(gongmun), ...extra];
21049
+ assertSequentialIds(items, "borderFill", 1);
20691
21050
  return `<hh:borderFills itemCnt="${items.length}">
20692
21051
  ${items.join("\n")}
20693
21052
  </hh:borderFills>`;
@@ -20705,15 +21064,15 @@ function buildStyles(gongmun) {
20705
21064
  ${items.join("\n ")}
20706
21065
  </hh:styles>`;
20707
21066
  }
20708
- function generateHeaderXml(theme, gongmun, ratioVariants = [], extraBorderFills = [], extraCharPrXmls = []) {
21067
+ function generateHeaderXml(theme, gongmun, ratioVariants = [], extraBorderFills = [], extraCharPrXmls = [], listIndentVariants = [], extraFonts = []) {
20709
21068
  const bodyFace = gongmun?.fonts.body ?? (gongmun?.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD568\uCD08\uB86C\uBC14\uD0D5");
20710
21069
  const charPropsXml = buildCharProperties(theme, gongmun, ratioVariants, extraCharPrXmls);
20711
- const paraPropsXml = buildParaProperties(gongmun);
21070
+ const paraPropsXml = buildParaProperties(gongmun, gongmun ? listIndentVariants : []);
20712
21071
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
20713
21072
  <hh:head xmlns:hh="${NS_HEAD}" xmlns:hp="${NS_PARA}" xmlns:hc="${NS_CORE}" version="1.4" secCnt="1">
20714
21073
  <hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
20715
21074
  <hh:refList>
20716
- ${buildFontFaces(gongmun, bodyFace)}
21075
+ ${buildFontFaces(gongmun, bodyFace, extraFonts)}
20717
21076
  ${buildBorderFills(gongmun, extraBorderFills)}
20718
21077
  ${charPropsXml}
20719
21078
  <hh:tabProperties itemCnt="0"/>
@@ -20787,7 +21146,6 @@ function chartSpec(t) {
20787
21146
  const key = CHART_ALIAS[t.toLowerCase()] ?? Number(t);
20788
21147
  return CHART_TYPES[key] ?? CHART_TYPES[0];
20789
21148
  }
20790
- var HU_PER_MM = 7200 / 25.4;
20791
21149
  var RESERVED_KEYS = /* @__PURE__ */ new Set(["type", "cat", "size", "colors", "point_colors", "title"]);
20792
21150
  function parseChartFence(text) {
20793
21151
  let type;
@@ -20946,7 +21304,7 @@ function plainRenderText(text) {
20946
21304
  function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
20947
21305
  const minRatio = gongmun.autoFitMinRatio;
20948
21306
  if (minRatio === null || minRatio >= GONGMUN_BODY_RATIO) return null;
20949
- const pageW = 59528 - mmToHwpunit(gongmun.margins.left) - mmToHwpunit(gongmun.margins.right);
21307
+ const pageW = A4_W_HU - mmToHwpunit(gongmun.margins.left) - mmToHwpunit(gongmun.margins.right);
20950
21308
  const ratioByBlock = /* @__PURE__ */ new Map();
20951
21309
  const variants = [];
20952
21310
  for (let i = 0; i < blocks.length; i++) {
@@ -20954,14 +21312,14 @@ function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
20954
21312
  let text;
20955
21313
  let firstW;
20956
21314
  let contW;
20957
- if (block.type === "list_item" && gongmunList.has(i)) {
20958
- const { marker, depth } = gongmunList.get(i);
21315
+ if (block.type === "list_item" && gongmunList.items.has(i)) {
21316
+ const { marker, depth, indentVariant } = gongmunList.items.get(i);
20959
21317
  if (gongmun.numbering === "gaejosik" && (depth === 0 || (block.text || "").trimStart().startsWith("\u203B"))) continue;
20960
21318
  const content = plainRenderText(block.text || "");
20961
21319
  text = marker ? `${marker} ${content}` : content;
20962
- const li = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, gongmun.preset === "press");
21320
+ const li = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
20963
21321
  const left = li.left;
20964
- const indent = marker ? li.indent : 0;
21322
+ const indent = marker ? indentVariant !== void 0 ? -gongmunList.indentVariants[indentVariant].widthHu : li.indent : 0;
20965
21323
  firstW = pageW - left - Math.max(indent, 0);
20966
21324
  contW = pageW - left - Math.max(-indent, 0);
20967
21325
  } else if (block.type === "paragraph") {
@@ -20987,9 +21345,55 @@ function variantMapper(fit, blockIdx, base = CHAR_VARIANT_BASE) {
20987
21345
  const vi = fit.variants.indexOf(r);
20988
21346
  return (id) => id >= 0 && id <= 3 ? base + vi * 4 + id : id;
20989
21347
  }
21348
+ var ORDINAL_SYLLABLES = new Set(Array.from({ length: 84 }, (_, i) => hangulOrdinal(i)));
21349
+ function literalMarkerDepth(text) {
21350
+ const t = text.trimStart();
21351
+ let m;
21352
+ if ((m = /^([가-힣])\.\s+(.*)$/s.exec(t)) && ORDINAL_SYLLABLES.has(m[1])) return { depth: 1, content: m[2] };
21353
+ if (m = /^(\d{1,3})\)\s+(.*)$/s.exec(t)) return { depth: 2, content: m[2] };
21354
+ if ((m = /^([가-힣])\)\s+(.*)$/s.exec(t)) && ORDINAL_SYLLABLES.has(m[1])) return { depth: 3, content: m[2] };
21355
+ if (m = /^\((\d{1,3})\)\s+(.*)$/s.exec(t)) return { depth: 4, content: m[2] };
21356
+ if ((m = /^\(([가-힣])\)\s+(.*)$/s.exec(t)) && ORDINAL_SYLLABLES.has(m[1])) return { depth: 5, content: m[2] };
21357
+ if (m = /^([①-⑳㉑-㉟㊱-㊿])\s+(.*)$/s.exec(t)) return { depth: 6, content: m[2] };
21358
+ if (m = /^([㉮-㉻])\s+(.*)$/s.exec(t)) return { depth: 7, content: m[2] };
21359
+ if (m = /^□\s+(.*)$/s.exec(t)) return { depth: 0, content: m[1] };
21360
+ if (m = /^[○ㅇ]\s+(.*)$/s.exec(t)) return { depth: 1, content: m[1] };
21361
+ if (m = /^ㆍ\s+(.*)$/s.exec(t)) return { depth: 3, content: m[1] };
21362
+ return null;
21363
+ }
21364
+ function reclassifyLiteralItems(blocks, depthOffset) {
21365
+ for (const b of blocks) {
21366
+ if (b.type !== "paragraph") continue;
21367
+ const raw = (b.text || "").trimStart();
21368
+ if (!raw || raw.startsWith("\u203B") || /^\*\s/.test(raw) || /^<(center|right)>/i.test(raw)) continue;
21369
+ const lit = literalMarkerDepth(raw);
21370
+ if (!lit || !lit.content.trim()) continue;
21371
+ b.type = "list_item";
21372
+ b.text = lit.content;
21373
+ b.indent = Math.max(lit.depth - depthOffset, 0);
21374
+ b.ordered = lit.depth !== 0;
21375
+ }
21376
+ }
20990
21377
  function precomputeGongmunList(blocks, gongmun) {
20991
21378
  const result = /* @__PURE__ */ new Map();
21379
+ const indentVariants = [];
21380
+ const variantIdx = /* @__PURE__ */ new Map();
21381
+ const variantOf = (marker, depth) => {
21382
+ if (!marker || gongmun.numbering !== "standard") return void 0;
21383
+ const w = markerWidth(marker, gongmun.bodyHeight);
21384
+ const rep = -levelIndent(depth, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, usesAsteriskThird(gongmun.preset)).indent;
21385
+ if (w === rep) return void 0;
21386
+ const key = `${depth}:${w}`;
21387
+ let vi = variantIdx.get(key);
21388
+ if (vi === void 0) {
21389
+ vi = indentVariants.length;
21390
+ indentVariants.push({ depth, widthHu: w });
21391
+ variantIdx.set(key, vi);
21392
+ }
21393
+ return vi;
21394
+ };
20992
21395
  const depthOffset = gongmun.numbering === "gaejosik" && blocks.some((b) => b.type === "heading" && b.level === 3) || gongmun.numbering === "standard" && gongmun.h2Marker === "number" && blocks.some((b) => b.type === "heading" && b.level === 2) ? 1 : 0;
21396
+ reclassifyLiteralItems(blocks, depthOffset);
20993
21397
  let i = 0;
20994
21398
  while (i < blocks.length) {
20995
21399
  if (blocks[i].type !== "list_item") {
@@ -21016,14 +21420,21 @@ function precomputeGongmunList(blocks, gongmun) {
21016
21420
  break;
21017
21421
  }
21018
21422
  const depths = run.map((bi) => Math.min(Math.max((blocks[bi].indent || 0) + depthOffset, 0), GONGMUN_LIST_LEVELS - 1));
21019
- const suppress = gongmun.suppressSingle && gongmun.numbering === "standard" ? computeSuppression(depths) : depths.map(() => false);
21020
- const numberer = new GongmunNumberer(gongmun.numbering, gongmun.bullet2, gongmun.preset === "press");
21423
+ const measured = usesReportFonts(gongmun.preset);
21424
+ const isRef = (bi) => measured && ((blocks[bi].text || "").trimStart().startsWith("\u203B") || blocks[bi].marker === "*");
21425
+ const activeK = run.map((_, k) => k).filter((k) => !isRef(run[k]));
21426
+ const activeDepths = activeK.map((k) => depths[k]);
21427
+ const suppress = gongmun.suppressSingle && gongmun.numbering === "standard" ? computeSuppression(activeDepths) : activeDepths.map(() => false);
21428
+ const numberer = new GongmunNumberer(gongmun.numbering, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
21021
21429
  run.forEach((bi, k) => {
21022
- const marker = numberer.next(depths[k], suppress[k]);
21023
- result.set(bi, { marker, depth: depths[k] });
21430
+ if (isRef(run[k])) result.set(bi, { marker: "", depth: depths[k] });
21431
+ });
21432
+ activeK.forEach((k, j) => {
21433
+ const marker = numberer.next(activeDepths[j], suppress[j]);
21434
+ result.set(run[k], { marker, depth: depths[k], indentVariant: variantOf(marker, depths[k]) });
21024
21435
  });
21025
21436
  }
21026
- return result;
21437
+ return { items: result, indentVariants };
21027
21438
  }
21028
21439
 
21029
21440
  // src/hwpx/gen-gaejosik.ts
@@ -21041,26 +21452,29 @@ function emptyPara(paraPrId = PARA_NORMAL, pageBreak = false, charPrId = CHAR_NO
21041
21452
  const brk = pageBreak ? ` pageBreak="1"` : "";
21042
21453
  return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"${brk}><hp:run charPrIDRef="${charPrId}"><hp:t></hp:t></hp:run></hp:p>`;
21043
21454
  }
21044
- var gjTableId = 92e5;
21455
+ var gjTableId = GJ_TABLE_ID_BASE;
21456
+ function resetGjTableIds() {
21457
+ gjTableId = GJ_TABLE_ID_BASE;
21458
+ }
21045
21459
  function cell(opts) {
21046
21460
  const paras = opts.paras ?? emptyPara();
21047
- return `<hp:tc name="" header="0" hasMargin="0" protect="0" editable="1" dirty="0" borderFillIDRef="${opts.bf}"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="${opts.vertAlign ?? "CENTER"}" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0">${paras}</hp:subList><hp:cellAddr colAddr="${opts.col}" rowAddr="${opts.row ?? 0}"/><hp:cellSpan colSpan="${opts.colSpan ?? 1}" rowSpan="1"/><hp:cellSz width="${opts.w}" height="${opts.h}"/><hp:cellMargin left="141" right="141" top="141" bottom="141"/></hp:tc>`;
21461
+ return `<hp:tc name="${opts.name ?? ""}" header="0" hasMargin="0" protect="0" editable="1" dirty="0" borderFillIDRef="${opts.bf}"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="${opts.vertAlign ?? "CENTER"}" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0">${paras}</hp:subList><hp:cellAddr colAddr="${opts.col}" rowAddr="${opts.row ?? 0}"/><hp:cellSpan colSpan="${opts.colSpan ?? 1}" rowSpan="1"/><hp:cellSz width="${opts.w}" height="${opts.h}"/><hp:cellMargin left="141" right="141" top="141" bottom="141"/></hp:tc>`;
21048
21462
  }
21049
- function table(rows, w, h, cols, tblBf = 1) {
21050
- const outLR = w + 566 > 48e3 ? 0 : 283;
21463
+ function table(rows, w, h, cols, tblBf = 1, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21464
+ const outLR = w + 566 > bodyWidth ? 0 : 283;
21051
21465
  return `<hp:tbl id="${++gjTableId}" zOrder="0" numberingType="TABLE" textWrap="TOP_AND_BOTTOM" textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL" repeatHeader="0" rowCnt="${rows.length}" colCnt="${cols}" cellSpacing="0" borderFillIDRef="${tblBf}" noAdjust="1"><hp:sz width="${w}" widthRelTo="ABSOLUTE" height="${h}" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="${outLR}" right="${outLR}" top="283" bottom="283"/><hp:inMargin left="141" right="141" top="141" bottom="141"/>` + rows.map((r) => `<hp:tr>${r}</hp:tr>`).join("") + `</hp:tbl>`;
21052
21466
  }
21053
- function buildGaejosikCover(title, gongmun) {
21054
- const g = coverGeom(gongmun.bodyHeight, gongmun.sizes);
21467
+ function buildGaejosikCover(title, gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21468
+ const g = coverGeom(gongmun.bodyHeight, gongmun.sizes, bodyWidth);
21055
21469
  const sz = gaejosikSizes(gongmun.bodyHeight, gongmun.sizes);
21056
21470
  const innerW = g.totalW - 282;
21057
21471
  const titleCharPr = simulateWrap(title, innerW, innerW, sz.coverTitle, 100, "keep").lines > 2 ? GJ_CHAR_COVER_SUB : GJ_CHAR_COVER_TITLE;
21058
21472
  const titlePara = `<hp:p paraPrIDRef="${GJ_PARA_COVER}" styleIDRef="0"><hp:run charPrIDRef="${titleCharPr}"><hp:t>${escapeXml(title)}</hp:t></hp:run></hp:p>`;
21059
21473
  const barEmpty = emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR);
21060
21474
  const topRow = cell({ bf: GJ_BF_BAR_DARK, col: 0, colSpan: 2, row: 0, w: g.topDarkW, h: g.barH, paras: barEmpty }) + cell({ bf: GJ_BF_BAR_LIGHT, col: 2, row: 0, w: g.topLightW, h: g.barH, paras: barEmpty });
21061
- const titleRow = cell({ bf: 1, col: 0, colSpan: 3, row: 1, w: g.totalW, h: g.titleH, paras: titlePara });
21475
+ const titleRow = cell({ bf: 1, col: 0, colSpan: 3, row: 1, w: g.totalW, h: g.titleH, paras: titlePara, name: "__kordoc_h1" });
21062
21476
  const botRow = cell({ bf: GJ_BF_BAR_DARK, col: 0, row: 2, w: g.botDarkW, h: g.botBarH, paras: barEmpty }) + cell({ bf: GJ_BF_BAR_LIGHT, col: 1, colSpan: 2, row: 2, w: g.botLightW, h: g.botBarH, paras: barEmpty });
21063
- const tbl2 = table([topRow, titleRow, botRow], g.totalW, g.barH + g.titleH + g.botBarH, 3);
21477
+ const tbl2 = table([topRow, titleRow, botRow], g.totalW, g.barH + g.titleH + g.botBarH, 3, 1, bodyWidth);
21064
21478
  const host = `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${tbl2}</hp:run></hp:p>`;
21065
21479
  const date = gongmun.cover?.date ?? formatGaejosikDate(/* @__PURE__ */ new Date());
21066
21480
  const org = gongmun.cover?.org ?? "";
@@ -21074,48 +21488,51 @@ function buildGaejosikCover(title, gongmun) {
21074
21488
  ...org ? [...Array(4).fill(subEmpty), sub(org)] : []
21075
21489
  ];
21076
21490
  }
21077
- function buildTocBanner(gongmun) {
21491
+ function buildTocBanner(gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21078
21492
  const g = tocBannerGeom(gongmun.bodyHeight, gongmun.sizes);
21079
21493
  const bfs = [GJ_BF_BAR_DARK, 1, GJ_BF_TOC_STRIPE, 1, GJ_BF_TOC_STRIPE, 1, GJ_BF_BAR_DARK];
21080
21494
  const filler = emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR);
21081
21495
  const label = `<hp:p paraPrIDRef="${GONGMUN_CENTER}" styleIDRef="0"><hp:run charPrIDRef="${GJ_CHAR_TOC_LABEL}"><hp:t>\uBAA9 \uCC28</hp:t></hp:run></hp:p>`;
21082
21496
  const row = g.colWs.map(
21083
- (w, c) => cell({ bf: bfs[c], col: c, w, h: g.h, paras: c === 3 ? label : filler })
21497
+ (w, c) => cell({ bf: bfs[c], col: c, w, h: g.h, paras: c === 3 ? label : filler, name: c === 3 ? "__kordoc_toc" : void 0 })
21084
21498
  ).join("");
21085
- const banner = table([row], g.colWs.reduce((a, b) => a + b, 0), g.h, 7, 2);
21499
+ const banner = table([row], g.colWs.reduce((a, b) => a + b, 0), g.h, 7, 2, bodyWidth);
21086
21500
  return `<hp:p paraPrIDRef="${GJ_PARA_BAR}" styleIDRef="0" pageBreak="1"><hp:run charPrIDRef="${CHAR_NORMAL}">${banner}</hp:run></hp:p>`;
21087
21501
  }
21088
- function buildGaejosikToc(chapters, gongmun) {
21089
- const label = buildTocBanner(gongmun);
21502
+ function buildGaejosikToc(chapters, gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21503
+ const boxW = Math.round(TOC_GEOM.boxW * bodyWidth / GAEJOSIK_BASE_WIDTH);
21504
+ const label = buildTocBanner(gongmun, bodyWidth);
21090
21505
  const items = chapters.map(
21091
21506
  (title, i) => `<hp:p paraPrIDRef="${GJ_PARA_TOC_ITEM}" styleIDRef="0"><hp:run charPrIDRef="${GJ_CHAR_TOC_ROMAN}"><hp:t>${chapterRoman(i + 1)}</hp:t></hp:run><hp:run charPrIDRef="${GJ_CHAR_TOC_ITEM}"><hp:t>. ${escapeXml(title)}</hp:t></hp:run></hp:p>`
21092
21507
  ).join("");
21093
21508
  const box = table(
21094
- [cell({ bf: GJ_BF_TOC_BOX, col: 0, w: TOC_GEOM.boxW, h: TOC_GEOM.boxH, paras: items })],
21095
- TOC_GEOM.boxW,
21509
+ [cell({ bf: GJ_BF_TOC_BOX, col: 0, w: boxW, h: TOC_GEOM.boxH, paras: items, name: "__kordoc_toc" })],
21510
+ boxW,
21096
21511
  TOC_GEOM.boxH,
21097
- 1
21512
+ 1,
21513
+ 1,
21514
+ bodyWidth
21098
21515
  );
21099
21516
  const host = `<hp:p paraPrIDRef="${GJ_PARA_BAR}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${box}</hp:run></hp:p>`;
21100
21517
  return [label, emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR), host];
21101
21518
  }
21102
- function buildGaejosikBodyTitle(title, gongmun) {
21103
- const g = COVER_GEOM;
21519
+ function buildGaejosikBodyTitle(title, gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21520
+ const g = coverGeom(gongmun.bodyHeight, gongmun.sizes, bodyWidth);
21104
21521
  const bt = bodyTitleGeom(gongmun.bodyHeight, gongmun.sizes);
21105
21522
  const barEmpty = emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR);
21106
21523
  const titlePara = `<hp:p paraPrIDRef="${GONGMUN_CENTER}" styleIDRef="0"><hp:run charPrIDRef="${GJ_CHAR_BODY_TITLE}"><hp:t>${escapeXml(title)}</hp:t></hp:run></hp:p>`;
21107
21524
  const topRow = cell({ bf: GJ_BF_BAR_DARK, col: 0, colSpan: 2, row: 0, w: g.topDarkW, h: bt.barH, paras: barEmpty }) + cell({ bf: GJ_BF_BAR_LIGHT, col: 2, row: 0, w: g.topLightW, h: bt.barH, paras: barEmpty });
21108
- const titleRow = cell({ bf: 1, col: 0, colSpan: 3, row: 1, w: g.totalW, h: bt.titleH, paras: titlePara });
21525
+ const titleRow = cell({ bf: 1, col: 0, colSpan: 3, row: 1, w: g.totalW, h: bt.titleH, paras: titlePara, name: "__kordoc_skip" });
21109
21526
  const botRow = cell({ bf: GJ_BF_BAR_DARK, col: 0, row: 2, w: g.botDarkW, h: bt.barH, paras: barEmpty }) + cell({ bf: GJ_BF_BAR_LIGHT, col: 1, colSpan: 2, row: 2, w: g.botLightW, h: bt.barH, paras: barEmpty });
21110
- const box = table([topRow, titleRow, botRow], g.totalW, bt.barH * 2 + bt.titleH, 3);
21527
+ const box = table([topRow, titleRow, botRow], g.totalW, bt.barH * 2 + bt.titleH, 3, 1, bodyWidth);
21111
21528
  return `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${box}</hp:run></hp:p>`;
21112
21529
  }
21113
- function buildGaejosikChapter(n, title, gongmun) {
21114
- const g = gongmun ? chapterGeom(gongmun.bodyHeight, gongmun.sizes) : chapterGeom(1500);
21530
+ function buildGaejosikChapter(n, title, gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH, srcLevel = 2) {
21531
+ const g = gongmun ? chapterGeom(gongmun.bodyHeight, gongmun.sizes, bodyWidth) : chapterGeom(1500, {}, bodyWidth);
21115
21532
  const numPara = `<hp:p paraPrIDRef="${GONGMUN_CENTER}" styleIDRef="0"><hp:run charPrIDRef="${GJ_CHAR_CHAPTER_NUM}"><hp:t>${chapterRoman(n)}</hp:t></hp:run></hp:p>`;
21116
21533
  const titlePara = `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${GJ_CHAR_CHAPTER_TITLE}"><hp:t> ${escapeXml(title)}</hp:t></hp:run></hp:p>`;
21117
- const row = cell({ bf: GJ_BF_CHAPTER_NUM, col: 0, w: g.numW, h: g.rowH, paras: numPara }) + cell({ bf: GJ_BF_CHAPTER_GAP, col: 1, w: g.gapW, h: g.rowH }) + cell({ bf: GJ_BF_CHAPTER_TITLE, col: 2, w: g.titleW, h: g.rowH, paras: titlePara });
21118
- const tbl2 = table([row], g.numW + g.gapW + g.titleW, g.rowH, 3);
21534
+ const row = cell({ bf: GJ_BF_CHAPTER_NUM, col: 0, w: g.numW, h: g.rowH, paras: numPara }) + cell({ bf: GJ_BF_CHAPTER_GAP, col: 1, w: g.gapW, h: g.rowH }) + cell({ bf: GJ_BF_CHAPTER_TITLE, col: 2, w: g.titleW, h: g.rowH, paras: titlePara, name: `__kordoc_h${Math.min(Math.max(srcLevel, 1), 6)}` });
21535
+ const tbl2 = table([row], g.numW + g.gapW + g.titleW, g.rowH, 3, 1, bodyWidth);
21119
21536
  return `<hp:p paraPrIDRef="${GJ_PARA_CHAPTER}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${tbl2}</hp:run></hp:p>`;
21120
21537
  }
21121
21538
 
@@ -21483,16 +21900,22 @@ var AUTONUM_PREFIX_RE = /^(?:[0-90-9a-zA-Z가-힣]{1,6}[.)\]:]|[([][0-90-
21483
21900
  function htmlCellInnerToLines(inner) {
21484
21901
  let hadNonText = false;
21485
21902
  let work = inner;
21903
+ const imgSrcs = [];
21486
21904
  if (/<table[\s>]/i.test(work)) {
21487
21905
  hadNonText = true;
21488
21906
  work = removeNestedTables(work);
21489
21907
  }
21490
21908
  if (/<img\s/i.test(work)) {
21491
21909
  hadNonText = true;
21492
- work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, "");
21910
+ work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, (tag) => {
21911
+ const m = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(tag);
21912
+ const src = m?.[1] ?? m?.[2];
21913
+ if (src) imgSrcs.push(src);
21914
+ return "";
21915
+ });
21493
21916
  }
21494
21917
  const lines = work.split(/<br\s*\/?>/gi).map((s) => s.trim()).filter((s) => s.length > 0);
21495
- return { lines, hadNonText };
21918
+ return { lines, hadNonText, imgSrcs };
21496
21919
  }
21497
21920
  function extractTopLevelTables(html) {
21498
21921
  const result = [];
@@ -21587,13 +22010,19 @@ function dataCellSpec(opts) {
21587
22010
  function normalizeAnchor(s) {
21588
22011
  return s.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "").slice(0, 24);
21589
22012
  }
21590
- function takeProfile(remap, rows, cols, anchor, seq) {
22013
+ function normalizeRowAnchor(cells) {
22014
+ const j = cells.map(normalizeAnchor).join("|");
22015
+ return /[\p{L}\p{N}]/u.test(j) ? j.slice(0, 64) : "";
22016
+ }
22017
+ function takeProfile(remap, rows, cols, anchor, seq, rowAnchor = "") {
21591
22018
  if (!remap) return null;
21592
22019
  for (const t of remap.tables) {
21593
22020
  if (t.used) continue;
21594
22021
  if (t.rows !== rows || t.cols !== cols) continue;
21595
22022
  if (t.anchor && anchor) {
21596
22023
  if (t.anchor !== anchor) continue;
22024
+ } else if (t.anchorRow && rowAnchor) {
22025
+ if (t.anchorRow !== rowAnchor) continue;
21597
22026
  } else if (t.index !== seq) {
21598
22027
  continue;
21599
22028
  }
@@ -21608,10 +22037,10 @@ function parseHu(s) {
21608
22037
  return Number.isFinite(n) ? n : void 0;
21609
22038
  }
21610
22039
  function edgeXml(tag, d) {
21611
- return d ? `<hh:${tag} type="${d.type}" width="${d.width}" color="${d.color}"/>` : `<hh:${tag} type="NONE" width="0.1 mm" color="#000000"/>`;
22040
+ return d ? `<hh:${tag} type="${escapeXml(d.type)}" width="${escapeXml(d.width)}" color="${escapeXml(d.color)}"/>` : `<hh:${tag} type="NONE" width="0.1 mm" color="#000000"/>`;
21612
22041
  }
21613
22042
  function borderFillDefToXml(id, def) {
21614
- const fill = def.fill?.faceColor ? `<hh:fillBrush><hh:winBrush faceColor="${def.fill.faceColor}" hatchColor="#000000" alpha="0"/></hh:fillBrush>` : "";
22043
+ const fill = def.fill?.faceColor ? `<hh:fillBrush><hh:winBrush faceColor="${escapeXml(def.fill.faceColor)}" hatchColor="#000000" alpha="0"/></hh:fillBrush>` : "";
21615
22044
  return ` <hh:borderFill id="${id}" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
21616
22045
  <hh:slash type="NONE" Crooked="0" isCounter="0"/>
21617
22046
  <hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
@@ -21622,28 +22051,45 @@ function borderFillDefToXml(id, def) {
21622
22051
  ${fill}` : ""}
21623
22052
  </hh:borderFill>`;
21624
22053
  }
21625
- var PROFILE_FONT_MAX = 2;
21626
- function profileCharPrXml(id, def) {
21627
- const height = parseHu(def.height_hwpunit) ?? 1e3;
21628
- const color = def.textColor ?? "#000000";
21629
- const rawFont = def.fontRef_hangul != null ? parseInt(def.fontRef_hangul, 10) || 0 : 0;
21630
- const font = rawFont >= 0 && rawFont <= PROFILE_FONT_MAX ? rawFont : 0;
22054
+ var LEGACY_PROFILE_FONT_MAX = 2;
22055
+ function profileCharPrXml(id, def, namedFontId) {
22056
+ const height = Math.max(parseHu(def.height_hwpunit) ?? 1e3, 100);
22057
+ const color = escapeXml(def.textColor ?? "#000000");
22058
+ let font = 0;
22059
+ let restFont = 0;
22060
+ if (namedFontId != null) {
22061
+ font = namedFontId;
22062
+ } else {
22063
+ const rawFont = def.fontRef_hangul != null ? parseInt(def.fontRef_hangul, 10) || 0 : 0;
22064
+ font = rawFont >= 0 && rawFont <= LEGACY_PROFILE_FONT_MAX ? rawFont : 0;
22065
+ restFont = font;
22066
+ }
21631
22067
  const boldAttr = def.bold ? ` bold="1"` : "";
21632
22068
  const italicAttr = def.italic ? ` italic="1"` : "";
21633
22069
  const underline = def.underline ? `
21634
22070
  <hh:underline type="BOTTOM" shape="SOLID" color="${color}"/>` : "";
21635
22071
  return ` <hh:charPr id="${id}" height="${height}" textColor="${color}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
21636
- <hh:fontRef hangul="${font}" latin="${font}" hanja="${font}" japanese="${font}" other="${font}" symbol="${font}" user="${font}"/>
22072
+ <hh:fontRef hangul="${font}" latin="${font}" hanja="${restFont}" japanese="${restFont}" other="${restFont}" symbol="${restFont}" user="${restFont}"/>
21637
22073
  <hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
21638
22074
  <hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
21639
22075
  <hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
21640
22076
  <hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>${underline}
21641
22077
  </hh:charPr>`;
21642
22078
  }
21643
- function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21644
- const remap = { borderFillXmls: [], charPrXmls: [], tables: [] };
22079
+ function buildProfileRemap(profile, charPrBase, borderFillBase = 3, fontBase = 3) {
22080
+ const remap = { borderFillXmls: [], charPrXmls: [], fontFaces: [], tables: [] };
21645
22081
  let bfNext = borderFillBase;
21646
22082
  let charNext = charPrBase;
22083
+ const fontIds = /* @__PURE__ */ new Map();
22084
+ const fontIdOf = (name) => {
22085
+ let id = fontIds.get(name);
22086
+ if (id == null) {
22087
+ id = fontBase + remap.fontFaces.length;
22088
+ remap.fontFaces.push(name);
22089
+ fontIds.set(name, id);
22090
+ }
22091
+ return id;
22092
+ };
21647
22093
  for (const t of profile.tables) {
21648
22094
  const localBf = {};
21649
22095
  for (const [key, def] of Object.entries(t.used_border_fills ?? {})) {
@@ -21654,7 +22100,7 @@ function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21654
22100
  const localChar = {};
21655
22101
  for (const [key, def] of Object.entries(t.used_char_prs ?? {})) {
21656
22102
  const gid = charNext++;
21657
- remap.charPrXmls.push(profileCharPrXml(gid, def));
22103
+ remap.charPrXmls.push(profileCharPrXml(gid, def, def.fontName_hangul ? fontIdOf(def.fontName_hangul) : void 0));
21658
22104
  localChar[key] = gid;
21659
22105
  }
21660
22106
  let colWidths;
@@ -21668,6 +22114,7 @@ function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21668
22114
  cols: t.cols,
21669
22115
  // 재정규화 — 추출기는 정규화해 담지만 손편집된 프로필 JSON도 같은 키 공간으로
21670
22116
  anchor: t.anchor_text ? normalizeAnchor(t.anchor_text) : void 0,
22117
+ anchorRow: t.anchor_row ? normalizeRowAnchor(t.anchor_row.split("|")) || void 0 : void 0,
21671
22118
  width: parseHu(t.width_hwpunit),
21672
22119
  colWidths,
21673
22120
  cellBf: /* @__PURE__ */ new Map(),
@@ -21685,17 +22132,208 @@ function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21685
22132
  const h = parseHu(cell2.height_hwpunit);
21686
22133
  if (h != null) tr.cellH.set(k, h);
21687
22134
  }
21688
- remap.tables.push(tr);
22135
+ remap.tables.push(tr);
22136
+ }
22137
+ return remap;
22138
+ }
22139
+
22140
+ // src/hwpx/gen-image.ts
22141
+ var PLACEHOLDER_BMP = Uint8Array.from([
22142
+ 66,
22143
+ 77,
22144
+ 58,
22145
+ 0,
22146
+ 0,
22147
+ 0,
22148
+ 0,
22149
+ 0,
22150
+ 0,
22151
+ 0,
22152
+ 54,
22153
+ 0,
22154
+ 0,
22155
+ 0,
22156
+ 40,
22157
+ 0,
22158
+ 0,
22159
+ 0,
22160
+ 1,
22161
+ 0,
22162
+ 0,
22163
+ 0,
22164
+ 1,
22165
+ 0,
22166
+ 0,
22167
+ 0,
22168
+ 1,
22169
+ 0,
22170
+ 24,
22171
+ 0,
22172
+ 0,
22173
+ 0,
22174
+ 0,
22175
+ 0,
22176
+ 4,
22177
+ 0,
22178
+ 0,
22179
+ 0,
22180
+ 19,
22181
+ 11,
22182
+ 0,
22183
+ 0,
22184
+ 19,
22185
+ 11,
22186
+ 0,
22187
+ 0,
22188
+ 0,
22189
+ 0,
22190
+ 0,
22191
+ 0,
22192
+ 0,
22193
+ 0,
22194
+ 0,
22195
+ 0,
22196
+ 255,
22197
+ 255,
22198
+ 255,
22199
+ 0
22200
+ ]);
22201
+ var PLACEHOLDER_PNG = Uint8Array.from([
22202
+ 137,
22203
+ 80,
22204
+ 78,
22205
+ 71,
22206
+ 13,
22207
+ 10,
22208
+ 26,
22209
+ 10,
22210
+ 0,
22211
+ 0,
22212
+ 0,
22213
+ 13,
22214
+ 73,
22215
+ 72,
22216
+ 68,
22217
+ 82,
22218
+ 0,
22219
+ 0,
22220
+ 0,
22221
+ 1,
22222
+ 0,
22223
+ 0,
22224
+ 0,
22225
+ 1,
22226
+ 8,
22227
+ 6,
22228
+ 0,
22229
+ 0,
22230
+ 0,
22231
+ 31,
22232
+ 21,
22233
+ 196,
22234
+ 137,
22235
+ 0,
22236
+ 0,
22237
+ 0,
22238
+ 11,
22239
+ 73,
22240
+ 68,
22241
+ 65,
22242
+ 84,
22243
+ 120,
22244
+ 156,
22245
+ 99,
22246
+ 0,
22247
+ 1,
22248
+ 0,
22249
+ 0,
22250
+ 5,
22251
+ 0,
22252
+ 1,
22253
+ 13,
22254
+ 10,
22255
+ 45,
22256
+ 180,
22257
+ 0,
22258
+ 0,
22259
+ 0,
22260
+ 0,
22261
+ 73,
22262
+ 69,
22263
+ 78,
22264
+ 68,
22265
+ 174,
22266
+ 66,
22267
+ 96,
22268
+ 130
22269
+ ]);
22270
+ var IMAGE_EXT_MIME = {
22271
+ png: "image/png",
22272
+ jpg: "image/jpeg",
22273
+ jpeg: "image/jpeg",
22274
+ gif: "image/gif",
22275
+ bmp: "image/bmp",
22276
+ tif: "image/tiff",
22277
+ tiff: "image/tiff"
22278
+ };
22279
+ var ImageRegistry = class {
22280
+ byUrl = /* @__PURE__ */ new Map();
22281
+ ids = /* @__PURE__ */ new Set();
22282
+ parts = [];
22283
+ picSeq = 0;
22284
+ /** url 등록(중복 시 기존 파트) — 수용 불가면 null */
22285
+ take(url) {
22286
+ const cached = this.byUrl.get(url);
22287
+ if (cached !== void 0) return cached;
22288
+ let part = null;
22289
+ const m = /^([A-Za-z0-9._-]+)\.([A-Za-z0-9]+)$/.exec(url);
22290
+ const mime = m ? IMAGE_EXT_MIME[m[2].toLowerCase()] : void 0;
22291
+ if (m && mime && !url.includes("..")) {
22292
+ let itemId = m[1].replace(/[^A-Za-z0-9_]/g, "_");
22293
+ let n = 1;
22294
+ while (this.ids.has(itemId)) itemId = `${m[1].replace(/[^A-Za-z0-9_]/g, "_")}_${n++}`;
22295
+ this.ids.add(itemId);
22296
+ part = {
22297
+ name: `BinData/${url}`,
22298
+ itemId,
22299
+ mime,
22300
+ data: m[2].toLowerCase() === "bmp" ? PLACEHOLDER_BMP : PLACEHOLDER_PNG
22301
+ };
22302
+ this.parts.push(part);
22303
+ }
22304
+ this.byUrl.set(url, part);
22305
+ return part;
21689
22306
  }
21690
- return remap;
22307
+ /** manifest <opf:item> 조각들 (실측: isEmbeded="1") */
22308
+ manifestItems() {
22309
+ return this.parts.map((p) => `<opf:item id="${p.itemId}" href="${p.name}" media-type="${p.mime}" isEmbeded="1"/>`);
22310
+ }
22311
+ /** 인라인 <hp:pic> XML — 실측 저장본 미러, treatAsChar=1 (셀·문단 안 배치) */
22312
+ inlinePicXml(part) {
22313
+ const id = PIC_ID_BASE + ++this.picSeq;
22314
+ const s = 1130;
22315
+ return `<hp:pic id="${id}" zOrder="0" numberingType="PICTURE" textWrap="TOP_AND_BOTTOM" textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" href="" groupLevel="0" instid="${id}" reverse="0" xmlns:hc="http://www.hancom.co.kr/hwpml/2011/core"><hp:offset x="0" y="0"/><hp:orgSz width="${s}" height="${s}"/><hp:curSz width="${s}" height="${s}"/><hp:flip horizontal="0" vertical="0"/><hp:rotationInfo angle="0" centerX="${s / 2}" centerY="${s / 2}" rotateimage="1"/><hp:renderingInfo><hc:transMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/><hc:scaMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/><hc:rotMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/></hp:renderingInfo><hp:imgRect><hc:pt0 x="0" y="0"/><hc:pt1 x="${s}" y="0"/><hc:pt2 x="${s}" y="${s}"/><hc:pt3 x="0" y="${s}"/></hp:imgRect><hp:imgClip left="0" right="${s}" top="0" bottom="${s}"/><hp:inMargin left="0" right="0" top="0" bottom="0"/><hp:imgDim dimwidth="${s}" dimheight="${s}"/><hc:img binaryItemIDRef="${part.itemId}" bright="0" contrast="0" effect="REAL_PIC" alpha="0"/><hp:effects/><hp:sz width="${s}" widthRelTo="ABSOLUTE" height="${s}" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="0" right="0" top="0" bottom="0"/></hp:pic>`;
22316
+ }
22317
+ };
22318
+ var MD_IMAGE_RE = /!\[[^\]]*\]\(([^)\s]+)\)/g;
22319
+ function splitImageRefs(text) {
22320
+ const urls = [];
22321
+ const out = text.replace(MD_IMAGE_RE, (_, url) => {
22322
+ urls.push(url);
22323
+ return "";
22324
+ });
22325
+ return { text: out, urls };
21691
22326
  }
21692
22327
 
21693
22328
  // src/hwpx/gen-table.ts
21694
- var TABLE_ID_BASE = 1e3;
22329
+ var TABLE_ID_BASE = DATA_TABLE_ID_BASE;
21695
22330
  var tableIdCounter = TABLE_ID_BASE;
21696
22331
  function nextTableId() {
21697
22332
  return ++tableIdCounter;
21698
22333
  }
22334
+ function resetTableIds() {
22335
+ tableIdCounter = TABLE_ID_BASE;
22336
+ }
21699
22337
  var DATA_TABLE_INSET = 1800;
21700
22338
  function anchorOfMarkdownCell(cell2) {
21701
22339
  return normalizeAnchor(cell2.replace(/!\[[^\]]*\]\([^)]*\)/g, ""));
@@ -21714,7 +22352,7 @@ function profileColWidths(tp, colCnt) {
21714
22352
  var CELL_PAD = 1200;
21715
22353
  function cellContentWidth(text, charHeight) {
21716
22354
  let max = 0;
21717
- for (const seg of text.replace(/\*\*|__|`/g, "").split(/<br\s*\/?>/i)) {
22355
+ for (const seg of text.replace(/!\[[^\]]*\]\([^)\s]+\)/g, "").replace(/\*\*|__|`/g, "").split(/<br\s*\/?>/i)) {
21718
22356
  const w = measureTextWidth(seg.trim(), charHeight, 100);
21719
22357
  if (w > max) max = w;
21720
22358
  }
@@ -21722,7 +22360,7 @@ function cellContentWidth(text, charHeight) {
21722
22360
  }
21723
22361
  function cellMinWordWidth(text, charHeight) {
21724
22362
  let max = 0;
21725
- for (const seg of text.replace(/\*\*|__|`/g, "").split(/<br\s*\/?>/i)) {
22363
+ for (const seg of text.replace(/!\[[^\]]*\]\([^)\s]+\)/g, "").replace(/\*\*|__|`/g, "").split(/<br\s*\/?>/i)) {
21726
22364
  for (const word of seg.trim().split(/\s+/)) {
21727
22365
  const w = measureTextWidth(word, charHeight, 100);
21728
22366
  if (w > max) max = w;
@@ -21752,7 +22390,7 @@ function computeColWidths(colMax, totalWidth, colMinWord = []) {
21752
22390
  const sumFreeRaw = [...free].reduce((a, i) => a + raw[i], 0);
21753
22391
  if (budget > sumFreeRaw * 1.6) {
21754
22392
  const sumRaw = raw.reduce((a, b) => a + b, 0);
21755
- for (let i = 0; i < colCnt; i++) widths[i] = raw[i] + Math.floor(raw[i] / sumRaw * (totalWidth - sumRaw));
22393
+ for (let i = 0; i < colCnt; i++) widths[i] = Math.min(cap, Math.round(raw[i]) + Math.floor(raw[i] / sumRaw * (totalWidth - sumRaw)));
21756
22394
  free.clear();
21757
22395
  } else {
21758
22396
  for (; ; ) {
@@ -21773,7 +22411,23 @@ function computeColWidths(colMax, totalWidth, colMinWord = []) {
21773
22411
  }
21774
22412
  let rem = totalWidth - widths.reduce((a, b) => a + b, 0);
21775
22413
  const order = [...raw.keys()].sort((a, b) => raw[b] - raw[a]);
21776
- for (let k = 0; rem > 0; k = (k + 1) % colCnt, rem--) widths[order[k]]++;
22414
+ for (let k = 0, skipped = 0; rem > 0; k = (k + 1) % colCnt, rem--) {
22415
+ while (widths[order[k]] >= cap && skipped < colCnt) {
22416
+ skipped++;
22417
+ k = (k + 1) % colCnt;
22418
+ }
22419
+ skipped = 0;
22420
+ widths[order[k]]++;
22421
+ }
22422
+ for (let k = 0, spins = 0; rem < 0; k = (k + 1) % colCnt, rem++) {
22423
+ while (widths[order[k]] <= 1 && spins < colCnt) {
22424
+ spins++;
22425
+ k = (k + 1) % colCnt;
22426
+ }
22427
+ if (widths[order[k]] <= 1) break;
22428
+ spins = 0;
22429
+ widths[order[k]]--;
22430
+ }
21777
22431
  return widths;
21778
22432
  }
21779
22433
  function estimateRowHeight(cells, widths, charHeight) {
@@ -21788,13 +22442,14 @@ function estimateRowHeight(cells, widths, charHeight) {
21788
22442
  });
21789
22443
  return maxLines * Math.round(charHeight * 1.6) + 282;
21790
22444
  }
21791
- function generateTable(rows, theme, style = null, remap = null, seq = 0) {
22445
+ function generateTable(rows, theme, style = null, remap = null, seq = 0, images = null) {
21792
22446
  const rowCnt = rows.length;
21793
22447
  const colCnt = Math.max(...rows.map((r) => r.length), 1);
21794
22448
  const reg = style?.bfRegistry ?? null;
21795
22449
  const totalW = style ? reg ? style.totalWidth - DATA_TABLE_INSET : style.totalWidth : 44e3;
21796
22450
  const measureH = style?.charHeight ?? 1e3;
21797
- const prof = takeProfile(remap, rowCnt, colCnt, anchorOfMarkdownCell(rows[0]?.[0] ?? ""), seq);
22451
+ const rowAnchor = normalizeRowAnchor((rows[0] ?? []).map((c) => c.replace(/!\[[^\]]*\]\([^)]*\)/g, "")));
22452
+ const prof = takeProfile(remap, rowCnt, colCnt, anchorOfMarkdownCell(rows[0]?.[0] ?? ""), seq, rowAnchor);
21798
22453
  const colMax = Array(colCnt).fill(0);
21799
22454
  const colMaxBody = Array(colCnt).fill(0);
21800
22455
  const colMinWord = Array(colCnt).fill(0);
@@ -21826,8 +22481,23 @@ function generateTable(rows, theme, style = null, remap = null, seq = 0) {
21826
22481
  const centered = isHeaderRow || colCentered[colIdx];
21827
22482
  const paraPrId = style ? centered ? style.tblCenterParaPr ?? style.centerParaPr : reg ? style.tblLeftParaPr ?? PARA_NORMAL : PARA_NORMAL : PARA_NORMAL;
21828
22483
  const p = cell2.split(/<br\s*\/?>/i).map((seg) => {
22484
+ let picRuns = "";
22485
+ if (images) {
22486
+ const { text: rest, urls } = splitImageRefs(seg);
22487
+ if (urls.length > 0) {
22488
+ const pics = urls.map((u) => {
22489
+ const part = images.take(u);
22490
+ return part ? images.inlinePicXml(part) : null;
22491
+ });
22492
+ if (pics.every(Boolean)) {
22493
+ seg = rest;
22494
+ picRuns = `<hp:run charPrIDRef="${cellCharPr}">${pics.join("")}</hp:run>`;
22495
+ }
22496
+ }
22497
+ }
21829
22498
  const runs = generateRuns(seg, cellCharPr, prof?.cellChar.has(k) ? void 0 : mapId);
21830
- return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0">${runs || `<hp:run charPrIDRef="${cellCharPr}"><hp:t></hp:t></hp:run>`}</hp:p>`;
22499
+ const body = (runs || (picRuns ? "" : `<hp:run charPrIDRef="${cellCharPr}"><hp:t></hp:t></hp:run>`)) + picRuns;
22500
+ return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0">${body}</hp:p>`;
21831
22501
  }).join("");
21832
22502
  const bf = prof?.cellBf.get(k) ?? (reg ? reg.get(dataCellSpec({
21833
22503
  row: rowIdx,
@@ -21869,23 +22539,37 @@ function layoutHtmlRows(rows) {
21869
22539
  colCnt = Math.max(colCnt, c);
21870
22540
  }
21871
22541
  }
22542
+ for (let r = 0; r < rows.length; r++) {
22543
+ for (let c = 0; c < colCnt; c++) {
22544
+ if (!occupied.has(`${r},${c}`)) {
22545
+ placed.push({ r, c, colSpan: 1, rowSpan: 1, inner: "", isHeader: rows[r].tag === "th" });
22546
+ occupied.add(`${r},${c}`);
22547
+ }
22548
+ }
22549
+ }
22550
+ placed.sort((a, b) => a.r - b.r || a.c - b.c);
21872
22551
  return { placed, rowCnt: rows.length, colCnt };
21873
22552
  }
21874
22553
  function unescapeHtml(s) {
21875
22554
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
21876
22555
  }
21877
- function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, remap = null, seq = 0) {
22556
+ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, remap = null, seq = 0, images = null) {
21878
22557
  const rows = parseHtmlTable(rawHtml);
21879
22558
  if (!rows || rows.length === 0) return null;
21880
22559
  const { placed, rowCnt, colCnt } = layoutHtmlRows(rows);
21881
22560
  if (rowCnt === 0 || colCnt === 0) return null;
21882
22561
  const measureH = style?.charHeight ?? 1e3;
21883
22562
  const first = placed.find((p) => p.r === 0 && p.c === 0) ?? placed[0];
21884
- const prof = takeProfile(remap, rowCnt, colCnt, first ? anchorOfHtmlCell(first.inner) : "", seq);
22563
+ const row0 = Array.from({ length: colCnt }, () => "");
22564
+ for (const cell2 of placed) {
22565
+ if (cell2.r === 0 && cell2.c < colCnt) row0[cell2.c] = anchorOfHtmlCell(cell2.inner);
22566
+ }
22567
+ const prof = takeProfile(remap, rowCnt, colCnt, first ? anchorOfHtmlCell(first.inner) : "", seq, normalizeRowAnchor(row0));
21885
22568
  const colMax = Array(colCnt).fill(0);
21886
22569
  const colMaxBody = Array(colCnt).fill(0);
21887
22570
  const colMinWord = Array(colCnt).fill(0);
21888
- const cellLines = placed.map((cell2) => htmlCellInnerToLines(cell2.inner).lines);
22571
+ const cellParsed = placed.map((cell2) => htmlCellInnerToLines(cell2.inner));
22572
+ const cellLines = cellParsed.map((p) => p.lines);
21889
22573
  placed.forEach((cell2, i) => {
21890
22574
  const w = Math.max(...cellLines[i].map((l) => measureTextWidth(unescapeHtml(l).trim(), measureH, 100)), 0) / cell2.colSpan;
21891
22575
  const mw = cell2.colSpan === 1 ? Math.max(...cellLines[i].map((l) => cellMinWordWidth(unescapeHtml(l), measureH)), 0) : 0;
@@ -21906,7 +22590,33 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
21906
22590
  const reg = style?.bfRegistry ?? null;
21907
22591
  let htmlHeaderRows = 0;
21908
22592
  while (htmlHeaderRows < rows.length && rows[htmlHeaderRows].tag === "th") htmlHeaderRows++;
21909
- const tcXmls = placed.map((cell2, i) => {
22593
+ const tableRowHeights = Array(rowCnt).fill(cellH);
22594
+ const meta = placed.map((cell2, i) => {
22595
+ const rowSpan = Math.min(cell2.rowSpan, rowCnt - cell2.r);
22596
+ const lines = cellLines[i];
22597
+ let nestedH = 0;
22598
+ const nestedXmls = [];
22599
+ for (const nested of extractTopLevelTables(cell2.inner)) {
22600
+ const sw = spanW(cell2);
22601
+ const nestedW = Math.max(Math.min(Math.max(sw - 1020, 4e3), sw - 282), 500);
22602
+ const nestedXml = generateHtmlTableXml(nested, theme, nestedW, style ? { ...style, totalWidth: nestedW } : null);
22603
+ if (nestedXml) {
22604
+ nestedXmls.push(nestedXml);
22605
+ const szH = nestedXml.match(/<hp:sz [^>]*height="(\d+)"/)?.[1];
22606
+ nestedH += (szH ? Number(szH) : (nested.match(/<tr[\s>]/gi) ?? []).length * cellH) + 300;
22607
+ }
22608
+ }
22609
+ const usable = Math.max(spanW(cell2) - CELL_PAD, 1e3);
22610
+ let wrapLines = 0;
22611
+ for (const line of lines) wrapLines += Math.max(1, Math.ceil(measureTextWidth(unescapeHtml(line).trim(), measureH, 100) / usable));
22612
+ const lineH = style ? Math.round(measureH * 1.6) : 800;
22613
+ const contentH = Math.max(cellH * rowSpan, Math.max(wrapLines, 1) * lineH + nestedH);
22614
+ const cellHeight = Math.max(prof?.cellH.get(`${cell2.r},${cell2.c}`) ?? 0, contentH);
22615
+ const perRow = Math.ceil(cellHeight / rowSpan);
22616
+ for (let r = cell2.r; r < cell2.r + rowSpan; r++) tableRowHeights[r] = Math.max(tableRowHeights[r], perRow);
22617
+ return { cell: cell2, rowSpan, lines, nestedXmls, imgSrcs: cellParsed[i].imgSrcs };
22618
+ });
22619
+ const tcXmls = meta.map(({ cell: cell2, rowSpan, lines, nestedXmls, imgSrcs }) => {
21910
22620
  const k = `${cell2.r},${cell2.c}`;
21911
22621
  const isHeader = cell2.isHeader;
21912
22622
  const baseCharPr = style ? style.charPr : CHAR_NORMAL;
@@ -21915,27 +22625,42 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
21915
22625
  const charPrId = prof?.cellChar.get(k) ?? defaultCharPr;
21916
22626
  const centered = isHeader || colCentered[cell2.c];
21917
22627
  const paraPrId = style ? centered ? reg ? style.tblCenterParaPr ?? style.centerParaPr : style.centerParaPr : reg ? style.tblLeftParaPr ?? PARA_NORMAL : PARA_NORMAL : PARA_NORMAL;
21918
- const lines = cellLines[i];
21919
- const paras = lines.map(
21920
- (line) => `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t>${escapeXml(unescapeHtml(line))}</hp:t></hp:run></hp:p>`
21921
- );
21922
- let nestedH = 0;
21923
- for (const nested of extractTopLevelTables(cell2.inner)) {
21924
- const nestedW = Math.max(spanW(cell2) - 1020, 4e3);
21925
- const nestedXml = generateHtmlTableXml(nested, theme, nestedW, style ? { ...style, totalWidth: nestedW } : null);
21926
- if (nestedXml) {
21927
- paras.push(`<hp:p paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0">${nestedXml}</hp:run></hp:p>`);
21928
- nestedH += (nested.match(/<tr[\s>]/gi) ?? []).length * cellH + 300;
22628
+ const picUrls = images ? [...imgSrcs] : [];
22629
+ const paras = [];
22630
+ for (const line of lines) {
22631
+ let text = unescapeHtml(line);
22632
+ if (images) {
22633
+ const { text: rest, urls } = splitImageRefs(text);
22634
+ if (urls.length > 0) {
22635
+ picUrls.push(...urls);
22636
+ text = rest;
22637
+ }
22638
+ }
22639
+ if (!images || text.trim() || picUrls.length === 0) {
22640
+ paras.push(`<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`);
22641
+ }
22642
+ }
22643
+ if (images && picUrls.length > 0) {
22644
+ const pics = picUrls.map((u) => {
22645
+ const part = images.take(u);
22646
+ return part ? images.inlinePicXml(part) : null;
22647
+ }).filter(Boolean);
22648
+ if (pics.length > 0) {
22649
+ paras.push(`<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}">${pics.join("")}</hp:run></hp:p>`);
21929
22650
  }
21930
22651
  }
22652
+ for (const nestedXml of nestedXmls) {
22653
+ paras.push(`<hp:p paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0">${nestedXml}</hp:run></hp:p>`);
22654
+ }
21931
22655
  if (paras.length === 0) {
21932
22656
  paras.push(`<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t></hp:t></hp:run></hp:p>`);
21933
22657
  }
21934
- const contentH = Math.max(cellH * cell2.rowSpan, Math.max(lines.length, 1) * (style ? Math.round(measureH * 1.6) : 800) + nestedH);
21935
- const cellHeight = Math.max(prof?.cellH.get(k) ?? 0, contentH);
22658
+ let cellHeight = 0;
22659
+ for (let r = cell2.r; r < cell2.r + rowSpan; r++) cellHeight += tableRowHeights[r];
22660
+ const rowEnd = cell2.r + rowSpan - 1;
21936
22661
  const bf = prof?.cellBf.get(k) ?? (reg ? reg.get(dataCellSpec({
21937
22662
  row: cell2.r,
21938
- rowEnd: cell2.r + cell2.rowSpan - 1,
22663
+ rowEnd,
21939
22664
  col: cell2.c,
21940
22665
  colEnd: cell2.c + cell2.colSpan - 1,
21941
22666
  rowCnt,
@@ -21943,31 +22668,35 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
21943
22668
  headerRows: htmlHeaderRows,
21944
22669
  fill: isHeader ? style.headerFill ?? "#E6E6E6" : void 0
21945
22670
  })) : style && isHeader ? style.headerBf : 2);
21946
- return `<hp:tc name="" header="${isHeader ? 1 : 0}" hasMargin="0" protect="0" editable="1" dirty="0" borderFillIDRef="${bf}"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="${style ? "CENTER" : "TOP"}" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0">${paras.join("")}</hp:subList><hp:cellAddr colAddr="${cell2.c}" rowAddr="${cell2.r}"/><hp:cellSpan colSpan="${cell2.colSpan}" rowSpan="${cell2.rowSpan}"/><hp:cellSz width="${spanW(cell2)}" height="${cellHeight}"/><hp:cellMargin left="141" right="141" top="141" bottom="141"/></hp:tc>`;
22671
+ return `<hp:tc name="" header="${isHeader ? 1 : 0}" hasMargin="0" protect="0" editable="1" dirty="0" borderFillIDRef="${bf}"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="${style ? "CENTER" : "TOP"}" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0">${paras.join("")}</hp:subList><hp:cellAddr colAddr="${cell2.c}" rowAddr="${cell2.r}"/><hp:cellSpan colSpan="${cell2.colSpan}" rowSpan="${rowSpan}"/><hp:cellSz width="${spanW(cell2)}" height="${cellHeight}"/><hp:cellMargin left="141" right="141" top="141" bottom="141"/></hp:tc>`;
21947
22672
  });
21948
22673
  const trXmls = [];
21949
22674
  for (let r = 0; r < rowCnt; r++) {
21950
22675
  const rowTcs = tcXmls.filter((_, i) => placed[i].r === r);
21951
22676
  trXmls.push(`<hp:tr>${rowTcs.join("")}</hp:tr>`);
21952
22677
  }
21953
- return `<hp:tbl id="${tblId}" zOrder="0" numberingType="TABLE" pageBreak="CELL" repeatHeader="${style ? 1 : 0}" rowCnt="${rowCnt}" colCnt="${colCnt}" cellSpacing="0" borderFillIDRef="2" noShading="0"><hp:sz width="${tblW}" widthRelTo="ABSOLUTE" height="${cellH * rowCnt}" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="0" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="0" right="0" top="0" bottom="0"/><hp:inMargin left="510" right="510" top="141" bottom="141"/>` + trXmls.join("") + `</hp:tbl>`;
22678
+ const tableH = tableRowHeights.reduce((sum, height) => sum + height, 0);
22679
+ return `<hp:tbl id="${tblId}" zOrder="0" numberingType="TABLE" pageBreak="CELL" repeatHeader="${style ? 1 : 0}" rowCnt="${rowCnt}" colCnt="${colCnt}" cellSpacing="0" borderFillIDRef="2" noShading="0"><hp:sz width="${tblW}" widthRelTo="ABSOLUTE" height="${tableH}" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="0" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="0" right="0" top="0" bottom="0"/><hp:inMargin left="510" right="510" top="141" bottom="141"/>` + trXmls.join("") + `</hp:tbl>`;
21954
22680
  }
21955
22681
 
21956
22682
  // src/hwpx/gen-gongmun-extra.ts
21957
- var extraTableId = 93e5;
22683
+ var extraTableId = EXTRA_TABLE_ID_BASE;
22684
+ function resetExtraTableIds() {
22685
+ extraTableId = EXTRA_TABLE_ID_BASE;
22686
+ }
21958
22687
  function tbl(rows, w, h, cols) {
21959
22688
  return `<hp:tbl id="${++extraTableId}" zOrder="0" numberingType="TABLE" textWrap="TOP_AND_BOTTOM" textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL" repeatHeader="0" rowCnt="${rows.length}" colCnt="${cols}" cellSpacing="0" borderFillIDRef="1" noAdjust="1"><hp:sz width="${w}" widthRelTo="ABSOLUTE" height="${h}" heightRelTo="ABSOLUTE" protect="0"/><hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/><hp:outMargin left="283" right="283" top="283" bottom="283"/><hp:inMargin left="141" right="141" top="141" bottom="141"/>` + rows.map((r) => `<hp:tr>${r}</hp:tr>`).join("") + `</hp:tbl>`;
21960
22689
  }
21961
22690
  function tc(opts) {
21962
- return `<hp:tc name="" header="0" hasMargin="0" protect="0" editable="1" dirty="0" borderFillIDRef="${opts.bf}"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0">${opts.paras}</hp:subList><hp:cellAddr colAddr="${opts.col}" rowAddr="${opts.row}"/><hp:cellSpan colSpan="${opts.colSpan ?? 1}" rowSpan="1"/><hp:cellSz width="${opts.w}" height="${opts.h}"/><hp:cellMargin left="141" right="141" top="141" bottom="141"/></hp:tc>`;
22691
+ return `<hp:tc name="${opts.name ?? ""}" header="0" hasMargin="0" protect="0" editable="1" dirty="0" borderFillIDRef="${opts.bf}"><hp:subList id="" textDirection="HORIZONTAL" lineWrap="BREAK" vertAlign="CENTER" linkListIDRef="0" linkListNextIDRef="0" textWidth="0" textHeight="0" hasTextRef="0" hasNumRef="0">${opts.paras}</hp:subList><hp:cellAddr colAddr="${opts.col}" rowAddr="${opts.row}"/><hp:cellSpan colSpan="${opts.colSpan ?? 1}" rowSpan="1"/><hp:cellSz width="${opts.w}" height="${opts.h}"/><hp:cellMargin left="141" right="141" top="141" bottom="141"/></hp:tc>`;
21963
22692
  }
21964
22693
  function para(text, paraPrId, charPrId) {
21965
22694
  return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`;
21966
22695
  }
21967
22696
  var APPROVAL_COL_W = 7085;
21968
- var APPROVAL_LABEL_H = 1100;
22697
+ var APPROVAL_LABEL_H = 1300;
21969
22698
  var APPROVAL_SIGN_H = 4200;
21970
- function buildApprovalTable(labels, gongmun, reg) {
22699
+ function buildApprovalTable(labels, reg, approvalCharPr) {
21971
22700
  const n = labels.length;
21972
22701
  const w = APPROVAL_COL_W * n;
21973
22702
  const bfAt = (row, col) => reg.get({
@@ -21977,31 +22706,31 @@ function buildApprovalTable(labels, gongmun, reg) {
21977
22706
  r: col === n - 1 ? "thick" : "thin"
21978
22707
  });
21979
22708
  const labelRow = labels.map(
21980
- (label, c) => tc({ bf: bfAt(0, c), row: 0, col: c, w: APPROVAL_COL_W, h: APPROVAL_LABEL_H, paras: para(label, GONGMUN_TBL_CENTER, CHAR_NORMAL) })
22709
+ (label, c) => tc({ bf: bfAt(0, c), row: 0, col: c, w: APPROVAL_COL_W, h: APPROVAL_LABEL_H, paras: para(label, GONGMUN_PARA_APPROVAL, approvalCharPr) })
21981
22710
  ).join("");
21982
22711
  const signRow = labels.map(
21983
- (_, c) => tc({ bf: bfAt(1, c), row: 1, col: c, w: APPROVAL_COL_W, h: APPROVAL_SIGN_H, paras: para("", GONGMUN_TBL_CENTER, CHAR_NORMAL) })
22712
+ (_, c) => tc({ bf: bfAt(1, c), row: 1, col: c, w: APPROVAL_COL_W, h: APPROVAL_SIGN_H, paras: para("", GONGMUN_PARA_APPROVAL, approvalCharPr) })
21984
22713
  ).join("");
21985
22714
  const table2 = tbl([labelRow, signRow], w, APPROVAL_LABEL_H + APPROVAL_SIGN_H, n);
21986
22715
  return `<hp:p paraPrIDRef="${GONGMUN_RIGHT}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${table2}</hp:run></hp:p>`;
21987
22716
  }
21988
22717
  function hasEndMark(lastText) {
21989
- return /끝\s*\.?\s*$/.test(lastText.trim());
22718
+ return /(^|\s)끝\s*\.\s*$/.test(lastText.trim());
21990
22719
  }
21991
22720
  function buildEndMark() {
21992
22721
  return para(" \uB05D.", PARA_NORMAL, CHAR_NORMAL);
21993
22722
  }
21994
22723
  var TITLE_BOX = { barH: 382, titleH: 2850, color: "#0080C0", gradient: ["#0080C0", "#3CBFFF"] };
21995
- function buildTitleBox(title, titleCharPr, bodyWidth, reg) {
22724
+ function buildTitleBox(title, titleCharPr, barCharPr, bodyWidth, reg) {
21996
22725
  const w = bodyWidth - 280;
21997
22726
  const outer = { t: "thin", b: "thin", l: "thin", r: "thin" };
21998
22727
  const barTop = reg.get({ ...outer, fill: TITLE_BOX.color });
21999
22728
  const barBottom = reg.get({ ...outer, fill: { gradient: TITLE_BOX.gradient } });
22000
22729
  const mid = reg.get(outer);
22001
- const barPara = `<hp:p paraPrIDRef="${GONGMUN_TBL_CENTER}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}"><hp:t></hp:t></hp:run></hp:p>`;
22730
+ const barPara = para("", GJ_PARA_BAR, barCharPr);
22002
22731
  const rows = [
22003
22732
  tc({ bf: barTop, row: 0, col: 0, w, h: TITLE_BOX.barH, paras: barPara }),
22004
- tc({ bf: mid, row: 1, col: 0, w, h: TITLE_BOX.titleH, paras: para(title, GONGMUN_TBL_CENTER, titleCharPr) }),
22733
+ tc({ bf: mid, row: 1, col: 0, w, h: TITLE_BOX.titleH, paras: para(title, GONGMUN_TBL_CENTER, titleCharPr), name: "__kordoc_h1" }),
22005
22734
  tc({ bf: barBottom, row: 2, col: 0, w, h: TITLE_BOX.barH, paras: barPara })
22006
22735
  ];
22007
22736
  const table2 = tbl(rows, w, TITLE_BOX.barH * 2 + TITLE_BOX.titleH, 1);
@@ -22015,8 +22744,8 @@ function docframeIds(base) {
22015
22744
  function docframeActive(g) {
22016
22745
  return !!(g.docHead || g.docFoot || g.reportInfo || g.noticeHead || g.press);
22017
22746
  }
22018
- function docframeCharPrXmls(base, measured) {
22019
- const body = measured ? 4 : 0;
22747
+ function docframeCharPrXmls(base, richAssets) {
22748
+ const body = richAssets ? 4 : 0;
22020
22749
  const ids = docframeIds(base);
22021
22750
  return [
22022
22751
  charPr(ids.org, 1800, true, false, body),
@@ -22048,7 +22777,10 @@ function buildDocHead(g, ids) {
22048
22777
  out.push(blank());
22049
22778
  return out;
22050
22779
  }
22051
- var FOOT_SEP = "\u2500".repeat(46);
22780
+ function footSep(g) {
22781
+ const bodyMm = 210 - g.margins.left - g.margins.right;
22782
+ return "\u2500".repeat(Math.max(10, Math.round(bodyMm * (46 / 175))));
22783
+ }
22052
22784
  function buildDocFoot(g, ids) {
22053
22785
  const f = g.docFoot;
22054
22786
  const out = [blank()];
@@ -22056,7 +22788,7 @@ function buildDocFoot(g, ids) {
22056
22788
  out.push(para(f.sender, GONGMUN_CENTER, ids.sender), blank());
22057
22789
  }
22058
22790
  const foot = (text) => para(text, GONGMUN_TBL_LEFT, ids.foot);
22059
- out.push(foot(FOOT_SEP));
22791
+ out.push(foot(footSep(g)));
22060
22792
  if (f.drafter || f.reviewer || f.approver)
22061
22793
  out.push(foot(`\uAE30\uC548\uC790 ${f.drafter ?? ""} \uAC80\uD1A0\uC790 ${f.reviewer ?? ""} \uACB0\uC7AC\uAD8C\uC790 ${f.approver ?? ""}`));
22062
22794
  if (f.cooperator) out.push(foot(`\uD611\uC870\uC790 ${f.cooperator}`));
@@ -22205,6 +22937,54 @@ var COMMAND_MAP = {
22205
22937
  vdots: "VDOTS",
22206
22938
  ddots: "DDOTS"
22207
22939
  };
22940
+ var EQEDIT_FUNCTIONS = /* @__PURE__ */ new Set([
22941
+ "sin",
22942
+ "cos",
22943
+ "tan",
22944
+ "cot",
22945
+ "sec",
22946
+ "csc",
22947
+ "arcsin",
22948
+ "arccos",
22949
+ "arctan",
22950
+ "sinh",
22951
+ "cosh",
22952
+ "tanh",
22953
+ "coth",
22954
+ "log",
22955
+ "ln",
22956
+ "exp",
22957
+ "det",
22958
+ "gcd",
22959
+ "mod",
22960
+ "max",
22961
+ "min",
22962
+ "arg",
22963
+ "deg",
22964
+ "hom",
22965
+ "ker",
22966
+ "Pr"
22967
+ ]);
22968
+ {
22969
+ const reverse = /* @__PURE__ */ new Map();
22970
+ const prefer = (name, a, b) => {
22971
+ if (a === name) return a;
22972
+ if (b === name) return b;
22973
+ const aAlpha = /^[A-Za-z]+$/.test(a);
22974
+ const bAlpha = /^[A-Za-z]+$/.test(b);
22975
+ if (aAlpha !== bAlpha) return aAlpha ? a : b;
22976
+ return a < b ? a : b;
22977
+ };
22978
+ for (const [token, latex] of Object.entries(CONVERT_MAP)) {
22979
+ const m = /^\\([A-Za-z]+)$/.exec(latex);
22980
+ if (!m) continue;
22981
+ const name = m[1];
22982
+ if (name in COMMAND_MAP) continue;
22983
+ const prev = reverse.get(name);
22984
+ reverse.set(name, prev === void 0 ? token : prefer(name, token, prev));
22985
+ }
22986
+ for (const [name, token] of reverse) COMMAND_MAP[name] = token;
22987
+ }
22208
22988
  var ACCENT_COMMANDS = {
22209
22989
  bar: "bar",
22210
22990
  overline: "bar",
@@ -22298,8 +23078,20 @@ function readCommand(input, idx, depth) {
22298
23078
  const endIdx = input.indexOf(endTag, env.next);
22299
23079
  if (endIdx === -1) return { value: env.value, next: env.next };
22300
23080
  const body = convertLatexFragment(input.slice(env.next, endIdx), depth + 1);
22301
- if (env.value === "matrix" || env.value === "pmatrix" || env.value === "bmatrix") {
22302
- return { value: `{${env.value}{${body}}}`, next: endIdx + endTag.length };
23081
+ const ENV_TOKEN = {
23082
+ matrix: "matrix",
23083
+ pmatrix: "pmatrix",
23084
+ bmatrix: "bmatrix",
23085
+ vmatrix: "dmatrix",
23086
+ cases: "cases",
23087
+ align: "eqalign",
23088
+ "align*": "eqalign",
23089
+ aligned: "eqalign"
23090
+ };
23091
+ const tok = ENV_TOKEN[env.value];
23092
+ if (tok) return { value: `{${tok}{${body}}}`, next: endIdx + endTag.length };
23093
+ if (env.value === "Bmatrix") {
23094
+ return { value: `LEFT { {matrix{${body}}} RIGHT }`, next: endIdx + endTag.length };
22303
23095
  }
22304
23096
  return { value: body, next: endIdx + endTag.length };
22305
23097
  }
@@ -22331,7 +23123,11 @@ function readCommand(input, idx, depth) {
22331
23123
  const tok = readGroupOrToken(input, start, depth);
22332
23124
  return { value: `"${tok.value}"`, next: tok.next };
22333
23125
  }
22334
- return { value: COMMAND_MAP[command] ?? command, next: name.next };
23126
+ const mapped = COMMAND_MAP[command];
23127
+ if (mapped !== void 0) return { value: mapped, next: name.next };
23128
+ if (EQEDIT_FUNCTIONS.has(command)) return { value: command, next: name.next };
23129
+ if (/^[A-Za-z]{2,}$/.test(command)) return { value: `"${command}"`, next: name.next };
23130
+ return { value: command, next: name.next };
22335
23131
  }
22336
23132
  function convertLatexFragment(input, depth) {
22337
23133
  if (depth > MAX_GROUP_DEPTH) return normalizeEqEdit(input);
@@ -22411,48 +23207,52 @@ function generateSecPr(gongmun) {
22411
23207
  m.header = gongmun.headerFooter;
22412
23208
  m.footer = gongmun.headerFooter;
22413
23209
  }
22414
- const secPr = `<hp:secPr textDirection="HORIZONTAL" spaceColumns="1134" tabStop="8000" outlineShapeIDRef="1" memoShapeIDRef="0" textVerticalWidthHead="0" masterPageCnt="0"><hp:grid lineGrid="0" charGrid="0" wonggojiFormat="0"/><hp:startNum pageStartsOn="BOTH" page="0" pic="0" tbl="0" equation="0"/><hp:visibility hideFirstHeader="0" hideFirstFooter="0" hideFirstMasterPage="0" border="SHOW_ALL" fill="SHOW_ALL" hideFirstPageNum="0" hideFirstEmptyLine="0" showLineNumber="0"/><hp:pagePr landscape="WIDELY" width="59528" height="84188" gutterType="LEFT_ONLY"><hp:margin header="${m.header}" footer="${m.footer}" gutter="0" left="${m.left}" right="${m.right}" top="${m.top}" bottom="${m.bottom}"/></hp:pagePr><hp:footNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="-1" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="283" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="EACH_COLUMN" beneathText="0"/></hp:footNotePr><hp:endNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="14692344" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="0" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="END_OF_DOCUMENT" beneathText="0"/></hp:endNotePr></hp:secPr>`;
23210
+ const secPr = `<hp:secPr textDirection="HORIZONTAL" spaceColumns="1134" tabStop="8000" outlineShapeIDRef="1" memoShapeIDRef="0" textVerticalWidthHead="0" masterPageCnt="0"><hp:grid lineGrid="0" charGrid="0" wonggojiFormat="0"/><hp:startNum pageStartsOn="BOTH" page="0" pic="0" tbl="0" equation="0"/><hp:visibility hideFirstHeader="0" hideFirstFooter="0" hideFirstMasterPage="0" border="SHOW_ALL" fill="SHOW_ALL" hideFirstPageNum="0" hideFirstEmptyLine="0" showLineNumber="0"/><hp:pagePr landscape="WIDELY" width="${A4_W_HU}" height="${A4_H_HU}" gutterType="LEFT_ONLY"><hp:margin header="${m.header}" footer="${m.footer}" gutter="0" left="${m.left}" right="${m.right}" top="${m.top}" bottom="${m.bottom}"/></hp:pagePr><hp:footNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="-1" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="283" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="EACH_COLUMN" beneathText="0"/></hp:footNotePr><hp:endNotePr><hp:autoNumFormat type="DIGIT" userChar="" prefixChar="" suffixChar=")" supscript="0"/><hp:noteLine length="14692344" type="SOLID" width="0.12 mm" color="#000000"/><hp:noteSpacing betweenNotes="0" belowLine="567" aboveLine="850"/><hp:numbering type="CONTINUOUS" newNum="1"/><hp:placement place="END_OF_DOCUMENT" beneathText="0"/></hp:endNotePr></hp:secPr>`;
22415
23211
  const colPr = `<hp:ctrl><hp:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="1" sameSz="1" sameGap="0"/></hp:ctrl>`;
22416
23212
  return secPr + colPr + (gongmun?.pageNumbers ? pageNumCtrl() : "");
22417
23213
  }
22418
- function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null, fit = null, chartParts = null, bfReg = null, remap = null, dfIds = null) {
22419
- const paraXmls = [];
22420
- let isFirst = true;
22421
- let tableSeq = 0;
22422
- const orderedCounters = {};
22423
- let prevWasOrdered = false;
22424
- const gaejosik = gongmun?.preset === "gaejosik";
22425
- const measured = !!gongmun && usesReportFonts(gongmun.preset);
22426
- const vBase = charVariantBase(measured, !!gongmun);
22427
- const tableStyle = gongmun ? {
22428
- totalWidth: mmToHwpunit(210 - gongmun.margins.left - gongmun.margins.right),
22429
- // 셀 크기 — 실측: 실측 프리셋 맑은 고딕 12pt(22·23), 비실측도 12pt(11·12,
22430
- // 실결재 지배값 — 본문 크기 셀은 열폭 부족으로 서술 열 세로 신장, v4.0.2 QA)
22431
- charPr: measured ? GJ_CHAR_TABLE : GONGMUN_TBL_CHAR,
22432
- boldCharPr: measured ? GJ_CHAR_TABLE_BOLD : GONGMUN_TBL_CHAR_BOLD,
22433
- charHeight: measured ? gaejosikSizes(gongmun.bodyHeight, gongmun.sizes).table : GONGMUN_TBL_PT,
22434
- headerBf: gongmunTableHeaderBf(gaejosik),
22435
- centerParaPr: GONGMUN_CENTER,
22436
- tblCenterParaPr: GONGMUN_TBL_CENTER,
22437
- tblLeftParaPr: GONGMUN_TBL_LEFT,
22438
- bfRegistry: bfReg ?? void 0,
22439
- rightParaPr: GONGMUN_RIGHT
22440
- } : null;
22441
- const chamMap = (id) => id === CHAR_BOLD ? GJ_CHAR_CHAM_BOLD : id;
22442
- let chapterNo = 0;
22443
- let h2Seq = 0;
22444
- let coverH1Idx = -1;
22445
- let pendingPageBreak = false;
22446
- let pendingNewNum = false;
22447
- let titleBoxH1Idx = -1;
22448
- let pressH1Idx = -1;
22449
- let hasFrontPages = false;
23214
+ var SectionOpener = class {
23215
+ constructor(gongmun) {
23216
+ this.gongmun = gongmun;
23217
+ }
23218
+ pending = true;
23219
+ /** 아직 secPr를 나를 첫 문단이 안 나왔는가 */
23220
+ get isFirst() {
23221
+ return this.pending;
23222
+ }
23223
+ /** 일반 문단 XML의 run에 secPr 주입 (소비) */
23224
+ inject(xml) {
23225
+ this.pending = false;
23226
+ return xml.replace(
23227
+ /<hp:run charPrIDRef="(\d+)">/,
23228
+ `<hp:run charPrIDRef="$1">${generateSecPr(this.gongmun)}`
23229
+ );
23230
+ }
23231
+ /**
23232
+ * 표·차트·수식처럼 첫 run에 직접 못 싣는 블록 앞에 secPr 전용 빈 문단을 방출 (소비).
23233
+ * 이미 소비됐으면 아무것도 안 한다.
23234
+ */
23235
+ emitCarrier(paraXmls) {
23236
+ if (!this.pending) return;
23237
+ this.pending = false;
23238
+ const secRun = `<hp:run charPrIDRef="0">${generateSecPr(this.gongmun)}<hp:t></hp:t></hp:run>`;
23239
+ paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
23240
+ }
23241
+ /** 문서 폴백 — secPr를 실은 빈 단락 XML */
23242
+ emptyDoc() {
23243
+ this.pending = false;
23244
+ return `<hp:p paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0">${generateSecPr(this.gongmun)}<hp:t></hp:t></hp:run></hp:p>`;
23245
+ }
23246
+ };
23247
+ function buildPreamble(blocks, ctx) {
23248
+ const { gongmun, dfIds, bfReg, tableStyle, richAssets, gjBodyW } = ctx;
22450
23249
  const preamble = [];
23250
+ let hasFrontPages = false;
22451
23251
  if (gongmun?.reportInfo && dfIds) {
22452
23252
  preamble.push(buildReportInfo(gongmun, dfIds));
22453
23253
  }
22454
23254
  if (gongmun?.approval && bfReg) {
22455
- preamble.push(buildApprovalTable(gongmun.approval, gongmun, bfReg));
23255
+ preamble.push(buildApprovalTable(gongmun.approval, bfReg, richAssets ? GJ_CHAR_APPROVAL : GONGMUN_APPROVAL_CHAR));
22456
23256
  }
22457
23257
  if (gongmun?.docHead && dfIds) {
22458
23258
  preamble.push(...buildDocHead(gongmun, dfIds));
@@ -22463,273 +23263,342 @@ function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? prec
22463
23263
  if (gongmun?.press && dfIds && bfReg && tableStyle) {
22464
23264
  preamble.push(...buildPressHead(gongmun, dfIds, tableStyle.totalWidth, bfReg));
22465
23265
  }
22466
- if (gaejosik && gongmun) {
23266
+ if (gongmun && (gongmun.cover || gongmun.toc)) {
22467
23267
  const h1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
22468
- const chapters = blocks.filter((b) => b.type === "heading" && b.level === 2).map((b) => stripChapterNumber(b.text || ""));
22469
23268
  let coverTitle = "";
22470
23269
  const hide = (xml, hideHeader) => gongmun.pageNumbers ? xml.replace(/<hp:run charPrIDRef="(\d+)">/, `<hp:run charPrIDRef="$1">${pageHidingCtrl(hideHeader)}`) : xml;
22471
23270
  if (gongmun.cover && h1Idx >= 0) {
22472
23271
  coverTitle = (blocks[h1Idx].text || "").trim();
22473
- const coverPart = buildGaejosikCover(coverTitle, gongmun);
23272
+ const coverPart = buildGaejosikCover(coverTitle, gongmun, gjBodyW);
22474
23273
  coverPart[0] = hide(coverPart[0], true);
22475
23274
  preamble.push(...coverPart);
22476
- coverH1Idx = h1Idx;
23275
+ ctx.coverH1Idx = h1Idx;
22477
23276
  hasFrontPages = true;
22478
23277
  }
22479
- if (gongmun.toc && chapters.length > 0) {
22480
- const tocPart = buildGaejosikToc(chapters, gongmun);
23278
+ ctx.gjChapterIdxs = blocks.map((b, i) => b.type === "heading" && (b.level ?? 1) <= 2 && i !== ctx.coverH1Idx ? i : -1).filter((i) => i >= 0);
23279
+ if (gongmun.toc && ctx.gjChapterIdxs.length > 0) {
23280
+ const chapters = ctx.gjChapterIdxs.map((i) => stripChapterNumber(blocks[i].text || ""));
23281
+ const tocPart = buildGaejosikToc(chapters, gongmun, gjBodyW);
22481
23282
  tocPart[0] = hide(tocPart[0], false);
22482
23283
  preamble.push(...tocPart);
22483
23284
  hasFrontPages = true;
22484
23285
  }
22485
23286
  if (gongmun.bodyTitleBox && coverTitle && hasFrontPages) {
22486
- preamble.push(buildGaejosikBodyTitle(coverTitle, gongmun).replace(/^<hp:p /, `<hp:p pageBreak="1" `));
23287
+ preamble.push(buildGaejosikBodyTitle(coverTitle, gongmun, gjBodyW).replace(/^<hp:p /, `<hp:p pageBreak="1" `));
22487
23288
  }
22488
- } else if (gongmun && !gaejosik && bfReg && (gongmun.preset === "report" || gongmun.preset === "plan" || gongmun.preset === "notice")) {
22489
- titleBoxH1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
22490
- } else if (gongmun?.press && dfIds) {
22491
- pressH1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
23289
+ }
23290
+ if (gongmun && !ctx.gaejosik && ctx.coverH1Idx < 0 && bfReg && (gongmun.preset === "report" || gongmun.preset === "plan" || gongmun.preset === "notice")) {
23291
+ ctx.titleBoxH1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
23292
+ } else if (gongmun?.press && ctx.coverH1Idx < 0 && dfIds) {
23293
+ ctx.pressH1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
22492
23294
  }
22493
23295
  if (preamble.length > 0) {
22494
- preamble[0] = preamble[0].replace(` pageBreak="1"`, "").replace(/<hp:run charPrIDRef="(\d+)">/, `<hp:run charPrIDRef="$1">${generateSecPr(gongmun)}`);
22495
- isFirst = false;
22496
- pendingPageBreak = hasFrontPages && !preamble[preamble.length - 1].includes(`pageBreak="1"`);
22497
- pendingNewNum = !!gongmun.pageNumbers && hasFrontPages;
22498
- paraXmls.push(...preamble);
23296
+ preamble[0] = ctx.opener.inject(preamble[0].replace(` pageBreak="1"`, ""));
23297
+ ctx.pendingPageBreak = hasFrontPages && !preamble[preamble.length - 1].includes(`pageBreak="1"`);
23298
+ ctx.pendingNewNum = !!gongmun.pageNumbers && hasFrontPages;
23299
+ ctx.paraXmls.push(...preamble);
23300
+ }
23301
+ }
23302
+ function renderHeading(block, blockIdx, ctx) {
23303
+ const { gongmun, dfIds, tableStyle, bfReg, measured, richAssets, gjBodyW } = ctx;
23304
+ if (gongmun && blockIdx === ctx.coverH1Idx) return "";
23305
+ if (gongmun && blockIdx === ctx.pressH1Idx && dfIds) {
23306
+ const parts = [generateParagraph((block.text || "").trim(), GONGMUN_CENTER, dfIds.pressTitle)];
23307
+ for (const s of gongmun.press?.sub ?? []) {
23308
+ parts.push(generateParagraph(`- ${s} -`, GONGMUN_CENTER, dfIds.pressSub));
23309
+ }
23310
+ return parts.join("\n ");
23311
+ }
23312
+ if (gongmun && blockIdx === ctx.titleBoxH1Idx && tableStyle && bfReg) {
23313
+ return buildTitleBox(
23314
+ (block.text || "").trim(),
23315
+ measured ? GJ_CHAR_BODY_TITLE : CHAR_H1,
23316
+ richAssets ? GJ_CHAR_TITLE_BAR : GONGMUN_TITLE_BAR_CHAR,
23317
+ tableStyle.totalWidth,
23318
+ bfReg
23319
+ );
23320
+ }
23321
+ if (ctx.gaejosik) {
23322
+ const lvl = block.level || 1;
23323
+ if (lvl <= 2) {
23324
+ const no = ctx.gjChapterIdxs.indexOf(blockIdx);
23325
+ ctx.chapterNo = no >= 0 ? no + 1 : ctx.chapterNo + 1;
23326
+ return buildGaejosikChapter(ctx.chapterNo, stripChapterNumber(block.text || ""), gongmun, gjBodyW, lvl);
23327
+ } else if (lvl === 3) {
23328
+ return generateParagraph(
23329
+ `\u25A1 ${block.text || ""}`,
23330
+ GONGMUN_LIST_BASE,
23331
+ GJ_CHAR_DAE,
23332
+ (id) => id === CHAR_BOLD ? GJ_CHAR_DAE_BOLD : id
23333
+ );
23334
+ }
23335
+ return generateParagraph(`${gongmun.bullet2} ${block.text || ""}`, GONGMUN_LIST_BASE + 1, CHAR_NORMAL);
23336
+ }
23337
+ const pId = headingParaPrId(block.level || 1);
23338
+ const cId = headingCharPrId(block.level || 1);
23339
+ const styleId = gongmun ? Math.min(block.level || 1, 4) : 0;
23340
+ let hText = block.text || "";
23341
+ if (gongmun && (block.level || 1) === 2 && gongmun.h2Marker !== "none") {
23342
+ const title = stripChapterNumber(hText);
23343
+ hText = gongmun.h2Marker === "box" ? `\u25A1 ${title}` : `${++ctx.h2Seq}. ${title}`;
23344
+ }
23345
+ return generateParagraph(hText, pId, cId, void 0, styleId);
23346
+ }
23347
+ function renderParagraph(block, blockIdx, ctx) {
23348
+ const { gongmun, measured, fit, vBase } = ctx;
23349
+ if (ctx.images) {
23350
+ const { text: rest, urls } = splitImageRefs(block.text || "");
23351
+ if (urls.length > 0 && !rest.trim()) {
23352
+ const pics = urls.map((u) => {
23353
+ const part = ctx.images.take(u);
23354
+ return part ? ctx.images.inlinePicXml(part) : null;
23355
+ });
23356
+ if (pics.every(Boolean)) {
23357
+ return `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${pics.join("")}</hp:run></hp:p>`;
23358
+ }
23359
+ }
23360
+ }
23361
+ const pTrim = (block.text || "").trimStart();
23362
+ if (measured && (pTrim.startsWith("\u203B") || /^\*\s/.test(pTrim))) {
23363
+ return generateParagraph((block.text || "").trim(), GJ_PARA_CHAM, GJ_CHAR_CHAM, ctx.chamMap);
23364
+ }
23365
+ const ctr = gongmun && /^<center>([\s\S]*)<\/center>$/i.exec((block.text || "").trim());
23366
+ const rgt = gongmun && /^<right>([\s\S]*)<\/right>$/i.exec((block.text || "").trim());
23367
+ if (ctr) return generateParagraph(ctr[1].trim(), GONGMUN_CENTER);
23368
+ if (rgt) return generateParagraph(rgt[1].trim(), GONGMUN_RIGHT);
23369
+ return generateParagraph(block.text || "", PARA_NORMAL, CHAR_NORMAL, fit ? variantMapper(fit, blockIdx, vBase) : void 0);
23370
+ }
23371
+ function renderCodeBlock(block, blockIdx, ctx) {
23372
+ if (ctx.chartParts !== null && (block.lang || "").toLowerCase() === "chart") {
23373
+ const fence = parseChartFence(block.text || "");
23374
+ if (fence) {
23375
+ const partName = `Chart/chart${ctx.chartParts.length + 1}.xml`;
23376
+ ctx.chartParts.push({ name: partName, xml: buildChartSpaceXml(fence) });
23377
+ const chartEl = buildChartElementXml(partName, fence.widthHu, fence.heightHu, CHART_TABLE_ID_BASE + blockIdx);
23378
+ ctx.opener.emitCarrier(ctx.paraXmls);
23379
+ return `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${chartEl}</hp:run></hp:p>`;
23380
+ }
23381
+ }
23382
+ const codeLines = (block.text || "").split("\n");
23383
+ return codeLines.map((line) => generateParagraph(line || " ", PARA_CODE)).join("\n ");
23384
+ }
23385
+ function renderEquation(block, blockIdx, ctx) {
23386
+ ctx.opener.emitCarrier(ctx.paraXmls);
23387
+ return generateEquationParagraph(block.text || "", blockIdx);
23388
+ }
23389
+ function renderBlockquote(block, ctx) {
23390
+ if (ctx.measured) {
23391
+ const t = (block.text || "").replace(/\n+/g, " ").trim();
23392
+ return t ? generateParagraph(t.startsWith("\u203B") ? t : `\u203B ${t}`, GJ_PARA_CHAM, GJ_CHAR_CHAM, ctx.chamMap) : "";
23393
+ }
23394
+ const quoteChar = ctx.theme.hasQuoteOption ? CHAR_QUOTE : CHAR_NORMAL;
23395
+ return (block.text || "").split("\n").map((line) => generateParagraph(line, PARA_QUOTE, quoteChar)).join("\n ");
23396
+ }
23397
+ function renderListItem(block, blockIdx, ctx) {
23398
+ const { gongmun, gongmunList, measured, fit, vBase, dfIds } = ctx;
23399
+ if (gongmun && gongmunList) {
23400
+ const info = gongmunList.items.get(blockIdx);
23401
+ const depth = info?.depth ?? 0;
23402
+ const marker2 = info?.marker ?? "";
23403
+ const content = block.text || "";
23404
+ if (measured && content.trimStart().startsWith("\u203B")) {
23405
+ return generateParagraph(content.trim(), GJ_PARA_CHAM, GJ_CHAR_CHAM, ctx.chamMap);
23406
+ }
23407
+ if (measured && block.marker === "*") {
23408
+ return generateParagraph(`* ${content.trim()}`, GJ_PARA_CHAM, GJ_CHAR_CHAM, ctx.chamMap);
23409
+ }
23410
+ const text = marker2 ? `${marker2} ${content}` : content;
23411
+ const listParaPr = marker2 ? info?.indentVariant !== void 0 ? GONGMUN_LIST_VARIANT_BASE + info.indentVariant : GONGMUN_LIST_BASE + depth : GONGMUN_LIST_PLAIN_BASE + depth;
23412
+ let listCharPr = gongmun.numbering === "report" && depth === 0 ? CHAR_BOLD : CHAR_NORMAL;
23413
+ let mapId = fit ? variantMapper(fit, blockIdx, vBase) : void 0;
23414
+ if (measured && depth === 0 && gongmun.numbering !== "standard") {
23415
+ listCharPr = GJ_CHAR_DAE;
23416
+ mapId = (id) => id === CHAR_BOLD ? GJ_CHAR_DAE_BOLD : id;
23417
+ }
23418
+ if (gongmun.preset === "press") {
23419
+ if (depth === 0) listCharPr = CHAR_NORMAL;
23420
+ if (depth >= 2 && dfIds) {
23421
+ listCharPr = dfIds.small;
23422
+ mapId = void 0;
23423
+ }
23424
+ }
23425
+ return generateParagraph(text, listParaPr, listCharPr, mapId);
23426
+ }
23427
+ const indent = block.indent || 0;
23428
+ let marker;
23429
+ if (block.marker) {
23430
+ marker = `${block.marker} `;
23431
+ ctx.prevWasOrdered = !!block.ordered;
23432
+ } else if (block.ordered) {
23433
+ ctx.orderedCounters[indent] = (ctx.orderedCounters[indent] || 0) + 1;
23434
+ for (const k of Object.keys(ctx.orderedCounters)) {
23435
+ if (+k > indent) delete ctx.orderedCounters[+k];
23436
+ }
23437
+ marker = `${ctx.orderedCounters[indent]}. `;
23438
+ ctx.prevWasOrdered = true;
23439
+ } else {
23440
+ marker = "\xB7 ";
23441
+ if (ctx.prevWasOrdered) {
23442
+ for (const k of Object.keys(ctx.orderedCounters)) delete ctx.orderedCounters[+k];
23443
+ }
23444
+ ctx.prevWasOrdered = false;
23445
+ }
23446
+ const indentPrefix = " ".repeat(indent);
23447
+ return generateParagraph(indentPrefix + marker + (block.text || ""), PARA_LIST);
23448
+ }
23449
+ function renderHr(ctx) {
23450
+ return ctx.gongmun ? `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}"><hp:t></hp:t></hp:run></hp:p>` : `<hp:p paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0"><hp:t>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500</hp:t></hp:run></hp:p>`;
23451
+ }
23452
+ function renderTable(block, ctx) {
23453
+ if (!block.rows) return "";
23454
+ ctx.opener.emitCarrier(ctx.paraXmls);
23455
+ return generateTable(block.rows, ctx.theme, ctx.tableStyle, ctx.remap, ctx.tableSeq++, ctx.images);
23456
+ }
23457
+ function renderHtmlTable(block, ctx) {
23458
+ const { tableStyle, bfReg } = ctx;
23459
+ const htmlW = tableStyle ? bfReg ? tableStyle.totalWidth - DATA_TABLE_INSET : tableStyle.totalWidth : 44e3;
23460
+ const tbl2 = generateHtmlTableXml(block.text || "", ctx.theme, htmlW, tableStyle, ctx.remap, ctx.tableSeq++, ctx.images);
23461
+ if (tbl2) {
23462
+ ctx.opener.emitCarrier(ctx.paraXmls);
23463
+ return `<hp:p paraPrIDRef="${tableStyle && bfReg ? GONGMUN_RIGHT : 0}" styleIDRef="0"><hp:run charPrIDRef="0">${tbl2}</hp:run></hp:p>`;
23464
+ }
23465
+ const plain = (block.text || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
23466
+ return plain ? generateParagraph(plain) : "";
23467
+ }
23468
+ function appendPostamble(blocks, ctx) {
23469
+ const { gongmun, dfIds, bfReg, tableStyle, paraXmls } = ctx;
23470
+ if (gongmun?.noticeHead && dfIds) {
23471
+ paraXmls.push(...buildNoticeFoot(gongmun));
23472
+ }
23473
+ if (gongmun?.endMark && paraXmls.length > 0) {
23474
+ const lastBlock = [...blocks].reverse().find((b) => (b.text || "").trim() || b.type === "table" && b.rows?.length);
23475
+ const lastText = lastBlock && (lastBlock.type === "paragraph" || lastBlock.type === "list_item") ? lastBlock.text || "" : "";
23476
+ if (!hasEndMark(lastText)) paraXmls.push(buildEndMark());
23477
+ }
23478
+ if (gongmun?.docFoot && dfIds) {
23479
+ paraXmls.push(...buildDocFoot(gongmun, dfIds));
23480
+ }
23481
+ if (gongmun?.press && dfIds && bfReg && tableStyle) {
23482
+ paraXmls.push(...buildPressContact(gongmun, dfIds, tableStyle.totalWidth, bfReg));
22499
23483
  }
23484
+ }
23485
+ function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null, fit = null, chartParts = null, bfReg = null, remap = null, dfIds = null, images = null) {
23486
+ resetTableIds();
23487
+ resetGjTableIds();
23488
+ resetExtraTableIds();
23489
+ const measured = !!gongmun && usesReportFonts(gongmun.preset);
23490
+ const richAssets = !!gongmun && needsGaejosikAssets(gongmun);
23491
+ const ctx = {
23492
+ theme,
23493
+ gongmun,
23494
+ gongmunList,
23495
+ fit,
23496
+ chartParts,
23497
+ bfReg,
23498
+ remap,
23499
+ dfIds,
23500
+ images,
23501
+ gaejosik: gongmun?.preset === "gaejosik",
23502
+ measured,
23503
+ richAssets,
23504
+ vBase: charVariantBase(richAssets, !!gongmun),
23505
+ // 공문서 표 스타일 — 본문 폭 맞춤 + 실측 정부 양식(헤더 음영·실측 프리셋 맑은 고딕 12pt)
23506
+ // bfReg가 있으면 실측 테두리 위계(외곽 0.4/내부 0.12/헤더 이중선)·셀 문단·우측 배치 적용
23507
+ tableStyle: gongmun ? {
23508
+ totalWidth: mmToHwpunit(210 - gongmun.margins.left - gongmun.margins.right),
23509
+ // 표 셀 크기 — 실측: 실측 프리셋 맑은 고딕 12pt(22·23), 비실측도 12pt(11·12,
23510
+ // 실결재 지배값 — 본문 크기 셀은 열폭 부족으로 서술 열 세로 신장, v4.0.2 QA)
23511
+ charPr: richAssets ? GJ_CHAR_TABLE : GONGMUN_TBL_CHAR,
23512
+ boldCharPr: richAssets ? GJ_CHAR_TABLE_BOLD : GONGMUN_TBL_CHAR_BOLD,
23513
+ charHeight: richAssets ? gaejosikSizes(gongmun.bodyHeight, gongmun.sizes).table : GONGMUN_TBL_PT,
23514
+ headerBf: gongmunTableHeaderBf(richAssets),
23515
+ centerParaPr: GONGMUN_CENTER,
23516
+ tblCenterParaPr: GONGMUN_TBL_CENTER,
23517
+ tblLeftParaPr: GONGMUN_TBL_LEFT,
23518
+ bfRegistry: bfReg ?? void 0,
23519
+ rightParaPr: GONGMUN_RIGHT
23520
+ } : null,
23521
+ // 개조식 장식표(표지·목차·장헤더·제목박스) 폭 스케일용 본문폭 — margins 오버라이드 대응
23522
+ gjBodyW: gongmun ? mmToHwpunit(210 - gongmun.margins.left - gongmun.margins.right) : GAEJOSIK_BASE_WIDTH,
23523
+ chamMap: (id) => id === CHAR_BOLD ? GJ_CHAR_CHAM_BOLD : id,
23524
+ opener: new SectionOpener(gongmun),
23525
+ paraXmls: [],
23526
+ // 표 방출 순번 — 생성 성공 여부와 무관하게 '시도' 기준으로 센다. 실패한 표가 이후
23527
+ // 표들의 순번을 밀면 앵커 없는 프로필(손편집·구버전)의 table_index 매칭이 어긋난다.
23528
+ tableSeq: 0,
23529
+ chapterNo: 0,
23530
+ h2Seq: 0,
23531
+ // h2 말머리 'number' 모드 아라비아 순번 (QA-2)
23532
+ coverH1Idx: -1,
23533
+ titleBoxH1Idx: -1,
23534
+ pressH1Idx: -1,
23535
+ gjChapterIdxs: [],
23536
+ pendingPageBreak: false,
23537
+ pendingNewNum: false,
23538
+ // 순서 있는 목록 카운터 — indent 레벨별 별도 유지. 다른 블록 만나면 해당 레벨 리셋.
23539
+ orderedCounters: {},
23540
+ prevWasOrdered: false
23541
+ };
23542
+ buildPreamble(blocks, ctx);
22500
23543
  for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
22501
23544
  const block = blocks[blockIdx];
22502
- let xml = "";
22503
23545
  if (block.type !== "list_item" || !block.ordered) {
22504
- if (prevWasOrdered) {
22505
- for (const k of Object.keys(orderedCounters)) delete orderedCounters[+k];
23546
+ if (ctx.prevWasOrdered) {
23547
+ for (const k of Object.keys(ctx.orderedCounters)) delete ctx.orderedCounters[+k];
22506
23548
  }
22507
- prevWasOrdered = false;
23549
+ ctx.prevWasOrdered = false;
22508
23550
  }
23551
+ let xml = "";
22509
23552
  switch (block.type) {
22510
- case "heading": {
22511
- if (gongmun && blockIdx === pressH1Idx && dfIds) {
22512
- const parts = [generateParagraph((block.text || "").trim(), GONGMUN_CENTER, dfIds.pressTitle)];
22513
- for (const s of gongmun.press?.sub ?? []) {
22514
- parts.push(generateParagraph(`- ${s} -`, GONGMUN_CENTER, dfIds.pressSub));
22515
- }
22516
- xml = parts.join("\n ");
22517
- break;
22518
- }
22519
- if (gongmun && blockIdx === titleBoxH1Idx && tableStyle && bfReg) {
22520
- xml = buildTitleBox((block.text || "").trim(), measured ? GJ_CHAR_BODY_TITLE : CHAR_H1, tableStyle.totalWidth, bfReg);
22521
- break;
22522
- }
22523
- if (gaejosik) {
22524
- const lvl = block.level || 1;
22525
- if (blockIdx === coverH1Idx) break;
22526
- if (lvl <= 2) {
22527
- chapterNo++;
22528
- xml = buildGaejosikChapter(chapterNo, stripChapterNumber(block.text || ""), gongmun);
22529
- } else if (lvl === 3) {
22530
- xml = generateParagraph(
22531
- `\u25A1 ${block.text || ""}`,
22532
- GONGMUN_LIST_BASE,
22533
- GJ_CHAR_DAE,
22534
- (id) => id === CHAR_BOLD ? GJ_CHAR_DAE_BOLD : id
22535
- );
22536
- } else {
22537
- xml = generateParagraph(`${gongmun.bullet2} ${block.text || ""}`, GONGMUN_LIST_BASE + 1, CHAR_NORMAL);
22538
- }
22539
- break;
22540
- }
22541
- const pId = headingParaPrId(block.level || 1);
22542
- const cId = headingCharPrId(block.level || 1);
22543
- const styleId = gongmun ? Math.min(block.level || 1, 4) : 0;
22544
- let hText = block.text || "";
22545
- if (gongmun && (block.level || 1) === 2 && gongmun.h2Marker !== "none") {
22546
- const title = stripChapterNumber(hText);
22547
- hText = gongmun.h2Marker === "box" ? `\u25A1 ${title}` : `${++h2Seq}. ${title}`;
22548
- }
22549
- xml = generateParagraph(hText, pId, cId, void 0, styleId);
23553
+ case "heading":
23554
+ xml = renderHeading(block, blockIdx, ctx);
22550
23555
  break;
22551
- }
22552
- case "paragraph": {
22553
- const pTrim = (block.text || "").trimStart();
22554
- if (measured && (pTrim.startsWith("\u203B") || /^\*\s/.test(pTrim))) {
22555
- xml = generateParagraph((block.text || "").trim(), GJ_PARA_CHAM, GJ_CHAR_CHAM, chamMap);
22556
- break;
22557
- }
22558
- const ctr = gongmun && /^<center>([\s\S]*)<\/center>$/i.exec((block.text || "").trim());
22559
- const rgt = gongmun && /^<right>([\s\S]*)<\/right>$/i.exec((block.text || "").trim());
22560
- if (ctr) {
22561
- xml = generateParagraph(ctr[1].trim(), GONGMUN_CENTER);
22562
- } else if (rgt) {
22563
- xml = generateParagraph(rgt[1].trim(), GONGMUN_RIGHT);
22564
- } else {
22565
- xml = generateParagraph(block.text || "", PARA_NORMAL, CHAR_NORMAL, fit ? variantMapper(fit, blockIdx, vBase) : void 0);
22566
- }
23556
+ case "paragraph":
23557
+ xml = renderParagraph(block, blockIdx, ctx);
22567
23558
  break;
22568
- }
22569
- case "code_block": {
22570
- if (chartParts !== null && (block.lang || "").toLowerCase() === "chart") {
22571
- const fence = parseChartFence(block.text || "");
22572
- if (fence) {
22573
- const partName = `Chart/chart${chartParts.length + 1}.xml`;
22574
- chartParts.push({ name: partName, xml: buildChartSpaceXml(fence) });
22575
- const chartEl = buildChartElementXml(partName, fence.widthHu, fence.heightHu, 91e5 + blockIdx);
22576
- if (isFirst) {
22577
- const secRun = `<hp:run charPrIDRef="0">${generateSecPr(gongmun)}<hp:t></hp:t></hp:run>`;
22578
- paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
22579
- isFirst = false;
22580
- }
22581
- xml = `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${chartEl}</hp:run></hp:p>`;
22582
- break;
22583
- }
22584
- }
22585
- const codeLines = (block.text || "").split("\n");
22586
- xml = codeLines.map((line) => generateParagraph(line || " ", PARA_CODE)).join("\n ");
23559
+ case "code_block":
23560
+ xml = renderCodeBlock(block, blockIdx, ctx);
22587
23561
  break;
22588
- }
22589
- case "equation": {
22590
- if (isFirst) {
22591
- const secRun = `<hp:run charPrIDRef="0">${generateSecPr(gongmun)}<hp:t></hp:t></hp:run>`;
22592
- paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
22593
- isFirst = false;
22594
- }
22595
- xml = generateEquationParagraph(block.text || "", blockIdx);
23562
+ case "equation":
23563
+ xml = renderEquation(block, blockIdx, ctx);
22596
23564
  break;
22597
- }
22598
- case "blockquote": {
22599
- if (measured) {
22600
- const t = (block.text || "").trim();
22601
- if (t) xml = generateParagraph(t.startsWith("\u203B") ? t : `\u203B ${t}`, GJ_PARA_CHAM, GJ_CHAR_CHAM, chamMap);
22602
- break;
22603
- }
22604
- xml = generateParagraph(
22605
- block.text || "",
22606
- PARA_QUOTE,
22607
- theme.hasQuoteOption ? CHAR_QUOTE : CHAR_NORMAL
22608
- );
23565
+ case "blockquote":
23566
+ xml = renderBlockquote(block, ctx);
22609
23567
  break;
22610
- }
22611
- case "list_item": {
22612
- if (gongmun && gongmunList) {
22613
- const info = gongmunList.get(blockIdx);
22614
- const depth = info?.depth ?? 0;
22615
- const marker2 = info?.marker ?? "";
22616
- const content = block.text || "";
22617
- if (measured && content.trimStart().startsWith("\u203B")) {
22618
- xml = generateParagraph(content.trim(), GJ_PARA_CHAM, GJ_CHAR_CHAM, chamMap);
22619
- break;
22620
- }
22621
- if (measured && block.marker === "*") {
22622
- xml = generateParagraph(`* ${content.trim()}`, GJ_PARA_CHAM, GJ_CHAR_CHAM, chamMap);
22623
- break;
22624
- }
22625
- const text = marker2 ? `${marker2} ${content}` : content;
22626
- const listParaPr = marker2 ? GONGMUN_LIST_BASE + depth : GONGMUN_LIST_PLAIN_BASE + depth;
22627
- let listCharPr = gongmun.numbering === "report" && depth === 0 ? CHAR_BOLD : CHAR_NORMAL;
22628
- let mapId = fit ? variantMapper(fit, blockIdx, vBase) : void 0;
22629
- if (measured && depth === 0 && gongmun.numbering !== "standard") {
22630
- listCharPr = GJ_CHAR_DAE;
22631
- mapId = (id) => id === CHAR_BOLD ? GJ_CHAR_DAE_BOLD : id;
22632
- }
22633
- if (gongmun.preset === "press") {
22634
- if (depth === 0) listCharPr = CHAR_NORMAL;
22635
- if (depth >= 2 && dfIds) {
22636
- listCharPr = dfIds.small;
22637
- mapId = void 0;
22638
- }
22639
- }
22640
- xml = generateParagraph(text, listParaPr, listCharPr, mapId);
22641
- break;
22642
- }
22643
- const indent = block.indent || 0;
22644
- let marker;
22645
- if (block.marker) {
22646
- marker = `${block.marker} `;
22647
- prevWasOrdered = !!block.ordered;
22648
- } else if (block.ordered) {
22649
- orderedCounters[indent] = (orderedCounters[indent] || 0) + 1;
22650
- for (const k of Object.keys(orderedCounters)) {
22651
- if (+k > indent) delete orderedCounters[+k];
22652
- }
22653
- marker = `${orderedCounters[indent]}. `;
22654
- prevWasOrdered = true;
22655
- } else {
22656
- marker = "\xB7 ";
22657
- if (prevWasOrdered) {
22658
- for (const k of Object.keys(orderedCounters)) delete orderedCounters[+k];
22659
- }
22660
- prevWasOrdered = false;
22661
- }
22662
- const indentPrefix = " ".repeat(indent);
22663
- xml = generateParagraph(indentPrefix + marker + (block.text || ""), PARA_LIST);
23568
+ case "list_item":
23569
+ xml = renderListItem(block, blockIdx, ctx);
22664
23570
  break;
22665
- }
22666
23571
  case "hr":
22667
- xml = gongmun ? `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}"><hp:t></hp:t></hp:run></hp:p>` : `<hp:p paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0"><hp:t>\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500</hp:t></hp:run></hp:p>`;
23572
+ xml = renderHr(ctx);
22668
23573
  break;
22669
23574
  case "table":
22670
- if (block.rows) {
22671
- if (isFirst) {
22672
- const secRun = `<hp:run charPrIDRef="0">${generateSecPr(gongmun)}<hp:t></hp:t></hp:run>`;
22673
- paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
22674
- isFirst = false;
22675
- }
22676
- xml = generateTable(block.rows, theme, tableStyle, remap, tableSeq++);
22677
- }
23575
+ xml = renderTable(block, ctx);
22678
23576
  break;
22679
- case "html_table": {
22680
- const htmlW = tableStyle ? bfReg ? tableStyle.totalWidth - DATA_TABLE_INSET : tableStyle.totalWidth : 44e3;
22681
- const tbl2 = generateHtmlTableXml(block.text || "", theme, htmlW, tableStyle, remap, tableSeq++);
22682
- if (tbl2) {
22683
- if (isFirst) {
22684
- const secRun = `<hp:run charPrIDRef="0">${generateSecPr(gongmun)}<hp:t></hp:t></hp:run>`;
22685
- paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
22686
- isFirst = false;
22687
- }
22688
- xml = `<hp:p paraPrIDRef="${tableStyle && bfReg ? GONGMUN_RIGHT : 0}" styleIDRef="0"><hp:run charPrIDRef="0">${tbl2}</hp:run></hp:p>`;
22689
- } else {
22690
- const plain = (block.text || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
22691
- xml = plain ? generateParagraph(plain) : "";
22692
- }
23577
+ case "html_table":
23578
+ xml = renderHtmlTable(block, ctx);
22693
23579
  break;
22694
- }
22695
23580
  }
22696
23581
  if (!xml) continue;
22697
- if (isFirst && block.type !== "table") {
22698
- xml = xml.replace(
22699
- /<hp:run charPrIDRef="(\d+)">/,
22700
- `<hp:run charPrIDRef="$1">${generateSecPr(gongmun)}`
22701
- );
22702
- isFirst = false;
23582
+ if (ctx.opener.isFirst && block.type !== "table") {
23583
+ xml = ctx.opener.inject(xml);
22703
23584
  }
22704
- if (pendingPageBreak) {
23585
+ if (ctx.pendingPageBreak) {
22705
23586
  xml = xml.replace(/^<hp:p /, `<hp:p pageBreak="1" `);
22706
- pendingPageBreak = false;
23587
+ ctx.pendingPageBreak = false;
22707
23588
  }
22708
- if (pendingNewNum) {
23589
+ if (ctx.pendingNewNum) {
22709
23590
  xml = xml.replace(/<hp:run charPrIDRef="(\d+)">/, `<hp:run charPrIDRef="$1">${newPageNumCtrl(1)}`);
22710
- pendingNewNum = false;
23591
+ ctx.pendingNewNum = false;
22711
23592
  }
22712
- paraXmls.push(xml);
22713
- }
22714
- if (gongmun?.noticeHead && dfIds) {
22715
- paraXmls.push(...buildNoticeFoot(gongmun));
22716
- }
22717
- if (gongmun?.endMark && paraXmls.length > 0) {
22718
- const lastText = [...blocks].reverse().find((b) => (b.type === "paragraph" || b.type === "list_item") && (b.text || "").trim())?.text ?? "";
22719
- if (!hasEndMark(lastText)) paraXmls.push(buildEndMark());
22720
- }
22721
- if (gongmun?.docFoot && dfIds) {
22722
- paraXmls.push(...buildDocFoot(gongmun, dfIds));
22723
- }
22724
- if (gongmun?.press && dfIds && bfReg && tableStyle) {
22725
- paraXmls.push(...buildPressContact(gongmun, dfIds, tableStyle.totalWidth, bfReg));
23593
+ ctx.paraXmls.push(xml);
22726
23594
  }
22727
- if (paraXmls.length === 0) {
22728
- paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0">${generateSecPr(gongmun)}<hp:t></hp:t></hp:run></hp:p>`);
23595
+ appendPostamble(blocks, ctx);
23596
+ if (ctx.paraXmls.length === 0) {
23597
+ ctx.paraXmls.push(ctx.opener.emptyDoc());
22729
23598
  }
22730
23599
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
22731
23600
  <hs:sec xmlns:hs="${NS_SECTION}" xmlns:hp="${NS_PARA}">
22732
- ${paraXmls.join("\n ")}
23601
+ ${ctx.paraXmls.join("\n ")}
22733
23602
  </hs:sec>`;
22734
23603
  }
22735
23604
 
@@ -22737,19 +23606,20 @@ function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? prec
22737
23606
  async function markdownToHwpx(markdown, options) {
22738
23607
  const theme = resolveTheme(options?.theme);
22739
23608
  const gongmun = options?.gongmun ? resolveGongmun(options.gongmun) : null;
22740
- const gaejosik = gongmun?.preset === "gaejosik";
22741
23609
  const measured = !!gongmun && usesReportFonts(gongmun.preset);
23610
+ const richAssets = !!gongmun && needsGaejosikAssets(gongmun);
22742
23611
  const blocks = parseMarkdownToBlocks(markdown);
22743
23612
  const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
22744
23613
  const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
22745
- const staticBfEnd = gongmun ? gaejosik ? 11 : 4 : 3;
22746
- const remap = options?.profile ? buildProfileRemap(options.profile, charVariantBase(measured, !!gongmun) + (fit?.variants?.length ?? 0) * 4, staticBfEnd) : null;
23614
+ const staticBfEnd = staticBorderFillNext(gongmun);
23615
+ const remap = options?.profile ? buildProfileRemap(options.profile, charVariantBase(richAssets, !!gongmun) + (fit?.variants?.length ?? 0) * 4, staticBfEnd, staticFontNext(gongmun)) : null;
22747
23616
  const bfReg = gongmun ? new TableBfRegistry(staticBfEnd + (remap?.borderFillXmls.length ?? 0)) : null;
22748
23617
  const dfActive = !!gongmun && docframeActive(gongmun);
22749
- const dfBase = charVariantBase(measured, !!gongmun) + (fit?.variants?.length ?? 0) * 4 + (remap?.charPrXmls.length ?? 0);
23618
+ const dfBase = charVariantBase(richAssets, !!gongmun) + (fit?.variants?.length ?? 0) * 4 + (remap?.charPrXmls.length ?? 0);
22750
23619
  const dfIds = dfActive ? docframeIds(dfBase) : null;
22751
23620
  const chartParts = [];
22752
- const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit, chartParts, bfReg, remap, dfIds);
23621
+ const images = new ImageRegistry();
23622
+ const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit, chartParts, bfReg, remap, dfIds, images);
22753
23623
  if (remap && remap.tables.length > 0) {
22754
23624
  const unused = remap.tables.filter((t) => !t.used).length;
22755
23625
  if (unused === remap.tables.length) {
@@ -22760,13 +23630,16 @@ async function markdownToHwpx(markdown, options) {
22760
23630
  const zip = new JSZip6();
22761
23631
  zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
22762
23632
  zip.file("META-INF/container.xml", generateContainerXml());
22763
- zip.file("Contents/content.hpf", generateManifest(chartParts));
23633
+ zip.file("Contents/content.hpf", generateManifest(chartParts, images.manifestItems(), gongmun ? "gongmun" : "default"));
23634
+ for (const part of images.parts) zip.file(part.name, part.data);
22764
23635
  zip.file("Contents/header.xml", generateHeaderXml(
22765
23636
  theme,
22766
23637
  gongmun,
22767
23638
  fit?.variants ?? [],
22768
23639
  extraBorderFills,
22769
- [...remap?.charPrXmls ?? [], ...dfActive ? docframeCharPrXmls(dfBase, measured) : []]
23640
+ [...remap?.charPrXmls ?? [], ...dfActive ? docframeCharPrXmls(dfBase, richAssets) : []],
23641
+ gongmunList?.indentVariants ?? [],
23642
+ remap?.fontFaces ?? []
22770
23643
  ));
22771
23644
  zip.file("Contents/section0.xml", sectionXml);
22772
23645
  for (const part of chartParts) zip.file(part.name, part.xml);
@@ -22946,7 +23819,19 @@ function parseBorderFills(headerDoc) {
22946
23819
  }
22947
23820
  return map;
22948
23821
  }
22949
- function parseCharPrs(headerDoc) {
23822
+ function parseHangulFonts(headerDoc) {
23823
+ const map = /* @__PURE__ */ new Map();
23824
+ for (const ff of elemsByLocal(headerDoc, "fontface")) {
23825
+ if (ff.getAttribute("lang") !== "HANGUL") continue;
23826
+ for (const font of elemsByLocal(ff, "font")) {
23827
+ const id = font.getAttribute("id");
23828
+ const face = font.getAttribute("face");
23829
+ if (id && face) map.set(id, face);
23830
+ }
23831
+ }
23832
+ return map;
23833
+ }
23834
+ function parseCharPrs(headerDoc, hangulFonts) {
22950
23835
  const map = /* @__PURE__ */ new Map();
22951
23836
  for (const cp of elemsByLocal(headerDoc, "charPr")) {
22952
23837
  const id = cp.getAttribute("id");
@@ -22961,7 +23846,11 @@ function parseCharPrs(headerDoc) {
22961
23846
  if (findChildByLocalName(cp, "underline")) def.underline = true;
22962
23847
  const fontRef = findChildByLocalName(cp, "fontRef");
22963
23848
  const hangul = fontRef?.getAttribute("hangul");
22964
- if (hangul) def.fontRef_hangul = hangul;
23849
+ if (hangul) {
23850
+ def.fontRef_hangul = hangul;
23851
+ const face = hangulFonts.get(hangul);
23852
+ if (face) def.fontName_hangul = face;
23853
+ }
22965
23854
  map.set(id, def);
22966
23855
  }
22967
23856
  return map;
@@ -22989,6 +23878,8 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
22989
23878
  const usedBf = /* @__PURE__ */ new Set();
22990
23879
  const usedCp = /* @__PURE__ */ new Set();
22991
23880
  const colWidths = new Array(cols).fill(void 0);
23881
+ const spanCells = [];
23882
+ const row0Texts = /* @__PURE__ */ new Map();
22992
23883
  let anchorText = "";
22993
23884
  for (const tc2 of elemsByLocal(tbl2, "tc")) {
22994
23885
  if (nearestTable(tc2) !== tbl2) continue;
@@ -22998,6 +23889,7 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
22998
23889
  const row = num(addr?.getAttribute("rowAddr")) ?? 0;
22999
23890
  const col = num(addr?.getAttribute("colAddr")) ?? 0;
23000
23891
  if (row === 0 && col === 0 && !anchorText) anchorText = directCellText(tc2);
23892
+ if (row === 0) row0Texts.set(col, directCellText(tc2));
23001
23893
  const colSpan = num(span?.getAttribute("colSpan")) ?? 1;
23002
23894
  const rowSpan = num(span?.getAttribute("rowSpan")) ?? 1;
23003
23895
  const bfId = tc2.getAttribute("borderFillIDRef") || void 0;
@@ -23016,7 +23908,20 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23016
23908
  usedCp.add(cpId);
23017
23909
  }
23018
23910
  cells.push(cell2);
23019
- if (row === 0 && colSpan === 1 && col < cols && w) colWidths[col] = w;
23911
+ const wNum = num(w);
23912
+ if (wNum != null && col < cols) {
23913
+ if (colSpan === 1) {
23914
+ if (colWidths[col] == null) colWidths[col] = wNum;
23915
+ } else spanCells.push({ col, colSpan, w: wNum });
23916
+ }
23917
+ }
23918
+ for (const s of spanCells) {
23919
+ const covered = Array.from({ length: s.colSpan }, (_, i) => s.col + i).filter((c) => c < cols);
23920
+ const unknown = covered.filter((c) => colWidths[c] == null);
23921
+ if (unknown.length === 0) continue;
23922
+ const known = covered.reduce((sum, c) => sum + (colWidths[c] ?? 0), 0);
23923
+ const each = Math.floor((s.w - known) / unknown.length);
23924
+ if (each > 0) for (const c of unknown) colWidths[c] = each;
23020
23925
  }
23021
23926
  const table2 = {
23022
23927
  table_index: tableIndex,
@@ -23027,8 +23932,10 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23027
23932
  };
23028
23933
  const anchor = normalizeAnchor(anchorText);
23029
23934
  if (anchor) table2.anchor_text = anchor;
23935
+ const rowAnchor = normalizeRowAnchor(Array.from({ length: cols }, (_, c) => row0Texts.get(c) ?? ""));
23936
+ if (rowAnchor) table2.anchor_row = rowAnchor;
23030
23937
  if (width) table2.width_hwpunit = width;
23031
- if (colWidths.every((w) => w != null)) table2.col_widths_hwpunit = colWidths;
23938
+ if (colWidths.every((w) => w != null)) table2.col_widths_hwpunit = colWidths.map(String);
23032
23939
  const cp = pick(charPrs, usedCp);
23033
23940
  if (Object.keys(cp).length) table2.used_char_prs = cp;
23034
23941
  return table2;
@@ -23085,7 +23992,7 @@ async function hwpxToProfile(input) {
23085
23992
  if (headerFile) headerXml = await headerFile.async("text");
23086
23993
  const headerDoc = parser.parseFromString(headerXml, "text/xml");
23087
23994
  const borderFills = parseBorderFills(headerDoc);
23088
- const charPrs = parseCharPrs(headerDoc);
23995
+ const charPrs = parseCharPrs(headerDoc, parseHangulFonts(headerDoc));
23089
23996
  const sectionFiles = zip.file(/[Ss]ection\d+\.xml$/).sort((a, b) => (num(a.name.match(/(\d+)\.xml$/)?.[1] ?? null) ?? 0) - (num(b.name.match(/(\d+)\.xml$/)?.[1] ?? null) ?? 0));
23090
23997
  const tables = [];
23091
23998
  let tableIndex = 0;
@@ -23096,7 +24003,7 @@ async function hwpxToProfile(input) {
23096
24003
  tables.push(parseTable3(tbl2, tableIndex++, borderFills, charPrs));
23097
24004
  }
23098
24005
  }
23099
- return { schema_version: "0.2.0", tables };
24006
+ return { schema_version: "0.3.0", tables };
23100
24007
  }
23101
24008
 
23102
24009
  // src/hwpx/font-catalog.ts
@@ -23236,7 +24143,7 @@ var COMMON_FREE = [
23236
24143
  "\uC724\uBA85\uC870"
23237
24144
  ];
23238
24145
  function norm(name) {
23239
- return name.replace(/\s+/g, "").toLowerCase();
24146
+ return name.normalize("NFC").replace(/\s+/g, "").toLowerCase();
23240
24147
  }
23241
24148
  var KNOWN = new Set([...HANCOM_BUNDLED, ...WINDOWS_DEFAULT, ...COMMON_FREE].map(norm));
23242
24149
  function isKnownFont(name) {
@@ -23266,7 +24173,7 @@ var RULES = [
23266
24173
  {
23267
24174
  code: "DATE_ZERO_PAD",
23268
24175
  severity: "error",
23269
- pattern: /\b\d{4}\.\s*0\d\.|\.\s*0\d\./g,
24176
+ pattern: /\b\d{4}\.\s*0\d\.|\b\d{4}\.\s*\d{1,2}\.\s*0\d/g,
23270
24177
  message: "\uC6D4\xB7\uC77C \uC55E\uC758 '0'\uC740 \uD45C\uAE30\uD558\uC9C0 \uC54A\uC74C",
23271
24178
  suggest: "\uC608) 2025. 1. 6. (2025. 01. 06. \u2715)"
23272
24179
  },
@@ -23348,22 +24255,25 @@ var RULES = [
23348
24255
  {
23349
24256
  code: "COLON_SPACE",
23350
24257
  severity: "warning",
23351
- pattern: /\S\s+:\S|\S:(?!\/\/)[^\s\d]/g,
24258
+ pattern: /\S\s+:(?!\/\/)|\S:(?!\/\/)[^\s\d]/g,
23352
24259
  message: "\uC30D\uC810\uC740 \uC55E\uB9D0\uC5D0 \uBD99\uC774\uACE0 \uB4A4\uB294 \uD55C \uCE78 \uB744\uC6C0",
23353
24260
  suggest: "\uC608) \uC6D0\uC7A5: \uAE40\uAC11\uB3D9"
23354
24261
  }
23355
24262
  ];
23356
24263
  function lintGongmunText(text) {
23357
24264
  const findings = [];
23358
- let inFence = false;
24265
+ let fenceMarker = null;
23359
24266
  const lines = text.split(/\r?\n/);
23360
24267
  for (let i = 0; i < lines.length; i++) {
23361
24268
  const line = lines[i];
23362
- if (/^\s*(```|~~~)/.test(line)) {
23363
- inFence = !inFence;
24269
+ const fence = line.match(/^\s*(```+|~~~+)/);
24270
+ if (fence) {
24271
+ const kind = fence[1][0];
24272
+ if (fenceMarker === null) fenceMarker = kind;
24273
+ else if (kind === fenceMarker) fenceMarker = null;
23364
24274
  continue;
23365
24275
  }
23366
- if (inFence) continue;
24276
+ if (fenceMarker !== null) continue;
23367
24277
  for (const r of RULES) {
23368
24278
  r.pattern.lastIndex = 0;
23369
24279
  for (const m of line.matchAll(r.pattern)) {
@@ -26120,7 +27030,7 @@ async function parseHwp(buffer, options) {
26120
27030
  async function parsePdf(buffer, options) {
26121
27031
  let parsePdfDocument;
26122
27032
  try {
26123
- const mod = await import("./parser-3HM2VYKK.js");
27033
+ const mod = await import("./parser-46JGSTPR.js");
26124
27034
  parsePdfDocument = mod.parsePdfDocument;
26125
27035
  } catch {
26126
27036
  return {
@@ -26211,6 +27121,9 @@ async function fillForm(input, values, outputFormat = "markdown") {
26211
27121
  }
26212
27122
 
26213
27123
  export {
27124
+ PRESET_ALIAS,
27125
+ normalizeGongmunPreset,
27126
+ incompatibleGongmunWarnings,
26214
27127
  extractHwpxMetadataOnly,
26215
27128
  extractHwp5MetadataOnly,
26216
27129
  formatFillValue,
@@ -26222,8 +27135,6 @@ export {
26222
27135
  extractFormSchema,
26223
27136
  fillFormFields,
26224
27137
  fillHwpx,
26225
- PRESET_ALIAS,
26226
- normalizeGongmunPreset,
26227
27138
  markdownToHwpx,
26228
27139
  compare,
26229
27140
  diffBlocks,
@@ -26252,4 +27163,4 @@ export {
26252
27163
  parseHwpml,
26253
27164
  fillForm
26254
27165
  };
26255
- //# sourceMappingURL=chunk-AHWRAGXN.js.map
27166
+ //# sourceMappingURL=chunk-MLNHZN32.js.map