kordoc 4.0.3 → 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 (59) hide show
  1. package/README.md +8 -0
  2. package/dist/{-QSF64HNK.js → -Q4ZCVQFI.js} +9 -7
  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-AUJICYJG.js → chunk-DKMKJMR6.js} +4 -3
  8. package/dist/chunk-DKMKJMR6.js.map +1 -0
  9. package/dist/{chunk-6JSZBSOD.js → chunk-KA5AN73R.js} +2 -2
  10. package/dist/{chunk-TQYX5E32.js → chunk-KSVLQ5BC.js} +33 -4
  11. package/dist/chunk-KSVLQ5BC.js.map +1 -0
  12. package/dist/{chunk-K2SETHJ6.js → chunk-MLNHZN32.js} +1589 -765
  13. package/dist/chunk-MLNHZN32.js.map +1 -0
  14. package/dist/{chunk-OVESZY5R.cjs → chunk-NIW7CZYX.cjs} +37 -8
  15. package/dist/chunk-NIW7CZYX.cjs.map +1 -0
  16. package/dist/{chunk-MQLKSRIK.js → chunk-S3KGEEBQ.js} +76 -25
  17. package/dist/chunk-S3KGEEBQ.js.map +1 -0
  18. package/dist/{chunk-RO2GVZ3I.js → chunk-TXNRIVWA.js} +3 -3
  19. package/dist/{chunk-PNQDQ3SM.js → chunk-ZJXR4PWY.js} +33 -4
  20. package/dist/chunk-ZJXR4PWY.js.map +1 -0
  21. package/dist/cli.js +119 -72
  22. package/dist/cli.js.map +1 -1
  23. package/dist/index.cjs +21122 -20246
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +47 -1
  26. package/dist/index.d.ts +47 -1
  27. package/dist/index.js +20944 -20068
  28. package/dist/index.js.map +1 -1
  29. package/dist/mcp.js +106 -83
  30. package/dist/mcp.js.map +1 -1
  31. package/dist/{parser-FMONFTAN.cjs → parser-2ZUTCU2G.cjs} +106 -17
  32. package/dist/parser-2ZUTCU2G.cjs.map +1 -0
  33. package/dist/{parser-AODAQQLQ.js → parser-46JGSTPR.js} +95 -6
  34. package/dist/parser-46JGSTPR.js.map +1 -0
  35. package/dist/{parser-J7YREDNS.js → parser-TLCC67W6.js} +94 -5
  36. package/dist/parser-TLCC67W6.js.map +1 -0
  37. package/dist/profile-io-MRVIJ5T4.js +11 -0
  38. package/dist/render-TTOG6CHW.js +10 -0
  39. package/dist/seal-NMLZ3M3K.js +10 -0
  40. package/dist/seal-NMLZ3M3K.js.map +1 -0
  41. package/dist/{watch-P65Z24AL.js → watch-NZNVJ4YD.js} +7 -7
  42. package/package.json +1 -1
  43. package/dist/chunk-AUJICYJG.js.map +0 -1
  44. package/dist/chunk-K2SETHJ6.js.map +0 -1
  45. package/dist/chunk-MQLKSRIK.js.map +0 -1
  46. package/dist/chunk-OVESZY5R.cjs.map +0 -1
  47. package/dist/chunk-PNQDQ3SM.js.map +0 -1
  48. package/dist/chunk-TQYX5E32.js.map +0 -1
  49. package/dist/parser-AODAQQLQ.js.map +0 -1
  50. package/dist/parser-FMONFTAN.cjs.map +0 -1
  51. package/dist/parser-J7YREDNS.js.map +0 -1
  52. package/dist/render-JPP7YTV3.js +0 -10
  53. package/dist/seal-MDE4DXZQ.js +0 -10
  54. /package/dist/{-QSF64HNK.js.map → -Q4ZCVQFI.js.map} +0 -0
  55. /package/dist/{chunk-6JSZBSOD.js.map → chunk-KA5AN73R.js.map} +0 -0
  56. /package/dist/{chunk-RO2GVZ3I.js.map → chunk-TXNRIVWA.js.map} +0 -0
  57. /package/dist/{render-JPP7YTV3.js.map → profile-io-MRVIJ5T4.js.map} +0 -0
  58. /package/dist/{seal-MDE4DXZQ.js.map → render-TTOG6CHW.js.map} +0 -0
  59. /package/dist/{watch-P65Z24AL.js.map → watch-NZNVJ4YD.js.map} +0 -0
@@ -11,7 +11,7 @@ import {
11
11
  dedupeRunningHeaders,
12
12
  flattenLayoutTables,
13
13
  mapPuaText
14
- } from "./chunk-PNQDQ3SM.js";
14
+ } from "./chunk-ZJXR4PWY.js";
15
15
  import {
16
16
  detectFormat,
17
17
  detectOle2Format,
@@ -32,14 +32,14 @@ import {
32
32
  paraTextPureT,
33
33
  patchZipEntries,
34
34
  scanSectionXml
35
- } from "./chunk-RO2GVZ3I.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-MQLKSRIK.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-AUJICYJG.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-6JSZBSOD.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) {
@@ -205,6 +206,7 @@ async function extractHwpxStyles(zip, decompressed) {
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,8 +864,390 @@ function hmlToLatex(hmlEqStr) {
832
864
  return out;
833
865
  }
834
866
 
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}`);
1064
+ }
1065
+ }
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");
1091
+ }
1092
+ }
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");
1102
+ }
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`);
1105
+ }
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`);
1108
+ }
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
+
835
1250
  // 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
1251
  var HANGUL_JAMO_SEQ = "\u3131\u3134\u3137\u3139\u3141\u3142\u3145\u3147\u3148\u314A\u314B\u314C\u314D\u314E";
838
1252
  function toRoman(n) {
839
1253
  if (n <= 0 || n >= 4e3) return String(n);
@@ -866,12 +1280,14 @@ function formatHeadNumber(n, numFormat) {
866
1280
  switch (numFormat) {
867
1281
  case "DIGIT":
868
1282
  return String(n);
1283
+ // 서수 시퀀스는 생성기(gongmun.ts)와 단일 소스 (v4.0.5) — 종전 mod-14 순환은
1284
+ // 15번째 형제를 다시 '가'로 만들어 한글 실렌더(가→하→거→너 단모음 연속)와 어긋났다
869
1285
  case "CIRCLED_DIGIT":
870
- return n <= 20 ? String.fromCodePoint(9312 + n - 1) : `(${n})`;
1286
+ return n <= 50 ? circledNumber(n - 1) : `(${n})`;
871
1287
  case "HANGUL_SYLLABLE":
872
- return HANGUL_SYLLABLE_SEQ[(n - 1) % HANGUL_SYLLABLE_SEQ.length];
1288
+ return hangulOrdinal(n - 1);
873
1289
  case "CIRCLED_HANGUL_SYLLABLE":
874
- return n <= 14 ? String.fromCodePoint(12910 + n - 1) : HANGUL_SYLLABLE_SEQ[(n - 1) % 14];
1290
+ return n <= 14 ? circledHangul(n - 1) : hangulOrdinal(n - 1);
875
1291
  case "HANGUL_JAMO":
876
1292
  return HANGUL_JAMO_SEQ[(n - 1) % HANGUL_JAMO_SEQ.length];
877
1293
  case "CIRCLED_HANGUL_JAMO":
@@ -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);
@@ -1121,8 +1546,8 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1121
1546
  const { text: rawText, href, footnote, style } = extractParagraphInfo(el, ctx.styleMap, ctx);
1122
1547
  let text = rawText;
1123
1548
  let headingLevel;
1549
+ const ph = resolveParaHeading(el, ctx);
1124
1550
  if (text) {
1125
- const ph = resolveParaHeading(el, ctx);
1126
1551
  if (ph?.prefix) text = ph.prefix + " " + text;
1127
1552
  headingLevel = ph?.headingLevel;
1128
1553
  }
@@ -1131,13 +1556,40 @@ function walkSection(node, blocks, tableCtx, tableStack, ctx, depth = 0) {
1131
1556
  const cell2 = tableCtx.cell;
1132
1557
  if (footnote) text += ` (\uC8FC: ${footnote})`;
1133
1558
  cell2.text += (cell2.text ? "\n" : "") + text;
1134
- (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);
1135
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
+ }
1136
1574
  const block = { type: headingLevel ? "heading" : "paragraph", text, pageNumber: ctx.sectionNum };
1137
1575
  if (headingLevel) block.level = headingLevel;
1138
1576
  if (style) block.style = style;
1139
1577
  if (href) block.href = href;
1140
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
+ }
1141
1593
  blocks.push(block);
1142
1594
  } else {
1143
1595
  blocks.push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
@@ -1219,6 +1671,34 @@ function mergeBlocksIntoCell(cell2, sink) {
1219
1671
  }
1220
1672
  }
1221
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
+ }
1222
1702
  function collectSubListText(el, ctx, depth = 0) {
1223
1703
  if (depth > 10) return "";
1224
1704
  const parts = [];
@@ -1254,6 +1734,15 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth =
1254
1734
  const tag = el.tagName || el.localName || "";
1255
1735
  const localTag = tag.replace(/^[^:]+:/, "");
1256
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
+ }
1257
1746
  if (tableCtx) tableStack.push(tableCtx);
1258
1747
  const newTable = { rows: [], currentRow: [], cell: null };
1259
1748
  walkSection(el, blocks, newTable, tableStack, ctx, d + 1);
@@ -1587,6 +2076,55 @@ function extractParagraphInfo(para2, styleMap, ctx) {
1587
2076
  }
1588
2077
  return { text: cleanText, href, footnote, style };
1589
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
+ }
1590
2128
 
1591
2129
  // src/hwpx/images.ts
1592
2130
  function imageExtToMime(ext) {
@@ -1776,6 +2314,17 @@ function extractFromBrokenZip(buffer) {
1776
2314
  const markdown = blocksToMarkdown(blocks);
1777
2315
  return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
1778
2316
  }
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
+ }
1779
2328
  async function resolveSectionPaths(zip) {
1780
2329
  const manifestPaths = ["Contents/content.hpf", "content.hpf"];
1781
2330
  for (const mp of manifestPaths) {
@@ -1906,6 +2455,7 @@ async function parseHwpxDocument(buffer, options) {
1906
2455
  const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
1907
2456
  const blocks = [];
1908
2457
  const shared = createSectionShared();
2458
+ shared.kordocLayout = await readKordocLayout(zip);
1909
2459
  let parsedSections = 0;
1910
2460
  for (let si = 0; si < sectionPaths.length; si++) {
1911
2461
  if (pageFilter && !pageFilter.has(si + 1)) continue;
@@ -19738,364 +20288,29 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
19738
20288
  for (const k of l.matchKeys) failedKeys.add(k);
19739
20289
  continue;
19740
20290
  }
19741
- for (const k of l.matchKeys) succeededKeys.add(k);
19742
- splices.push(...paraSplices);
19743
- }
19744
- if (splices.length > 0) {
19745
- splices.push(...allLinesegRemovalSplices(xml));
19746
- replacements.set(sectionPaths[si], encoder.encode(applySplices(xml, splices)));
19747
- }
19748
- }
19749
- for (const k of failedKeys) {
19750
- if (!succeededKeys.has(k)) matchedLabels.delete(k);
19751
- }
19752
- const cleanFilled = filled.filter((f) => f !== null);
19753
- const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
19754
- const out = replacements.size > 0 ? patchZipEntries(u8, replacements) : new Uint8Array(u8);
19755
- return {
19756
- buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
19757
- filled: cleanFilled,
19758
- unmatched
19759
- };
19760
- }
19761
-
19762
- // src/hwpx/generator.ts
19763
- import JSZip6 from "jszip";
19764
-
19765
- // src/hwpx/gaejosik.ts
19766
- var GAEJOSIK_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
19767
- function gaejosikMarker(depth, bullet2 = "\u25CB") {
19768
- if (depth === 1) return bullet2;
19769
- return GAEJOSIK_BULLETS[Math.min(depth, GAEJOSIK_BULLETS.length - 1)];
19770
- }
19771
- function gaejosikSizes(bodyHeight, ov = {}) {
19772
- const s = bodyHeight / 1500;
19773
- const r = (v, pt) => pt ? Math.round(pt * 100) : Math.round(v * s);
19774
- return {
19775
- /** □ 대항목 — HY헤드라인M 16pt */
19776
- dae: r(1600, ov.dae),
19777
- /** ※ 참고 — 한양중고딕 13pt */
19778
- cham: r(1300, ov.cham),
19779
- /** 장 헤더(로마숫자·제목) 17pt */
19780
- chapter: r(1700, ov.chapter),
19781
- /** 표지 제목 30pt */
19782
- coverTitle: r(3e3, ov.coverTitle),
19783
- /** 표지 날짜·기관명 25pt */
19784
- coverSub: r(2500, ov.coverSub),
19785
- /** 목차 라벨 "목 차" 28pt */
19786
- tocLabel: r(2800, ov.tocLabel),
19787
- /** 목차 로마숫자 21pt(한양신명조) */
19788
- tocRoman: r(2100, ov.tocRoman),
19789
- /** 목차 항목 18pt */
19790
- tocItem: r(1800, ov.tocItem),
19791
- /** 표 셀 — 맑은 고딕 12pt (실측: 정부 양식 표) */
19792
- table: r(1200, ov.table),
19793
- /** 표지 장식 바 셀 빈 문단 — 6pt (실측: 바 높이 818 안에 줄높이 수납) */
19794
- bar: r(600),
19795
- /** 본문 첫 페이지 제목 박스 — HY헤드라인M 22pt (실측: GT3 표④) */
19796
- bodyTitle: r(2200, ov.bodyTitle)
19797
- };
19798
- }
19799
- function gaejosikSpaceBefore(depth, bodyHeight) {
19800
- const s = bodyHeight / 1500;
19801
- const ui = [3e3, 2e3, 1200, 600][Math.min(depth, 3)];
19802
- return Math.round(ui * s);
19803
- }
19804
- function gaejosikLevelIndent(depth, bodyHeight, sizes = {}, bullet2 = "\u25CB") {
19805
- const lefts = [0, 1, 1.5, 2];
19806
- const left = depth <= 3 ? Math.round(lefts[depth] * bodyHeight) : Math.round((2 + (depth - 3) * 0.5) * bodyHeight);
19807
- const h = depth === 0 ? gaejosikSizes(bodyHeight, sizes).dae : bodyHeight;
19808
- return { left, indent: -markerWidth(gaejosikMarker(depth, bullet2), h) };
19809
- }
19810
- function gaejosikChamIndent(bodyHeight, sizes = {}) {
19811
- const cham = gaejosikSizes(bodyHeight, sizes).cham;
19812
- return { left: Math.round(2.5 * cham), indent: -markerWidth("\u203B", cham) };
19813
- }
19814
- function gaejosikTocItemIndent(bodyHeight, sizes = {}) {
19815
- const sz = gaejosikSizes(bodyHeight, sizes);
19816
- const hang = markerWidth("\u2167.", sz.tocRoman);
19817
- return { left: 2e3 + hang, indent: -hang };
19818
- }
19819
- function formatGaejosikDate(d) {
19820
- return `${d.getFullYear()}. ${d.getMonth() + 1}. ${d.getDate()}.`;
19821
- }
19822
- function stripChapterNumber(title) {
19823
- return title.replace(/^\s*(?:\d+|[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ]+)\s*[.)]\s*/u, "").trim();
19824
- }
19825
- var GAEJOSIK_COLORS = {
19826
- /** 장헤더 로마숫자 셀·표지 진한 바 음영 */
19827
- primary: "#193AAA",
19828
- /** 장헤더 셀 테두리 */
19829
- border: "#006699",
19830
- /** 표지 연한 바 */
19831
- accent: "#A0B4E6",
19832
- /** 장헤더 제목 셀 음영 */
19833
- titleFill: "#F2F2F2",
19834
- /** 장헤더 제목 셀 상하선 */
19835
- titleLine: "#A6A6A6",
19836
- /** 목차 박스 테두리 */
19837
- tocBorder: "#514BAC",
19838
- /** 목차 배너 라벤더 스트라이프 (실측: GT3 표②) */
19839
- tocStripe: "#E0E5FA"
19840
- };
19841
- var CHAPTER_GEOM = { numW: 3327, gapW: 848, titleW: 43513, rowH: 2832 };
19842
- var COVER_GEOM = {
19843
- totalW: 48180,
19844
- topDarkW: 38219,
19845
- topLightW: 9961,
19846
- barH: 818,
19847
- titleH: 11060,
19848
- botDarkW: 10466,
19849
- botLightW: 37714,
19850
- botBarH: 812
19851
- };
19852
- var TOC_GEOM = { boxW: 46492, boxH: 57840 };
19853
- function chapterGeom(bodyHeight, ov = {}) {
19854
- const s = gaejosikSizes(bodyHeight, ov).chapter / 1700;
19855
- return { ...CHAPTER_GEOM, rowH: Math.round(CHAPTER_GEOM.rowH * s) };
19856
- }
19857
- function coverGeom(bodyHeight, ov = {}) {
19858
- const s = gaejosikSizes(bodyHeight, ov).coverTitle / 3e3;
19859
- return { ...COVER_GEOM, titleH: Math.round(COVER_GEOM.titleH * s) };
19860
- }
19861
- var TOC_BANNER_GEOM = {
19862
- colWs: [565, 565, 1414, 13191, 1414, 565, 565],
19863
- h: 3931
19864
- };
19865
- function tocBannerGeom(bodyHeight, ov = {}) {
19866
- const s = gaejosikSizes(bodyHeight, ov).tocLabel / 2800;
19867
- return { colWs: TOC_BANNER_GEOM.colWs, h: Math.round(TOC_BANNER_GEOM.h * s) };
19868
- }
19869
- var BODY_TITLE_GEOM = { barH: 600, titleH: 3566 };
19870
- function bodyTitleGeom(bodyHeight, ov = {}) {
19871
- const s = gaejosikSizes(bodyHeight, ov).bodyTitle / 2200;
19872
- return { barH: BODY_TITLE_GEOM.barH, titleH: Math.round(BODY_TITLE_GEOM.titleH * s) };
19873
- }
19874
-
19875
- // src/hwpx/gongmun.ts
19876
- var OFFICIAL_MARGINS = { top: 20, bottom: 15, left: 20, right: 15 };
19877
- var GAEJOSIK_MARGINS = { top: 15, bottom: 15, left: 20, right: 20 };
19878
- var GAEJOSIK_HEADER_FOOTER = 4251;
19879
- var PRESET_DEFAULTS = {
19880
- // 서울시 실결재 104건 중 12pt 64건(13pt 30, 15pt 5) — 지배값을 기본으로 사용.
19881
- official: { bodyPt: 12, lineSpacing: 160, numbering: "standard" },
19882
- report: { bodyPt: 15, lineSpacing: 160, numbering: "report" },
19883
- // 실측 추진계획안: □ → ㅇ → * 계층.
19884
- plan: { bodyPt: 15, lineSpacing: 160, numbering: "report" },
19885
- notice: { bodyPt: 15, lineSpacing: 160, numbering: "standard" },
19886
- minutes: { bodyPt: 14, lineSpacing: 130, numbering: "standard" },
19887
- gaejosik: { bodyPt: 15, lineSpacing: 160, numbering: "gaejosik" },
19888
- // 보도자료 — 실측(국토부 실물): 본문 바탕 14pt 160%, □→ㅇ→*(각주) 부호
19889
- press: { bodyPt: 14, lineSpacing: 160, numbering: "report" }
19890
- };
19891
- var PRESET_ALIAS = {
19892
- official: "official",
19893
- \uAE30\uC548\uBB38: "official",
19894
- \uC2DC\uD589\uBB38: "official",
19895
- \uACF5\uBB38: "official",
19896
- \uACF5\uBB38\uC11C: "official",
19897
- report: "report",
19898
- \uBCF4\uACE0\uC11C: "report",
19899
- plan: "plan",
19900
- \uACC4\uD68D\uC11C: "plan",
19901
- \uACC4\uD68D: "plan",
19902
- notice: "notice",
19903
- \uD1B5\uC9C0: "notice",
19904
- \uC54C\uB9BC: "notice",
19905
- \uC548\uB0B4: "notice",
19906
- minutes: "minutes",
19907
- \uD68C\uC758\uB85D: "minutes",
19908
- gaejosik: "gaejosik",
19909
- \uAC1C\uC870\uC2DD: "gaejosik",
19910
- \uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
19911
- \uC815\uBD80\uBCF4\uACE0\uC11C: "gaejosik",
19912
- \uC815\uBD80\uD45C\uC900\uAC1C\uC870\uC2DD\uBCF4\uACE0\uC11C: "gaejosik",
19913
- press: "press",
19914
- \uBCF4\uB3C4\uC790\uB8CC: "press"
19915
- };
19916
- function normalizeGongmunPreset(preset) {
19917
- if (!preset) return "official";
19918
- return PRESET_ALIAS[preset.trim()] ?? "official";
19919
- }
19920
- function usesReportFonts(preset) {
19921
- return preset === "gaejosik" || preset === "report" || preset === "plan";
19922
- }
19923
- function usesAsteriskThird(preset) {
19924
- return preset === "plan" || preset === "press";
19925
- }
19926
- function needsGaejosikAssets(gongmun) {
19927
- return usesReportFonts(gongmun.preset) || gongmun.cover !== null || gongmun.toc;
19928
- }
19929
- function assertFiniteRange(name, value, min, max) {
19930
- if (!Number.isFinite(value) || value < min || value > max) {
19931
- throw new KordocError(`${name} must be a finite number between ${min} and ${max}`);
19932
- }
19933
- }
19934
- function validateGongmunOptions(opts) {
19935
- if (opts.bodyPt !== void 0) assertFiniteRange("bodyPt", opts.bodyPt, 6, 40);
19936
- if (opts.lineSpacing !== void 0) assertFiniteRange("lineSpacing", opts.lineSpacing, 50, 300);
19937
- if (typeof opts.autoFit === "object" && opts.autoFit.minRatio !== void 0) {
19938
- assertFiniteRange("autoFit.minRatio", opts.autoFit.minRatio, 50, 99);
19939
- }
19940
- if (opts.margins) {
19941
- for (const side of ["top", "bottom", "left", "right"]) {
19942
- const value = opts.margins[side];
19943
- assertFiniteRange(`margins.${side}`, value, 0, 200);
19944
- }
19945
- if (opts.margins.left + opts.margins.right >= 210) {
19946
- throw new KordocError("margins.left + margins.right must be less than 210");
19947
- }
19948
- if (opts.margins.top + opts.margins.bottom >= 297) {
19949
- throw new KordocError("margins.top + margins.bottom must be less than 297");
19950
- }
19951
- }
19952
- if (opts.sizes) {
19953
- for (const [key, value] of Object.entries(opts.sizes)) {
19954
- if (value !== void 0) assertFiniteRange(`sizes.${key}`, value, 6, 60);
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)));
19955
20297
  }
19956
20298
  }
19957
- if (opts.approval && opts.approval.length > 6) {
19958
- throw new KordocError("approval must contain at most 6 labels");
20299
+ for (const k of failedKeys) {
20300
+ if (!succeededKeys.has(k)) matchedLabels.delete(k);
19959
20301
  }
19960
- }
19961
- function resolveGongmun(opts) {
19962
- validateGongmunOptions(opts);
19963
- const preset = normalizeGongmunPreset(opts.preset);
19964
- const d = PRESET_DEFAULTS[preset];
19965
- const bodyPt = opts.bodyPt ?? d.bodyPt;
19966
- const autoFitMinRatio = opts.autoFit === false ? null : typeof opts.autoFit === "object" ? Math.min(Math.max(opts.autoFit.minRatio ?? 90, 50), 99) : 90;
19967
- const coverOn = opts.cover !== void 0 ? opts.cover !== false : preset === "gaejosik";
19968
- const coverOpts = typeof opts.cover === "object" ? opts.cover : {};
19969
- const gaejosik = preset === "gaejosik";
19970
- const reportFamily = gaejosik || preset === "report" || preset === "plan" || preset === "notice" || preset === "press";
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);
19971
20305
  return {
19972
- preset,
19973
- bodyFont: opts.bodyFont ?? "myeongjo",
19974
- bodyHeight: Math.round(bodyPt * 100),
19975
- lineSpacing: opts.lineSpacing ?? d.lineSpacing,
19976
- numbering: opts.numbering ?? d.numbering,
19977
- margins: opts.margins ?? (reportFamily ? GAEJOSIK_MARGINS : OFFICIAL_MARGINS),
19978
- centerTitle: opts.centerTitle ?? true,
19979
- autoFitMinRatio,
19980
- // 보도자료는 머리박스가 1페이지 최상단을 차지하는 서식이라 표지·목차와 양립 불가 —
19981
- // 켜면 머리박스가 표지에 얹히고 25pt 제목·부제가 유실된다 (docHead 프리셋 게이팅과 동일 관례)
19982
- cover: coverOn && preset !== "press" ? { date: coverOpts.date ?? null, org: coverOpts.org ?? "" } : null,
19983
- toc: preset !== "press" && (opts.toc ?? gaejosik),
19984
- fonts: opts.fonts ?? {},
19985
- sizes: opts.sizes ?? {},
19986
- // 쪽번호 — 보고서 계열 관행(실측: 2_보고서 양식·추진계획·공고문 전부 하단 중앙)
19987
- pageNumbers: opts.pageNumbers ?? (gaejosik || preset === "report" || preset === "plan"),
19988
- // 머리말·꼬리말 — 실측: 보고서 계열 15mm(GT3·t2·춘천·브라더), 공고·보도 10mm,
19989
- // 기안문 0(실결재 41/60건 h0/f0)
19990
- headerFooter: usesReportFonts(preset) ? GAEJOSIK_HEADER_FOOTER : preset === "notice" || preset === "press" ? 2835 : 0,
19991
- // "끝." — 기안문 규정(본문 끝 2타+"끝."). 그 외는 opt-in
19992
- endMark: opts.endMark ?? preset === "official",
19993
- approval: opts.approval && opts.approval.length > 0 ? opts.approval : null,
19994
- // 본문 제목박스 — 실측(GT3·GT12): 목차 뒤 본문 시작에 제목 반복. 표지 켜진 개조식 기본
19995
- bodyTitleBox: opts.bodyTitleBox ?? (gaejosik && coverOn),
19996
- // h2 말머리 — 실측: 보고서 양식 □ 대항목(QA-2), 공고문 아라비아("1. 사업개요", 바이오헬스 실측)
19997
- h2Marker: opts.h2Marker ?? (preset === "report" || preset === "plan" ? "box" : preset === "notice" ? "number" : "none"),
19998
- // 2단계 부호 — 실결재 기안문·공고문 ㅇ 지배(60건 중 ㅇ134:○5), 보고서 양식 계열 ○
19999
- bullet2: opts.bullet2 ?? (preset === "plan" || preset === "notice" || preset === "press" ? "\u3147" : "\u25CB"),
20000
- // 단일 형제 부호 생략 — 규정이지만 부호 없는 계단이 실무 눈에 어색 (실무자 QA)
20001
- suppressSingle: opts.suppressSingle ?? false,
20002
- docHead: preset === "official" && opts.docHead ? opts.docHead : null,
20003
- docFoot: preset === "official" && opts.docFoot ? opts.docFoot : null,
20004
- reportInfo: opts.reportInfo?.trim() || null,
20005
- noticeHead: preset === "notice" && opts.noticeHead ? opts.noticeHead : null,
20006
- // 보도자료 머리박스는 프리셋 자체가 요구 — 옵션 미지정이어도 빈 객체
20007
- press: preset === "press" ? opts.press ?? {} : null
20306
+ buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
20307
+ filled: cleanFilled,
20308
+ unmatched
20008
20309
  };
20009
20310
  }
20010
- var HANGUL_INITIALS = [0, 2, 3, 5, 6, 7, 9, 11, 12, 14, 15, 16, 17, 18];
20011
- var HANGUL_MEDIALS = [0, 4, 8, 13, 18, 20];
20012
- function hangulOrdinal(n) {
20013
- const cols = HANGUL_INITIALS.length;
20014
- const vowel = HANGUL_MEDIALS[Math.min(Math.floor(n / cols), HANGUL_MEDIALS.length - 1)];
20015
- const init = HANGUL_INITIALS[n % cols];
20016
- return String.fromCodePoint(44032 + init * 588 + vowel * 28);
20017
- }
20018
- function circledNumber(n) {
20019
- return String.fromCodePoint(9312 + n % 20);
20020
- }
20021
- function circledHangul(n) {
20022
- return String.fromCodePoint(12910 + n % 14);
20023
- }
20024
- var REPORT_BULLETS = ["\u25A1", "\u25CB", "-", "\u318D"];
20025
- var ASTERISK_BULLETS = ["\u25A1", "\u25CB", "*", "\u318D"];
20026
- function standardMarker(depth, n) {
20027
- switch (depth) {
20028
- case 0:
20029
- return `${n + 1}.`;
20030
- case 1:
20031
- return `${hangulOrdinal(n)}.`;
20032
- case 2:
20033
- return `${n + 1})`;
20034
- case 3:
20035
- return `${hangulOrdinal(n)})`;
20036
- case 4:
20037
- return `(${n + 1})`;
20038
- case 5:
20039
- return `(${hangulOrdinal(n)})`;
20040
- case 6:
20041
- return circledNumber(n);
20042
- case 7:
20043
- return circledHangul(n);
20044
- default:
20045
- return circledHangul(n);
20046
- }
20047
- }
20048
- function reportMarker(depth, bullet2 = "\u25CB", asteriskThird = false) {
20049
- const bullets = asteriskThird ? ASTERISK_BULLETS : REPORT_BULLETS;
20050
- const m = bullets[Math.min(depth, bullets.length - 1)];
20051
- return depth === 1 ? bullet2 : m;
20052
- }
20053
- function markerWidth(marker, bodyHeight) {
20054
- let em = SPACE_EM_FIXED;
20055
- for (const c of marker) em += charWidthEm1000(c.codePointAt(0));
20056
- return Math.round(em / 1e3 * bodyHeight);
20057
- }
20058
- function levelIndent(depth, bodyHeight, numbering, sizes = {}, bullet2 = "\u25CB", asteriskThird = false) {
20059
- if (numbering === "gaejosik") return gaejosikLevelIndent(depth, bodyHeight, sizes, bullet2);
20060
- const marker = numbering === "report" ? reportMarker(depth, bullet2, asteriskThird) : standardMarker(depth, 0);
20061
- return { left: Math.round(depth * bodyHeight), indent: -markerWidth(marker, bodyHeight) };
20062
- }
20063
- function computeSuppression(depths) {
20064
- const counts = /* @__PURE__ */ new Map();
20065
- const keys = [];
20066
- const path = [];
20067
- for (const depth of depths) {
20068
- path.length = depth + 1;
20069
- path[depth] = (path[depth] ?? 0) + 1;
20070
- const parentKey = path.slice(0, depth).join(".") + "|" + depth;
20071
- keys.push(parentKey);
20072
- counts.set(parentKey, (counts.get(parentKey) ?? 0) + 1);
20073
- }
20074
- return keys.map((k) => (counts.get(k) ?? 0) <= 1);
20075
- }
20076
- var GongmunNumberer = class {
20077
- constructor(numbering, bullet2 = "\u25CB", asteriskThird = false) {
20078
- this.numbering = numbering;
20079
- this.bullet2 = bullet2;
20080
- this.asteriskThird = asteriskThird;
20081
- }
20082
- counts = [];
20083
- /** depth 항목 하나에 대한 마커. suppress=true면 빈 문자열(부호 없음) */
20084
- next(depth, suppress) {
20085
- this.counts.length = depth + 1;
20086
- const n = this.counts[depth] ?? 0;
20087
- this.counts[depth] = n + 1;
20088
- if (suppress) return "";
20089
- if (this.numbering === "gaejosik") return gaejosikMarker(depth, this.bullet2);
20090
- return this.numbering === "report" ? reportMarker(depth, this.bullet2, this.asteriskThird) : standardMarker(depth, n);
20091
- }
20092
- reset() {
20093
- this.counts = [];
20094
- }
20095
- };
20096
- function mmToHwpunit(mm) {
20097
- return Math.round(mm * 7200 / 25.4);
20098
- }
20311
+
20312
+ // src/hwpx/generator.ts
20313
+ import JSZip6 from "jszip";
20099
20314
 
20100
20315
  // src/hwpx/gen-ids.ts
20101
20316
  var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
@@ -20140,7 +20355,7 @@ function resolveTheme(theme) {
20140
20355
  };
20141
20356
  }
20142
20357
  function escapeXml(text) {
20143
- 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;");
20144
20359
  }
20145
20360
  function headingParaPrId(level) {
20146
20361
  if (level === 1) return PARA_H1;
@@ -20159,7 +20374,7 @@ function charPr(id, height, bold, italic, fontId = 0, textColor = DEFAULT_TEXT_C
20159
20374
  const italicAttr = italic ? ` italic="1"` : "";
20160
20375
  const effFont = fontId;
20161
20376
  const boldEl = bold ? `<hh:bold/>` : "";
20162
- 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}>
20163
20378
  <hh:fontRef hangul="${effFont}" latin="${effFont}" hanja="${effFont}" japanese="${effFont}" other="${effFont}" symbol="${effFont}" user="${effFont}"/>
20164
20379
  <hh:ratio hangul="${ratioPct}" latin="${ratioPct}" hanja="${ratioPct}" japanese="100" other="100" symbol="100" user="100"/>
20165
20380
  <hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
@@ -20205,7 +20420,7 @@ var GJ_CHAR_BAR = 24;
20205
20420
  var GJ_CHAR_BODY_TITLE = 25;
20206
20421
  var GJ_CHAR_TITLE_BAR = 26;
20207
20422
  var GJ_CHAR_APPROVAL = 27;
20208
- var GJ_CHAR_COUNT = 17;
20423
+ var GJ_CHAR_COUNT = GJ_CHAR_APPROVAL - GJ_CHAR_DAE + 1;
20209
20424
  var GJ_PARA_CHAM = 20;
20210
20425
  var GJ_PARA_COVER = 21;
20211
20426
  var GJ_PARA_TOC_ITEM = 22;
@@ -20213,6 +20428,7 @@ var GJ_PARA_CHAPTER = 23;
20213
20428
  var GJ_PARA_BAR = 24;
20214
20429
  var GONGMUN_LIST_PLAIN_BASE = 25;
20215
20430
  var GONGMUN_PARA_APPROVAL = GONGMUN_LIST_PLAIN_BASE + GONGMUN_LIST_LEVELS;
20431
+ var GONGMUN_LIST_VARIANT_BASE = GONGMUN_PARA_APPROVAL + 1;
20216
20432
  var GJ_BF_CHAPTER_NUM = 3;
20217
20433
  var GJ_BF_CHAPTER_GAP = 4;
20218
20434
  var GJ_BF_CHAPTER_TITLE = 5;
@@ -20256,10 +20472,10 @@ var GONGMUN_TBL_CHAR_BOLD = 12;
20256
20472
  var GONGMUN_TBL_PT = 1200;
20257
20473
  var GONGMUN_TITLE_BAR_CHAR = 13;
20258
20474
  var GONGMUN_APPROVAL_CHAR = 14;
20259
- var CHAR_VARIANT_BASE = 11;
20475
+ var CHAR_VARIANT_BASE = CHAR_QUOTE + 1;
20260
20476
  function charVariantBase(richAssets, isGongmun = true) {
20261
20477
  if (!isGongmun) return CHAR_VARIANT_BASE;
20262
- return richAssets ? 11 + GJ_CHAR_COUNT : 11 + 4;
20478
+ return richAssets ? CHAR_VARIANT_BASE + GJ_CHAR_COUNT : CHAR_VARIANT_BASE + (GONGMUN_APPROVAL_CHAR - GONGMUN_TBL_CHAR + 1);
20263
20479
  }
20264
20480
  var GONGMUN_BODY_RATIO = 95;
20265
20481
 
@@ -20292,6 +20508,7 @@ function parseMarkdownToBlocks(md2) {
20292
20508
  const lines = md2.replace(/\r\n?/g, "\n").split("\n");
20293
20509
  const blocks = [];
20294
20510
  let i = 0;
20511
+ const listStack = [];
20295
20512
  while (i < lines.length) {
20296
20513
  const line = lines[i];
20297
20514
  if (!line.trim()) {
@@ -20380,7 +20597,8 @@ function parseMarkdownToBlocks(md2) {
20380
20597
  const tableRows = [];
20381
20598
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
20382
20599
  const row = lines[i];
20383
- if (/^[\s|:\-]+$/.test(row)) {
20600
+ const sepCells = row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
20601
+ if (sepCells.every((c) => /^\s*:?-+:?\s*$/.test(c))) {
20384
20602
  i++;
20385
20603
  continue;
20386
20604
  }
@@ -20394,17 +20612,36 @@ function parseMarkdownToBlocks(md2) {
20394
20612
  if (line.trimStart().startsWith("> ")) {
20395
20613
  const quoteLines = [];
20396
20614
  while (i < lines.length && (lines[i].trimStart().startsWith("> ") || lines[i].trimStart().startsWith(">"))) {
20397
- quoteLines.push(lines[i].replace(/^>\s?/, ""));
20615
+ quoteLines.push(lines[i].replace(/^>\s?/, "").trim());
20398
20616
  i++;
20399
20617
  }
20618
+ let joined = [];
20400
20619
  for (const ql of quoteLines) {
20401
- 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 = [];
20402
20626
  }
20627
+ if (joined.length) blocks.push({ type: "blockquote", text: joined.join("\n") });
20403
20628
  continue;
20404
20629
  }
20405
- const listMatch = line.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
20630
+ const listLine = line.replace(/^[\t ]+/, (ws) => ws.replace(/\t/g, " "));
20631
+ const listMatch = listLine.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
20406
20632
  if (listMatch) {
20407
- 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
+ }
20408
20645
  const ordered = /\d/.test(listMatch[2]);
20409
20646
  blocks.push({ type: "list_item", text: listMatch[3].trim(), ordered, indent, marker: listMatch[2] });
20410
20647
  i++;
@@ -20425,7 +20662,7 @@ function parseInlineMarkdown(text) {
20425
20662
  text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_, t, u) => t || u);
20426
20663
  text = text.replace(/~~([^~]+)~~/g, "$1");
20427
20664
  const spans = [];
20428
- 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;
20429
20666
  let lastIdx = 0;
20430
20667
  for (const match of text.matchAll(regex)) {
20431
20668
  const idx = match.index;
@@ -20491,14 +20728,20 @@ function generateContainerXml() {
20491
20728
  </ocf:rootfiles>
20492
20729
  </ocf:container>`;
20493
20730
  }
20494
- function generateManifest(chartParts = []) {
20731
+ function generateManifest(chartParts = [], imageItems = [], layout = "default") {
20495
20732
  const chartItems = chartParts.map((p, i) => `
20496
20733
  <opf:item id="chart${i + 1}" href="${p.name}" media-type="application/xml"/>`).join("");
20734
+ const imgItems = imageItems.map((x) => `
20735
+ ${x}`).join("");
20497
20736
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
20498
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>
20499
20742
  <opf:manifest>
20500
20743
  <opf:item id="header" href="Contents/header.xml" media-type="application/xml"/>
20501
- <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}
20502
20745
  </opf:manifest>
20503
20746
  <opf:spine>
20504
20747
  <opf:itemref idref="header" linear="no"/>
@@ -20506,6 +20749,17 @@ function generateManifest(chartParts = []) {
20506
20749
  </opf:spine>
20507
20750
  </opf:package>`;
20508
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
+ }
20509
20763
  function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls = []) {
20510
20764
  const measured = !!gongmun && usesReportFonts(gongmun.preset);
20511
20765
  const richAssets = !!gongmun && needsGaejosikAssets(gongmun);
@@ -20513,8 +20767,8 @@ function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls
20513
20767
  if (gongmun) {
20514
20768
  body = gongmun.bodyHeight;
20515
20769
  code = Math.max(body - 200, 900);
20516
- h1 = gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700;
20517
- h2 = 1600;
20770
+ h1 = Math.max(gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700, body);
20771
+ h2 = Math.max(1600, body);
20518
20772
  h3 = body;
20519
20773
  h4 = Math.min(h3, Math.max(body - 100, 1300));
20520
20774
  }
@@ -20582,6 +20836,12 @@ function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls
20582
20836
  // 결재란 라벨(맑은 고딕 10pt)
20583
20837
  );
20584
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`
20843
+ );
20844
+ }
20585
20845
  for (const r of ratioVariants) {
20586
20846
  rows.push(
20587
20847
  charPr(rows.length, body, false, false, bodyFont, theme.body, r),
@@ -20591,11 +20851,12 @@ function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls
20591
20851
  );
20592
20852
  }
20593
20853
  rows.push(...extraCharPrXmls);
20854
+ assertSequentialIds(rows, "charPr", 0);
20594
20855
  return `<hh:charProperties itemCnt="${rows.length}">
20595
20856
  ${rows.join("\n")}
20596
20857
  </hh:charProperties>`;
20597
20858
  }
20598
- function buildParaProperties(gongmun) {
20859
+ function buildParaProperties(gongmun, listIndentVariants = []) {
20599
20860
  if (!gongmun) {
20600
20861
  const base2 = [
20601
20862
  paraPr(0, { keepWord: true }),
@@ -20607,6 +20868,7 @@ function buildParaProperties(gongmun) {
20607
20868
  paraPr(6, { align: "LEFT", lineSpacing: 150, indent: 600, keepWord: true }),
20608
20869
  paraPr(7, { align: "LEFT", lineSpacing: 160, indent: 600, keepWord: true })
20609
20870
  ];
20871
+ assertSequentialIds(base2, "paraPr", 0);
20610
20872
  return `<hh:paraProperties itemCnt="${base2.length}">
20611
20873
  ${base2.join("\n")}
20612
20874
  </hh:paraProperties>`;
@@ -20653,6 +20915,14 @@ ${base2.join("\n")}
20653
20915
  base.push(paraPr(GONGMUN_LIST_PLAIN_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent: 0, spaceBefore: sectionGap, keepWord: true }));
20654
20916
  }
20655
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);
20656
20926
  return `<hh:paraProperties itemCnt="${base.length}">
20657
20927
  ${base.join("\n")}
20658
20928
  </hh:paraProperties>`;
@@ -20673,7 +20943,10 @@ function fontEntry(id, face, weight = 6) {
20673
20943
  <hh:typeInfo familyType="FCAT_GOTHIC" weight="${weight}" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20674
20944
  </hh:font>`;
20675
20945
  }
20676
- function buildFontFaces(gongmun, bodyFace) {
20946
+ function staticFontNext(gongmun) {
20947
+ return gongmun && needsGaejosikAssets(gongmun) ? 8 : 3;
20948
+ }
20949
+ function buildFontFaces(gongmun, bodyFace, extraFonts = []) {
20677
20950
  if (gongmun && needsGaejosikAssets(gongmun)) {
20678
20951
  const ov = gongmun.fonts;
20679
20952
  const bodyDefault = usesReportFonts(gongmun.preset) ? gongmun.preset !== "gaejosik" && gongmun.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD734\uBA3C\uBA85\uC870" : bodyFace;
@@ -20687,21 +20960,27 @@ function buildFontFaces(gongmun, bodyFace) {
20687
20960
  "\uD55C\uC591\uC2E0\uBA85\uC870",
20688
20961
  ov.table ?? "\uB9D1\uC740 \uACE0\uB515"
20689
20962
  ];
20690
- 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");
20691
20967
  const langs = ["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
20692
20968
  return `<hh:fontfaces itemCnt="${langs.length}">
20693
- ` + langs.map((l) => ` <hh:fontface lang="${l}" fontCnt="${faces.length}">
20969
+ ` + langs.map((l) => ` <hh:fontface lang="${l}" fontCnt="${faces.length + extraFonts.length}">
20694
20970
  ${list}
20695
20971
  </hh:fontface>`).join("\n") + `
20696
20972
  </hh:fontfaces>`;
20697
20973
  }
20974
+ const extraH = extraFonts.map((f, i) => fontEntry(3 + i, f)).join("\n");
20975
+ const extraBlock = extraH ? `
20976
+ ${extraH}` : "";
20698
20977
  return `<hh:fontfaces itemCnt="7">
20699
- <hh:fontface lang="HANGUL" fontCnt="3">
20978
+ <hh:fontface lang="HANGUL" fontCnt="${3 + extraFonts.length}">
20700
20979
  ${fontEntry(0, bodyFace)}
20701
20980
  ${fontEntry(1, "\uD568\uCD08\uB86C\uB3CB\uC6C0")}
20702
- ${fontEntry(2, "HY\uACAC\uACE0\uB515", 9)}
20981
+ ${fontEntry(2, "HY\uACAC\uACE0\uB515", 9)}${extraBlock}
20703
20982
  </hh:fontface>
20704
- <hh:fontface lang="LATIN" fontCnt="3">
20983
+ <hh:fontface lang="LATIN" fontCnt="${3 + extraFonts.length}">
20705
20984
  <hh:font id="0" face="Times New Roman" type="TTF" isEmbedded="0">
20706
20985
  <hh:typeInfo familyType="FCAT_OLDSTYLE" weight="5" proportion="4" contrast="2" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="4"/>
20707
20986
  </hh:font>
@@ -20710,7 +20989,7 @@ ${fontEntry(2, "HY\uACAC\uACE0\uB515", 9)}
20710
20989
  </hh:font>
20711
20990
  <hh:font id="2" face="Arial Black" type="TTF" isEmbedded="0">
20712
20991
  <hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
20713
- </hh:font>
20992
+ </hh:font>${extraBlock}
20714
20993
  </hh:fontface>
20715
20994
  <hh:fontface lang="HANJA" fontCnt="1">
20716
20995
  ${fontEntry(0, "\uD568\uCD08\uB86C\uBC14\uD0D5")}
@@ -20729,7 +21008,7 @@ ${fontEntry(0, "\uAD74\uB9BC")}
20729
21008
  </hh:fontface>
20730
21009
  </hh:fontfaces>`;
20731
21010
  }
20732
- function buildBorderFills(gongmun, extra = []) {
21011
+ function staticBorderFillItems(gongmun) {
20733
21012
  const thin = ["0.12 mm", "#000000"];
20734
21013
  const items = [
20735
21014
  borderFillEntry(1, {}),
@@ -20760,7 +21039,14 @@ function buildBorderFills(gongmun, extra = []) {
20760
21039
  if (gongmun) {
20761
21040
  items.push(borderFillEntry(items.length + 1, { l: thin, r: thin, t: thin, b: thin }, "#E6E6E6"));
20762
21041
  }
20763
- 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);
20764
21050
  return `<hh:borderFills itemCnt="${items.length}">
20765
21051
  ${items.join("\n")}
20766
21052
  </hh:borderFills>`;
@@ -20778,15 +21064,15 @@ function buildStyles(gongmun) {
20778
21064
  ${items.join("\n ")}
20779
21065
  </hh:styles>`;
20780
21066
  }
20781
- function generateHeaderXml(theme, gongmun, ratioVariants = [], extraBorderFills = [], extraCharPrXmls = []) {
21067
+ function generateHeaderXml(theme, gongmun, ratioVariants = [], extraBorderFills = [], extraCharPrXmls = [], listIndentVariants = [], extraFonts = []) {
20782
21068
  const bodyFace = gongmun?.fonts.body ?? (gongmun?.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD568\uCD08\uB86C\uBC14\uD0D5");
20783
21069
  const charPropsXml = buildCharProperties(theme, gongmun, ratioVariants, extraCharPrXmls);
20784
- const paraPropsXml = buildParaProperties(gongmun);
21070
+ const paraPropsXml = buildParaProperties(gongmun, gongmun ? listIndentVariants : []);
20785
21071
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
20786
21072
  <hh:head xmlns:hh="${NS_HEAD}" xmlns:hp="${NS_PARA}" xmlns:hc="${NS_CORE}" version="1.4" secCnt="1">
20787
21073
  <hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
20788
21074
  <hh:refList>
20789
- ${buildFontFaces(gongmun, bodyFace)}
21075
+ ${buildFontFaces(gongmun, bodyFace, extraFonts)}
20790
21076
  ${buildBorderFills(gongmun, extraBorderFills)}
20791
21077
  ${charPropsXml}
20792
21078
  <hh:tabProperties itemCnt="0"/>
@@ -20860,7 +21146,6 @@ function chartSpec(t) {
20860
21146
  const key = CHART_ALIAS[t.toLowerCase()] ?? Number(t);
20861
21147
  return CHART_TYPES[key] ?? CHART_TYPES[0];
20862
21148
  }
20863
- var HU_PER_MM = 7200 / 25.4;
20864
21149
  var RESERVED_KEYS = /* @__PURE__ */ new Set(["type", "cat", "size", "colors", "point_colors", "title"]);
20865
21150
  function parseChartFence(text) {
20866
21151
  let type;
@@ -21019,7 +21304,7 @@ function plainRenderText(text) {
21019
21304
  function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
21020
21305
  const minRatio = gongmun.autoFitMinRatio;
21021
21306
  if (minRatio === null || minRatio >= GONGMUN_BODY_RATIO) return null;
21022
- 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);
21023
21308
  const ratioByBlock = /* @__PURE__ */ new Map();
21024
21309
  const variants = [];
21025
21310
  for (let i = 0; i < blocks.length; i++) {
@@ -21027,14 +21312,14 @@ function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
21027
21312
  let text;
21028
21313
  let firstW;
21029
21314
  let contW;
21030
- if (block.type === "list_item" && gongmunList.has(i)) {
21031
- 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);
21032
21317
  if (gongmun.numbering === "gaejosik" && (depth === 0 || (block.text || "").trimStart().startsWith("\u203B"))) continue;
21033
21318
  const content = plainRenderText(block.text || "");
21034
21319
  text = marker ? `${marker} ${content}` : content;
21035
21320
  const li = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
21036
21321
  const left = li.left;
21037
- const indent = marker ? li.indent : 0;
21322
+ const indent = marker ? indentVariant !== void 0 ? -gongmunList.indentVariants[indentVariant].widthHu : li.indent : 0;
21038
21323
  firstW = pageW - left - Math.max(indent, 0);
21039
21324
  contW = pageW - left - Math.max(-indent, 0);
21040
21325
  } else if (block.type === "paragraph") {
@@ -21060,9 +21345,55 @@ function variantMapper(fit, blockIdx, base = CHAR_VARIANT_BASE) {
21060
21345
  const vi = fit.variants.indexOf(r);
21061
21346
  return (id) => id >= 0 && id <= 3 ? base + vi * 4 + id : id;
21062
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
+ }
21063
21377
  function precomputeGongmunList(blocks, gongmun) {
21064
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
+ };
21065
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);
21066
21397
  let i = 0;
21067
21398
  while (i < blocks.length) {
21068
21399
  if (blocks[i].type !== "list_item") {
@@ -21089,14 +21420,21 @@ function precomputeGongmunList(blocks, gongmun) {
21089
21420
  break;
21090
21421
  }
21091
21422
  const depths = run.map((bi) => Math.min(Math.max((blocks[bi].indent || 0) + depthOffset, 0), GONGMUN_LIST_LEVELS - 1));
21092
- const suppress = gongmun.suppressSingle && gongmun.numbering === "standard" ? computeSuppression(depths) : depths.map(() => false);
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);
21093
21428
  const numberer = new GongmunNumberer(gongmun.numbering, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
21094
21429
  run.forEach((bi, k) => {
21095
- const marker = numberer.next(depths[k], suppress[k]);
21096
- 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]) });
21097
21435
  });
21098
21436
  }
21099
- return result;
21437
+ return { items: result, indentVariants };
21100
21438
  }
21101
21439
 
21102
21440
  // src/hwpx/gen-gaejosik.ts
@@ -21114,26 +21452,29 @@ function emptyPara(paraPrId = PARA_NORMAL, pageBreak = false, charPrId = CHAR_NO
21114
21452
  const brk = pageBreak ? ` pageBreak="1"` : "";
21115
21453
  return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"${brk}><hp:run charPrIDRef="${charPrId}"><hp:t></hp:t></hp:run></hp:p>`;
21116
21454
  }
21117
- var gjTableId = 92e5;
21455
+ var gjTableId = GJ_TABLE_ID_BASE;
21456
+ function resetGjTableIds() {
21457
+ gjTableId = GJ_TABLE_ID_BASE;
21458
+ }
21118
21459
  function cell(opts) {
21119
21460
  const paras = opts.paras ?? emptyPara();
21120
- 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>`;
21121
21462
  }
21122
- function table(rows, w, h, cols, tblBf = 1) {
21123
- 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;
21124
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>`;
21125
21466
  }
21126
- function buildGaejosikCover(title, gongmun) {
21127
- 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);
21128
21469
  const sz = gaejosikSizes(gongmun.bodyHeight, gongmun.sizes);
21129
21470
  const innerW = g.totalW - 282;
21130
21471
  const titleCharPr = simulateWrap(title, innerW, innerW, sz.coverTitle, 100, "keep").lines > 2 ? GJ_CHAR_COVER_SUB : GJ_CHAR_COVER_TITLE;
21131
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>`;
21132
21473
  const barEmpty = emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR);
21133
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 });
21134
- 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" });
21135
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 });
21136
- 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);
21137
21478
  const host = `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${tbl2}</hp:run></hp:p>`;
21138
21479
  const date = gongmun.cover?.date ?? formatGaejosikDate(/* @__PURE__ */ new Date());
21139
21480
  const org = gongmun.cover?.org ?? "";
@@ -21147,48 +21488,51 @@ function buildGaejosikCover(title, gongmun) {
21147
21488
  ...org ? [...Array(4).fill(subEmpty), sub(org)] : []
21148
21489
  ];
21149
21490
  }
21150
- function buildTocBanner(gongmun) {
21491
+ function buildTocBanner(gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21151
21492
  const g = tocBannerGeom(gongmun.bodyHeight, gongmun.sizes);
21152
21493
  const bfs = [GJ_BF_BAR_DARK, 1, GJ_BF_TOC_STRIPE, 1, GJ_BF_TOC_STRIPE, 1, GJ_BF_BAR_DARK];
21153
21494
  const filler = emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR);
21154
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>`;
21155
21496
  const row = g.colWs.map(
21156
- (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 })
21157
21498
  ).join("");
21158
- 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);
21159
21500
  return `<hp:p paraPrIDRef="${GJ_PARA_BAR}" styleIDRef="0" pageBreak="1"><hp:run charPrIDRef="${CHAR_NORMAL}">${banner}</hp:run></hp:p>`;
21160
21501
  }
21161
- function buildGaejosikToc(chapters, gongmun) {
21162
- 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);
21163
21505
  const items = chapters.map(
21164
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>`
21165
21507
  ).join("");
21166
21508
  const box = table(
21167
- [cell({ bf: GJ_BF_TOC_BOX, col: 0, w: TOC_GEOM.boxW, h: TOC_GEOM.boxH, paras: items })],
21168
- 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,
21169
21511
  TOC_GEOM.boxH,
21170
- 1
21512
+ 1,
21513
+ 1,
21514
+ bodyWidth
21171
21515
  );
21172
21516
  const host = `<hp:p paraPrIDRef="${GJ_PARA_BAR}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${box}</hp:run></hp:p>`;
21173
21517
  return [label, emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR), host];
21174
21518
  }
21175
- function buildGaejosikBodyTitle(title, gongmun) {
21176
- const g = COVER_GEOM;
21519
+ function buildGaejosikBodyTitle(title, gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21520
+ const g = coverGeom(gongmun.bodyHeight, gongmun.sizes, bodyWidth);
21177
21521
  const bt = bodyTitleGeom(gongmun.bodyHeight, gongmun.sizes);
21178
21522
  const barEmpty = emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR);
21179
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>`;
21180
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 });
21181
- 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" });
21182
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 });
21183
- 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);
21184
21528
  return `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${box}</hp:run></hp:p>`;
21185
21529
  }
21186
- function buildGaejosikChapter(n, title, gongmun) {
21187
- 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);
21188
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>`;
21189
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>`;
21190
- 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 });
21191
- 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);
21192
21536
  return `<hp:p paraPrIDRef="${GJ_PARA_CHAPTER}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${tbl2}</hp:run></hp:p>`;
21193
21537
  }
21194
21538
 
@@ -21556,16 +21900,22 @@ var AUTONUM_PREFIX_RE = /^(?:[0-90-9a-zA-Z가-힣]{1,6}[.)\]:]|[([][0-90-
21556
21900
  function htmlCellInnerToLines(inner) {
21557
21901
  let hadNonText = false;
21558
21902
  let work = inner;
21903
+ const imgSrcs = [];
21559
21904
  if (/<table[\s>]/i.test(work)) {
21560
21905
  hadNonText = true;
21561
21906
  work = removeNestedTables(work);
21562
21907
  }
21563
21908
  if (/<img\s/i.test(work)) {
21564
21909
  hadNonText = true;
21565
- 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
+ });
21566
21916
  }
21567
21917
  const lines = work.split(/<br\s*\/?>/gi).map((s) => s.trim()).filter((s) => s.length > 0);
21568
- return { lines, hadNonText };
21918
+ return { lines, hadNonText, imgSrcs };
21569
21919
  }
21570
21920
  function extractTopLevelTables(html) {
21571
21921
  const result = [];
@@ -21660,13 +22010,19 @@ function dataCellSpec(opts) {
21660
22010
  function normalizeAnchor(s) {
21661
22011
  return s.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "").slice(0, 24);
21662
22012
  }
21663
- 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 = "") {
21664
22018
  if (!remap) return null;
21665
22019
  for (const t of remap.tables) {
21666
22020
  if (t.used) continue;
21667
22021
  if (t.rows !== rows || t.cols !== cols) continue;
21668
22022
  if (t.anchor && anchor) {
21669
22023
  if (t.anchor !== anchor) continue;
22024
+ } else if (t.anchorRow && rowAnchor) {
22025
+ if (t.anchorRow !== rowAnchor) continue;
21670
22026
  } else if (t.index !== seq) {
21671
22027
  continue;
21672
22028
  }
@@ -21681,10 +22037,10 @@ function parseHu(s) {
21681
22037
  return Number.isFinite(n) ? n : void 0;
21682
22038
  }
21683
22039
  function edgeXml(tag, d) {
21684
- 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"/>`;
21685
22041
  }
21686
22042
  function borderFillDefToXml(id, def) {
21687
- 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>` : "";
21688
22044
  return ` <hh:borderFill id="${id}" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
21689
22045
  <hh:slash type="NONE" Crooked="0" isCounter="0"/>
21690
22046
  <hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
@@ -21695,28 +22051,45 @@ function borderFillDefToXml(id, def) {
21695
22051
  ${fill}` : ""}
21696
22052
  </hh:borderFill>`;
21697
22053
  }
21698
- var PROFILE_FONT_MAX = 2;
21699
- function profileCharPrXml(id, def) {
21700
- const height = parseHu(def.height_hwpunit) ?? 1e3;
21701
- const color = def.textColor ?? "#000000";
21702
- const rawFont = def.fontRef_hangul != null ? parseInt(def.fontRef_hangul, 10) || 0 : 0;
21703
- 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
+ }
21704
22067
  const boldAttr = def.bold ? ` bold="1"` : "";
21705
22068
  const italicAttr = def.italic ? ` italic="1"` : "";
21706
22069
  const underline = def.underline ? `
21707
22070
  <hh:underline type="BOTTOM" shape="SOLID" color="${color}"/>` : "";
21708
22071
  return ` <hh:charPr id="${id}" height="${height}" textColor="${color}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
21709
- <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}"/>
21710
22073
  <hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
21711
22074
  <hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
21712
22075
  <hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
21713
22076
  <hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>${underline}
21714
22077
  </hh:charPr>`;
21715
22078
  }
21716
- function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21717
- const remap = { borderFillXmls: [], charPrXmls: [], tables: [] };
22079
+ function buildProfileRemap(profile, charPrBase, borderFillBase = 3, fontBase = 3) {
22080
+ const remap = { borderFillXmls: [], charPrXmls: [], fontFaces: [], tables: [] };
21718
22081
  let bfNext = borderFillBase;
21719
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
+ };
21720
22093
  for (const t of profile.tables) {
21721
22094
  const localBf = {};
21722
22095
  for (const [key, def] of Object.entries(t.used_border_fills ?? {})) {
@@ -21727,7 +22100,7 @@ function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21727
22100
  const localChar = {};
21728
22101
  for (const [key, def] of Object.entries(t.used_char_prs ?? {})) {
21729
22102
  const gid = charNext++;
21730
- remap.charPrXmls.push(profileCharPrXml(gid, def));
22103
+ remap.charPrXmls.push(profileCharPrXml(gid, def, def.fontName_hangul ? fontIdOf(def.fontName_hangul) : void 0));
21731
22104
  localChar[key] = gid;
21732
22105
  }
21733
22106
  let colWidths;
@@ -21741,6 +22114,7 @@ function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21741
22114
  cols: t.cols,
21742
22115
  // 재정규화 — 추출기는 정규화해 담지만 손편집된 프로필 JSON도 같은 키 공간으로
21743
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,
21744
22118
  width: parseHu(t.width_hwpunit),
21745
22119
  colWidths,
21746
22120
  cellBf: /* @__PURE__ */ new Map(),
@@ -21758,17 +22132,208 @@ function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21758
22132
  const h = parseHu(cell2.height_hwpunit);
21759
22133
  if (h != null) tr.cellH.set(k, h);
21760
22134
  }
21761
- 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;
21762
22306
  }
21763
- 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 };
21764
22326
  }
21765
22327
 
21766
22328
  // src/hwpx/gen-table.ts
21767
- var TABLE_ID_BASE = 1e3;
22329
+ var TABLE_ID_BASE = DATA_TABLE_ID_BASE;
21768
22330
  var tableIdCounter = TABLE_ID_BASE;
21769
22331
  function nextTableId() {
21770
22332
  return ++tableIdCounter;
21771
22333
  }
22334
+ function resetTableIds() {
22335
+ tableIdCounter = TABLE_ID_BASE;
22336
+ }
21772
22337
  var DATA_TABLE_INSET = 1800;
21773
22338
  function anchorOfMarkdownCell(cell2) {
21774
22339
  return normalizeAnchor(cell2.replace(/!\[[^\]]*\]\([^)]*\)/g, ""));
@@ -21787,7 +22352,7 @@ function profileColWidths(tp, colCnt) {
21787
22352
  var CELL_PAD = 1200;
21788
22353
  function cellContentWidth(text, charHeight) {
21789
22354
  let max = 0;
21790
- 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)) {
21791
22356
  const w = measureTextWidth(seg.trim(), charHeight, 100);
21792
22357
  if (w > max) max = w;
21793
22358
  }
@@ -21795,7 +22360,7 @@ function cellContentWidth(text, charHeight) {
21795
22360
  }
21796
22361
  function cellMinWordWidth(text, charHeight) {
21797
22362
  let max = 0;
21798
- 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)) {
21799
22364
  for (const word of seg.trim().split(/\s+/)) {
21800
22365
  const w = measureTextWidth(word, charHeight, 100);
21801
22366
  if (w > max) max = w;
@@ -21825,7 +22390,7 @@ function computeColWidths(colMax, totalWidth, colMinWord = []) {
21825
22390
  const sumFreeRaw = [...free].reduce((a, i) => a + raw[i], 0);
21826
22391
  if (budget > sumFreeRaw * 1.6) {
21827
22392
  const sumRaw = raw.reduce((a, b) => a + b, 0);
21828
- 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)));
21829
22394
  free.clear();
21830
22395
  } else {
21831
22396
  for (; ; ) {
@@ -21846,7 +22411,23 @@ function computeColWidths(colMax, totalWidth, colMinWord = []) {
21846
22411
  }
21847
22412
  let rem = totalWidth - widths.reduce((a, b) => a + b, 0);
21848
22413
  const order = [...raw.keys()].sort((a, b) => raw[b] - raw[a]);
21849
- 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
+ }
21850
22431
  return widths;
21851
22432
  }
21852
22433
  function estimateRowHeight(cells, widths, charHeight) {
@@ -21861,13 +22442,14 @@ function estimateRowHeight(cells, widths, charHeight) {
21861
22442
  });
21862
22443
  return maxLines * Math.round(charHeight * 1.6) + 282;
21863
22444
  }
21864
- function generateTable(rows, theme, style = null, remap = null, seq = 0) {
22445
+ function generateTable(rows, theme, style = null, remap = null, seq = 0, images = null) {
21865
22446
  const rowCnt = rows.length;
21866
22447
  const colCnt = Math.max(...rows.map((r) => r.length), 1);
21867
22448
  const reg = style?.bfRegistry ?? null;
21868
22449
  const totalW = style ? reg ? style.totalWidth - DATA_TABLE_INSET : style.totalWidth : 44e3;
21869
22450
  const measureH = style?.charHeight ?? 1e3;
21870
- 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);
21871
22453
  const colMax = Array(colCnt).fill(0);
21872
22454
  const colMaxBody = Array(colCnt).fill(0);
21873
22455
  const colMinWord = Array(colCnt).fill(0);
@@ -21899,8 +22481,23 @@ function generateTable(rows, theme, style = null, remap = null, seq = 0) {
21899
22481
  const centered = isHeaderRow || colCentered[colIdx];
21900
22482
  const paraPrId = style ? centered ? style.tblCenterParaPr ?? style.centerParaPr : reg ? style.tblLeftParaPr ?? PARA_NORMAL : PARA_NORMAL : PARA_NORMAL;
21901
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
+ }
21902
22498
  const runs = generateRuns(seg, cellCharPr, prof?.cellChar.has(k) ? void 0 : mapId);
21903
- 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>`;
21904
22501
  }).join("");
21905
22502
  const bf = prof?.cellBf.get(k) ?? (reg ? reg.get(dataCellSpec({
21906
22503
  row: rowIdx,
@@ -21942,23 +22539,37 @@ function layoutHtmlRows(rows) {
21942
22539
  colCnt = Math.max(colCnt, c);
21943
22540
  }
21944
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);
21945
22551
  return { placed, rowCnt: rows.length, colCnt };
21946
22552
  }
21947
22553
  function unescapeHtml(s) {
21948
22554
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
21949
22555
  }
21950
- 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) {
21951
22557
  const rows = parseHtmlTable(rawHtml);
21952
22558
  if (!rows || rows.length === 0) return null;
21953
22559
  const { placed, rowCnt, colCnt } = layoutHtmlRows(rows);
21954
22560
  if (rowCnt === 0 || colCnt === 0) return null;
21955
22561
  const measureH = style?.charHeight ?? 1e3;
21956
22562
  const first = placed.find((p) => p.r === 0 && p.c === 0) ?? placed[0];
21957
- 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));
21958
22568
  const colMax = Array(colCnt).fill(0);
21959
22569
  const colMaxBody = Array(colCnt).fill(0);
21960
22570
  const colMinWord = Array(colCnt).fill(0);
21961
- 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);
21962
22573
  placed.forEach((cell2, i) => {
21963
22574
  const w = Math.max(...cellLines[i].map((l) => measureTextWidth(unescapeHtml(l).trim(), measureH, 100)), 0) / cell2.colSpan;
21964
22575
  const mw = cell2.colSpan === 1 ? Math.max(...cellLines[i].map((l) => cellMinWordWidth(unescapeHtml(l), measureH)), 0) : 0;
@@ -21980,7 +22591,32 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
21980
22591
  let htmlHeaderRows = 0;
21981
22592
  while (htmlHeaderRows < rows.length && rows[htmlHeaderRows].tag === "th") htmlHeaderRows++;
21982
22593
  const tableRowHeights = Array(rowCnt).fill(cellH);
21983
- const tcXmls = placed.map((cell2, i) => {
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 }) => {
21984
22620
  const k = `${cell2.r},${cell2.c}`;
21985
22621
  const isHeader = cell2.isHeader;
21986
22622
  const baseCharPr = style ? style.charPr : CHAR_NORMAL;
@@ -21989,31 +22625,42 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
21989
22625
  const charPrId = prof?.cellChar.get(k) ?? defaultCharPr;
21990
22626
  const centered = isHeader || colCentered[cell2.c];
21991
22627
  const paraPrId = style ? centered ? reg ? style.tblCenterParaPr ?? style.centerParaPr : style.centerParaPr : reg ? style.tblLeftParaPr ?? PARA_NORMAL : PARA_NORMAL : PARA_NORMAL;
21992
- const lines = cellLines[i];
21993
- const paras = lines.map(
21994
- (line) => `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t>${escapeXml(unescapeHtml(line))}</hp:t></hp:run></hp:p>`
21995
- );
21996
- let nestedH = 0;
21997
- for (const nested of extractTopLevelTables(cell2.inner)) {
21998
- const nestedW = Math.max(spanW(cell2) - 1020, 4e3);
21999
- const nestedXml = generateHtmlTableXml(nested, theme, nestedW, style ? { ...style, totalWidth: nestedW } : null);
22000
- if (nestedXml) {
22001
- paras.push(`<hp:p paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0">${nestedXml}</hp:run></hp:p>`);
22002
- 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>`);
22003
22650
  }
22004
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
+ }
22005
22655
  if (paras.length === 0) {
22006
22656
  paras.push(`<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t></hp:t></hp:run></hp:p>`);
22007
22657
  }
22008
- const contentH = Math.max(cellH * cell2.rowSpan, Math.max(lines.length, 1) * (style ? Math.round(measureH * 1.6) : 800) + nestedH);
22009
- const cellHeight = Math.max(prof?.cellH.get(k) ?? 0, contentH);
22010
- const perRow = Math.ceil(cellHeight / cell2.rowSpan);
22011
- for (let r = cell2.r; r < Math.min(cell2.r + cell2.rowSpan, rowCnt); r++) {
22012
- tableRowHeights[r] = Math.max(tableRowHeights[r], perRow);
22013
- }
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;
22014
22661
  const bf = prof?.cellBf.get(k) ?? (reg ? reg.get(dataCellSpec({
22015
22662
  row: cell2.r,
22016
- rowEnd: cell2.r + cell2.rowSpan - 1,
22663
+ rowEnd,
22017
22664
  col: cell2.c,
22018
22665
  colEnd: cell2.c + cell2.colSpan - 1,
22019
22666
  rowCnt,
@@ -22021,7 +22668,7 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
22021
22668
  headerRows: htmlHeaderRows,
22022
22669
  fill: isHeader ? style.headerFill ?? "#E6E6E6" : void 0
22023
22670
  })) : style && isHeader ? style.headerBf : 2);
22024
- 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>`;
22025
22672
  });
22026
22673
  const trXmls = [];
22027
22674
  for (let r = 0; r < rowCnt; r++) {
@@ -22033,12 +22680,15 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
22033
22680
  }
22034
22681
 
22035
22682
  // src/hwpx/gen-gongmun-extra.ts
22036
- var extraTableId = 93e5;
22683
+ var extraTableId = EXTRA_TABLE_ID_BASE;
22684
+ function resetExtraTableIds() {
22685
+ extraTableId = EXTRA_TABLE_ID_BASE;
22686
+ }
22037
22687
  function tbl(rows, w, h, cols) {
22038
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>`;
22039
22689
  }
22040
22690
  function tc(opts) {
22041
- 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>`;
22042
22692
  }
22043
22693
  function para(text, paraPrId, charPrId) {
22044
22694
  return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`;
@@ -22065,7 +22715,7 @@ function buildApprovalTable(labels, reg, approvalCharPr) {
22065
22715
  return `<hp:p paraPrIDRef="${GONGMUN_RIGHT}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${table2}</hp:run></hp:p>`;
22066
22716
  }
22067
22717
  function hasEndMark(lastText) {
22068
- return /끝\s*\.?\s*$/.test(lastText.trim());
22718
+ return /(^|\s)끝\s*\.\s*$/.test(lastText.trim());
22069
22719
  }
22070
22720
  function buildEndMark() {
22071
22721
  return para(" \uB05D.", PARA_NORMAL, CHAR_NORMAL);
@@ -22080,7 +22730,7 @@ function buildTitleBox(title, titleCharPr, barCharPr, bodyWidth, reg) {
22080
22730
  const barPara = para("", GJ_PARA_BAR, barCharPr);
22081
22731
  const rows = [
22082
22732
  tc({ bf: barTop, row: 0, col: 0, w, h: TITLE_BOX.barH, paras: barPara }),
22083
- 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" }),
22084
22734
  tc({ bf: barBottom, row: 2, col: 0, w, h: TITLE_BOX.barH, paras: barPara })
22085
22735
  ];
22086
22736
  const table2 = tbl(rows, w, TITLE_BOX.barH * 2 + TITLE_BOX.titleH, 1);
@@ -22127,7 +22777,10 @@ function buildDocHead(g, ids) {
22127
22777
  out.push(blank());
22128
22778
  return out;
22129
22779
  }
22130
- 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
+ }
22131
22784
  function buildDocFoot(g, ids) {
22132
22785
  const f = g.docFoot;
22133
22786
  const out = [blank()];
@@ -22135,7 +22788,7 @@ function buildDocFoot(g, ids) {
22135
22788
  out.push(para(f.sender, GONGMUN_CENTER, ids.sender), blank());
22136
22789
  }
22137
22790
  const foot = (text) => para(text, GONGMUN_TBL_LEFT, ids.foot);
22138
- out.push(foot(FOOT_SEP));
22791
+ out.push(foot(footSep(g)));
22139
22792
  if (f.drafter || f.reviewer || f.approver)
22140
22793
  out.push(foot(`\uAE30\uC548\uC790 ${f.drafter ?? ""} \uAC80\uD1A0\uC790 ${f.reviewer ?? ""} \uACB0\uC7AC\uAD8C\uC790 ${f.approver ?? ""}`));
22141
22794
  if (f.cooperator) out.push(foot(`\uD611\uC870\uC790 ${f.cooperator}`));
@@ -22284,6 +22937,54 @@ var COMMAND_MAP = {
22284
22937
  vdots: "VDOTS",
22285
22938
  ddots: "DDOTS"
22286
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
+ }
22287
22988
  var ACCENT_COMMANDS = {
22288
22989
  bar: "bar",
22289
22990
  overline: "bar",
@@ -22377,8 +23078,20 @@ function readCommand(input, idx, depth) {
22377
23078
  const endIdx = input.indexOf(endTag, env.next);
22378
23079
  if (endIdx === -1) return { value: env.value, next: env.next };
22379
23080
  const body = convertLatexFragment(input.slice(env.next, endIdx), depth + 1);
22380
- if (env.value === "matrix" || env.value === "pmatrix" || env.value === "bmatrix") {
22381
- 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 };
22382
23095
  }
22383
23096
  return { value: body, next: endIdx + endTag.length };
22384
23097
  }
@@ -22410,7 +23123,11 @@ function readCommand(input, idx, depth) {
22410
23123
  const tok = readGroupOrToken(input, start, depth);
22411
23124
  return { value: `"${tok.value}"`, next: tok.next };
22412
23125
  }
22413
- 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 };
22414
23131
  }
22415
23132
  function convertLatexFragment(input, depth) {
22416
23133
  if (depth > MAX_GROUP_DEPTH) return normalizeEqEdit(input);
@@ -22490,44 +23207,47 @@ function generateSecPr(gongmun) {
22490
23207
  m.header = gongmun.headerFooter;
22491
23208
  m.footer = gongmun.headerFooter;
22492
23209
  }
22493
- 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>`;
22494
23211
  const colPr = `<hp:ctrl><hp:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="1" sameSz="1" sameGap="0"/></hp:ctrl>`;
22495
23212
  return secPr + colPr + (gongmun?.pageNumbers ? pageNumCtrl() : "");
22496
23213
  }
22497
- function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null, fit = null, chartParts = null, bfReg = null, remap = null, dfIds = null) {
22498
- const paraXmls = [];
22499
- let isFirst = true;
22500
- let tableSeq = 0;
22501
- const orderedCounters = {};
22502
- let prevWasOrdered = false;
22503
- const gaejosik = gongmun?.preset === "gaejosik";
22504
- const measured = !!gongmun && usesReportFonts(gongmun.preset);
22505
- const richAssets = !!gongmun && needsGaejosikAssets(gongmun);
22506
- const vBase = charVariantBase(richAssets, !!gongmun);
22507
- const tableStyle = gongmun ? {
22508
- totalWidth: mmToHwpunit(210 - gongmun.margins.left - gongmun.margins.right),
22509
- // 표 셀 크기 — 실측: 실측 프리셋 맑은 고딕 12pt(22·23), 비실측도 12pt(11·12,
22510
- // 실결재 지배값 — 본문 크기 셀은 열폭 부족으로 서술 열 세로 신장, v4.0.2 QA)
22511
- charPr: richAssets ? GJ_CHAR_TABLE : GONGMUN_TBL_CHAR,
22512
- boldCharPr: richAssets ? GJ_CHAR_TABLE_BOLD : GONGMUN_TBL_CHAR_BOLD,
22513
- charHeight: richAssets ? gaejosikSizes(gongmun.bodyHeight, gongmun.sizes).table : GONGMUN_TBL_PT,
22514
- headerBf: gongmunTableHeaderBf(richAssets),
22515
- centerParaPr: GONGMUN_CENTER,
22516
- tblCenterParaPr: GONGMUN_TBL_CENTER,
22517
- tblLeftParaPr: GONGMUN_TBL_LEFT,
22518
- bfRegistry: bfReg ?? void 0,
22519
- rightParaPr: GONGMUN_RIGHT
22520
- } : null;
22521
- const chamMap = (id) => id === CHAR_BOLD ? GJ_CHAR_CHAM_BOLD : id;
22522
- let chapterNo = 0;
22523
- let h2Seq = 0;
22524
- let coverH1Idx = -1;
22525
- let pendingPageBreak = false;
22526
- let pendingNewNum = false;
22527
- let titleBoxH1Idx = -1;
22528
- let pressH1Idx = -1;
22529
- 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;
22530
23249
  const preamble = [];
23250
+ let hasFrontPages = false;
22531
23251
  if (gongmun?.reportInfo && dfIds) {
22532
23252
  preamble.push(buildReportInfo(gongmun, dfIds));
22533
23253
  }
@@ -22545,278 +23265,340 @@ function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? prec
22545
23265
  }
22546
23266
  if (gongmun && (gongmun.cover || gongmun.toc)) {
22547
23267
  const h1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
22548
- const chapters = blocks.filter((b) => b.type === "heading" && b.level === 2).map((b) => stripChapterNumber(b.text || ""));
22549
23268
  let coverTitle = "";
22550
23269
  const hide = (xml, hideHeader) => gongmun.pageNumbers ? xml.replace(/<hp:run charPrIDRef="(\d+)">/, `<hp:run charPrIDRef="$1">${pageHidingCtrl(hideHeader)}`) : xml;
22551
23270
  if (gongmun.cover && h1Idx >= 0) {
22552
23271
  coverTitle = (blocks[h1Idx].text || "").trim();
22553
- const coverPart = buildGaejosikCover(coverTitle, gongmun);
23272
+ const coverPart = buildGaejosikCover(coverTitle, gongmun, gjBodyW);
22554
23273
  coverPart[0] = hide(coverPart[0], true);
22555
23274
  preamble.push(...coverPart);
22556
- coverH1Idx = h1Idx;
23275
+ ctx.coverH1Idx = h1Idx;
22557
23276
  hasFrontPages = true;
22558
23277
  }
22559
- if (gongmun.toc && chapters.length > 0) {
22560
- 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);
22561
23282
  tocPart[0] = hide(tocPart[0], false);
22562
23283
  preamble.push(...tocPart);
22563
23284
  hasFrontPages = true;
22564
23285
  }
22565
23286
  if (gongmun.bodyTitleBox && coverTitle && hasFrontPages) {
22566
- 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" `));
22567
23288
  }
22568
23289
  }
22569
- if (gongmun && !gaejosik && coverH1Idx < 0 && bfReg && (gongmun.preset === "report" || gongmun.preset === "plan" || gongmun.preset === "notice")) {
22570
- titleBoxH1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
22571
- } else if (gongmun?.press && coverH1Idx < 0 && dfIds) {
22572
- pressH1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
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);
22573
23294
  }
22574
23295
  if (preamble.length > 0) {
22575
- preamble[0] = preamble[0].replace(` pageBreak="1"`, "").replace(/<hp:run charPrIDRef="(\d+)">/, `<hp:run charPrIDRef="$1">${generateSecPr(gongmun)}`);
22576
- isFirst = false;
22577
- pendingPageBreak = hasFrontPages && !preamble[preamble.length - 1].includes(`pageBreak="1"`);
22578
- pendingNewNum = !!gongmun.pageNumbers && hasFrontPages;
22579
- 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));
22580
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);
22581
23543
  for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
22582
23544
  const block = blocks[blockIdx];
22583
- let xml = "";
22584
23545
  if (block.type !== "list_item" || !block.ordered) {
22585
- if (prevWasOrdered) {
22586
- 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];
22587
23548
  }
22588
- prevWasOrdered = false;
23549
+ ctx.prevWasOrdered = false;
22589
23550
  }
23551
+ let xml = "";
22590
23552
  switch (block.type) {
22591
- case "heading": {
22592
- if (gongmun && blockIdx === coverH1Idx) break;
22593
- if (gongmun && blockIdx === pressH1Idx && dfIds) {
22594
- const parts = [generateParagraph((block.text || "").trim(), GONGMUN_CENTER, dfIds.pressTitle)];
22595
- for (const s of gongmun.press?.sub ?? []) {
22596
- parts.push(generateParagraph(`- ${s} -`, GONGMUN_CENTER, dfIds.pressSub));
22597
- }
22598
- xml = parts.join("\n ");
22599
- break;
22600
- }
22601
- if (gongmun && blockIdx === titleBoxH1Idx && tableStyle && bfReg) {
22602
- xml = buildTitleBox(
22603
- (block.text || "").trim(),
22604
- measured ? GJ_CHAR_BODY_TITLE : CHAR_H1,
22605
- richAssets ? GJ_CHAR_TITLE_BAR : GONGMUN_TITLE_BAR_CHAR,
22606
- tableStyle.totalWidth,
22607
- bfReg
22608
- );
22609
- break;
22610
- }
22611
- if (gaejosik) {
22612
- const lvl = block.level || 1;
22613
- if (lvl <= 2) {
22614
- chapterNo++;
22615
- xml = buildGaejosikChapter(chapterNo, stripChapterNumber(block.text || ""), gongmun);
22616
- } else if (lvl === 3) {
22617
- xml = generateParagraph(
22618
- `\u25A1 ${block.text || ""}`,
22619
- GONGMUN_LIST_BASE,
22620
- GJ_CHAR_DAE,
22621
- (id) => id === CHAR_BOLD ? GJ_CHAR_DAE_BOLD : id
22622
- );
22623
- } else {
22624
- xml = generateParagraph(`${gongmun.bullet2} ${block.text || ""}`, GONGMUN_LIST_BASE + 1, CHAR_NORMAL);
22625
- }
22626
- break;
22627
- }
22628
- const pId = headingParaPrId(block.level || 1);
22629
- const cId = headingCharPrId(block.level || 1);
22630
- const styleId = gongmun ? Math.min(block.level || 1, 4) : 0;
22631
- let hText = block.text || "";
22632
- if (gongmun && (block.level || 1) === 2 && gongmun.h2Marker !== "none") {
22633
- const title = stripChapterNumber(hText);
22634
- hText = gongmun.h2Marker === "box" ? `\u25A1 ${title}` : `${++h2Seq}. ${title}`;
22635
- }
22636
- xml = generateParagraph(hText, pId, cId, void 0, styleId);
23553
+ case "heading":
23554
+ xml = renderHeading(block, blockIdx, ctx);
22637
23555
  break;
22638
- }
22639
- case "paragraph": {
22640
- const pTrim = (block.text || "").trimStart();
22641
- if (measured && (pTrim.startsWith("\u203B") || /^\*\s/.test(pTrim))) {
22642
- xml = generateParagraph((block.text || "").trim(), GJ_PARA_CHAM, GJ_CHAR_CHAM, chamMap);
22643
- break;
22644
- }
22645
- const ctr = gongmun && /^<center>([\s\S]*)<\/center>$/i.exec((block.text || "").trim());
22646
- const rgt = gongmun && /^<right>([\s\S]*)<\/right>$/i.exec((block.text || "").trim());
22647
- if (ctr) {
22648
- xml = generateParagraph(ctr[1].trim(), GONGMUN_CENTER);
22649
- } else if (rgt) {
22650
- xml = generateParagraph(rgt[1].trim(), GONGMUN_RIGHT);
22651
- } else {
22652
- xml = generateParagraph(block.text || "", PARA_NORMAL, CHAR_NORMAL, fit ? variantMapper(fit, blockIdx, vBase) : void 0);
22653
- }
23556
+ case "paragraph":
23557
+ xml = renderParagraph(block, blockIdx, ctx);
22654
23558
  break;
22655
- }
22656
- case "code_block": {
22657
- if (chartParts !== null && (block.lang || "").toLowerCase() === "chart") {
22658
- const fence = parseChartFence(block.text || "");
22659
- if (fence) {
22660
- const partName = `Chart/chart${chartParts.length + 1}.xml`;
22661
- chartParts.push({ name: partName, xml: buildChartSpaceXml(fence) });
22662
- const chartEl = buildChartElementXml(partName, fence.widthHu, fence.heightHu, 91e5 + blockIdx);
22663
- if (isFirst) {
22664
- const secRun = `<hp:run charPrIDRef="0">${generateSecPr(gongmun)}<hp:t></hp:t></hp:run>`;
22665
- paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
22666
- isFirst = false;
22667
- }
22668
- xml = `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${chartEl}</hp:run></hp:p>`;
22669
- break;
22670
- }
22671
- }
22672
- const codeLines = (block.text || "").split("\n");
22673
- xml = codeLines.map((line) => generateParagraph(line || " ", PARA_CODE)).join("\n ");
23559
+ case "code_block":
23560
+ xml = renderCodeBlock(block, blockIdx, ctx);
22674
23561
  break;
22675
- }
22676
- case "equation": {
22677
- if (isFirst) {
22678
- const secRun = `<hp:run charPrIDRef="0">${generateSecPr(gongmun)}<hp:t></hp:t></hp:run>`;
22679
- paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
22680
- isFirst = false;
22681
- }
22682
- xml = generateEquationParagraph(block.text || "", blockIdx);
23562
+ case "equation":
23563
+ xml = renderEquation(block, blockIdx, ctx);
22683
23564
  break;
22684
- }
22685
- case "blockquote": {
22686
- if (measured) {
22687
- const t = (block.text || "").trim();
22688
- if (t) xml = generateParagraph(t.startsWith("\u203B") ? t : `\u203B ${t}`, GJ_PARA_CHAM, GJ_CHAR_CHAM, chamMap);
22689
- break;
22690
- }
22691
- xml = generateParagraph(
22692
- block.text || "",
22693
- PARA_QUOTE,
22694
- theme.hasQuoteOption ? CHAR_QUOTE : CHAR_NORMAL
22695
- );
23565
+ case "blockquote":
23566
+ xml = renderBlockquote(block, ctx);
22696
23567
  break;
22697
- }
22698
- case "list_item": {
22699
- if (gongmun && gongmunList) {
22700
- const info = gongmunList.get(blockIdx);
22701
- const depth = info?.depth ?? 0;
22702
- const marker2 = info?.marker ?? "";
22703
- const content = block.text || "";
22704
- if (measured && content.trimStart().startsWith("\u203B")) {
22705
- xml = generateParagraph(content.trim(), GJ_PARA_CHAM, GJ_CHAR_CHAM, chamMap);
22706
- break;
22707
- }
22708
- if (measured && block.marker === "*") {
22709
- xml = generateParagraph(`* ${content.trim()}`, GJ_PARA_CHAM, GJ_CHAR_CHAM, chamMap);
22710
- break;
22711
- }
22712
- const text = marker2 ? `${marker2} ${content}` : content;
22713
- const listParaPr = marker2 ? GONGMUN_LIST_BASE + depth : GONGMUN_LIST_PLAIN_BASE + depth;
22714
- let listCharPr = gongmun.numbering === "report" && depth === 0 ? CHAR_BOLD : CHAR_NORMAL;
22715
- let mapId = fit ? variantMapper(fit, blockIdx, vBase) : void 0;
22716
- if (measured && depth === 0 && gongmun.numbering !== "standard") {
22717
- listCharPr = GJ_CHAR_DAE;
22718
- mapId = (id) => id === CHAR_BOLD ? GJ_CHAR_DAE_BOLD : id;
22719
- }
22720
- if (gongmun.preset === "press") {
22721
- if (depth === 0) listCharPr = CHAR_NORMAL;
22722
- if (depth >= 2 && dfIds) {
22723
- listCharPr = dfIds.small;
22724
- mapId = void 0;
22725
- }
22726
- }
22727
- xml = generateParagraph(text, listParaPr, listCharPr, mapId);
22728
- break;
22729
- }
22730
- const indent = block.indent || 0;
22731
- let marker;
22732
- if (block.marker) {
22733
- marker = `${block.marker} `;
22734
- prevWasOrdered = !!block.ordered;
22735
- } else if (block.ordered) {
22736
- orderedCounters[indent] = (orderedCounters[indent] || 0) + 1;
22737
- for (const k of Object.keys(orderedCounters)) {
22738
- if (+k > indent) delete orderedCounters[+k];
22739
- }
22740
- marker = `${orderedCounters[indent]}. `;
22741
- prevWasOrdered = true;
22742
- } else {
22743
- marker = "\xB7 ";
22744
- if (prevWasOrdered) {
22745
- for (const k of Object.keys(orderedCounters)) delete orderedCounters[+k];
22746
- }
22747
- prevWasOrdered = false;
22748
- }
22749
- const indentPrefix = " ".repeat(indent);
22750
- xml = generateParagraph(indentPrefix + marker + (block.text || ""), PARA_LIST);
23568
+ case "list_item":
23569
+ xml = renderListItem(block, blockIdx, ctx);
22751
23570
  break;
22752
- }
22753
23571
  case "hr":
22754
- 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);
22755
23573
  break;
22756
23574
  case "table":
22757
- if (block.rows) {
22758
- if (isFirst) {
22759
- const secRun = `<hp:run charPrIDRef="0">${generateSecPr(gongmun)}<hp:t></hp:t></hp:run>`;
22760
- paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
22761
- isFirst = false;
22762
- }
22763
- xml = generateTable(block.rows, theme, tableStyle, remap, tableSeq++);
22764
- }
23575
+ xml = renderTable(block, ctx);
22765
23576
  break;
22766
- case "html_table": {
22767
- const htmlW = tableStyle ? bfReg ? tableStyle.totalWidth - DATA_TABLE_INSET : tableStyle.totalWidth : 44e3;
22768
- const tbl2 = generateHtmlTableXml(block.text || "", theme, htmlW, tableStyle, remap, tableSeq++);
22769
- if (tbl2) {
22770
- if (isFirst) {
22771
- const secRun = `<hp:run charPrIDRef="0">${generateSecPr(gongmun)}<hp:t></hp:t></hp:run>`;
22772
- paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
22773
- isFirst = false;
22774
- }
22775
- xml = `<hp:p paraPrIDRef="${tableStyle && bfReg ? GONGMUN_RIGHT : 0}" styleIDRef="0"><hp:run charPrIDRef="0">${tbl2}</hp:run></hp:p>`;
22776
- } else {
22777
- const plain = (block.text || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
22778
- xml = plain ? generateParagraph(plain) : "";
22779
- }
23577
+ case "html_table":
23578
+ xml = renderHtmlTable(block, ctx);
22780
23579
  break;
22781
- }
22782
23580
  }
22783
23581
  if (!xml) continue;
22784
- if (isFirst && block.type !== "table") {
22785
- xml = xml.replace(
22786
- /<hp:run charPrIDRef="(\d+)">/,
22787
- `<hp:run charPrIDRef="$1">${generateSecPr(gongmun)}`
22788
- );
22789
- isFirst = false;
23582
+ if (ctx.opener.isFirst && block.type !== "table") {
23583
+ xml = ctx.opener.inject(xml);
22790
23584
  }
22791
- if (pendingPageBreak) {
23585
+ if (ctx.pendingPageBreak) {
22792
23586
  xml = xml.replace(/^<hp:p /, `<hp:p pageBreak="1" `);
22793
- pendingPageBreak = false;
23587
+ ctx.pendingPageBreak = false;
22794
23588
  }
22795
- if (pendingNewNum) {
23589
+ if (ctx.pendingNewNum) {
22796
23590
  xml = xml.replace(/<hp:run charPrIDRef="(\d+)">/, `<hp:run charPrIDRef="$1">${newPageNumCtrl(1)}`);
22797
- pendingNewNum = false;
23591
+ ctx.pendingNewNum = false;
22798
23592
  }
22799
- paraXmls.push(xml);
22800
- }
22801
- if (gongmun?.noticeHead && dfIds) {
22802
- paraXmls.push(...buildNoticeFoot(gongmun));
22803
- }
22804
- if (gongmun?.endMark && paraXmls.length > 0) {
22805
- const lastText = [...blocks].reverse().find((b) => (b.type === "paragraph" || b.type === "list_item") && (b.text || "").trim())?.text ?? "";
22806
- if (!hasEndMark(lastText)) paraXmls.push(buildEndMark());
22807
- }
22808
- if (gongmun?.docFoot && dfIds) {
22809
- paraXmls.push(...buildDocFoot(gongmun, dfIds));
22810
- }
22811
- if (gongmun?.press && dfIds && bfReg && tableStyle) {
22812
- paraXmls.push(...buildPressContact(gongmun, dfIds, tableStyle.totalWidth, bfReg));
23593
+ ctx.paraXmls.push(xml);
22813
23594
  }
22814
- if (paraXmls.length === 0) {
22815
- 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());
22816
23598
  }
22817
23599
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
22818
23600
  <hs:sec xmlns:hs="${NS_SECTION}" xmlns:hp="${NS_PARA}">
22819
- ${paraXmls.join("\n ")}
23601
+ ${ctx.paraXmls.join("\n ")}
22820
23602
  </hs:sec>`;
22821
23603
  }
22822
23604
 
@@ -22829,14 +23611,15 @@ async function markdownToHwpx(markdown, options) {
22829
23611
  const blocks = parseMarkdownToBlocks(markdown);
22830
23612
  const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
22831
23613
  const fit = gongmun && gongmunList ? computeGongmunFitPlan(blocks, gongmun, gongmunList) : null;
22832
- const staticBfEnd = gongmun ? richAssets ? 11 : 4 : 3;
22833
- const remap = options?.profile ? buildProfileRemap(options.profile, charVariantBase(richAssets, !!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;
22834
23616
  const bfReg = gongmun ? new TableBfRegistry(staticBfEnd + (remap?.borderFillXmls.length ?? 0)) : null;
22835
23617
  const dfActive = !!gongmun && docframeActive(gongmun);
22836
23618
  const dfBase = charVariantBase(richAssets, !!gongmun) + (fit?.variants?.length ?? 0) * 4 + (remap?.charPrXmls.length ?? 0);
22837
23619
  const dfIds = dfActive ? docframeIds(dfBase) : null;
22838
23620
  const chartParts = [];
22839
- 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);
22840
23623
  if (remap && remap.tables.length > 0) {
22841
23624
  const unused = remap.tables.filter((t) => !t.used).length;
22842
23625
  if (unused === remap.tables.length) {
@@ -22847,13 +23630,16 @@ async function markdownToHwpx(markdown, options) {
22847
23630
  const zip = new JSZip6();
22848
23631
  zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
22849
23632
  zip.file("META-INF/container.xml", generateContainerXml());
22850
- 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);
22851
23635
  zip.file("Contents/header.xml", generateHeaderXml(
22852
23636
  theme,
22853
23637
  gongmun,
22854
23638
  fit?.variants ?? [],
22855
23639
  extraBorderFills,
22856
- [...remap?.charPrXmls ?? [], ...dfActive ? docframeCharPrXmls(dfBase, richAssets) : []]
23640
+ [...remap?.charPrXmls ?? [], ...dfActive ? docframeCharPrXmls(dfBase, richAssets) : []],
23641
+ gongmunList?.indentVariants ?? [],
23642
+ remap?.fontFaces ?? []
22857
23643
  ));
22858
23644
  zip.file("Contents/section0.xml", sectionXml);
22859
23645
  for (const part of chartParts) zip.file(part.name, part.xml);
@@ -23033,7 +23819,19 @@ function parseBorderFills(headerDoc) {
23033
23819
  }
23034
23820
  return map;
23035
23821
  }
23036
- 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) {
23037
23835
  const map = /* @__PURE__ */ new Map();
23038
23836
  for (const cp of elemsByLocal(headerDoc, "charPr")) {
23039
23837
  const id = cp.getAttribute("id");
@@ -23048,7 +23846,11 @@ function parseCharPrs(headerDoc) {
23048
23846
  if (findChildByLocalName(cp, "underline")) def.underline = true;
23049
23847
  const fontRef = findChildByLocalName(cp, "fontRef");
23050
23848
  const hangul = fontRef?.getAttribute("hangul");
23051
- 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
+ }
23052
23854
  map.set(id, def);
23053
23855
  }
23054
23856
  return map;
@@ -23076,6 +23878,8 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23076
23878
  const usedBf = /* @__PURE__ */ new Set();
23077
23879
  const usedCp = /* @__PURE__ */ new Set();
23078
23880
  const colWidths = new Array(cols).fill(void 0);
23881
+ const spanCells = [];
23882
+ const row0Texts = /* @__PURE__ */ new Map();
23079
23883
  let anchorText = "";
23080
23884
  for (const tc2 of elemsByLocal(tbl2, "tc")) {
23081
23885
  if (nearestTable(tc2) !== tbl2) continue;
@@ -23085,6 +23889,7 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23085
23889
  const row = num(addr?.getAttribute("rowAddr")) ?? 0;
23086
23890
  const col = num(addr?.getAttribute("colAddr")) ?? 0;
23087
23891
  if (row === 0 && col === 0 && !anchorText) anchorText = directCellText(tc2);
23892
+ if (row === 0) row0Texts.set(col, directCellText(tc2));
23088
23893
  const colSpan = num(span?.getAttribute("colSpan")) ?? 1;
23089
23894
  const rowSpan = num(span?.getAttribute("rowSpan")) ?? 1;
23090
23895
  const bfId = tc2.getAttribute("borderFillIDRef") || void 0;
@@ -23103,7 +23908,20 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23103
23908
  usedCp.add(cpId);
23104
23909
  }
23105
23910
  cells.push(cell2);
23106
- 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;
23107
23925
  }
23108
23926
  const table2 = {
23109
23927
  table_index: tableIndex,
@@ -23114,8 +23932,10 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23114
23932
  };
23115
23933
  const anchor = normalizeAnchor(anchorText);
23116
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;
23117
23937
  if (width) table2.width_hwpunit = width;
23118
- 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);
23119
23939
  const cp = pick(charPrs, usedCp);
23120
23940
  if (Object.keys(cp).length) table2.used_char_prs = cp;
23121
23941
  return table2;
@@ -23172,7 +23992,7 @@ async function hwpxToProfile(input) {
23172
23992
  if (headerFile) headerXml = await headerFile.async("text");
23173
23993
  const headerDoc = parser.parseFromString(headerXml, "text/xml");
23174
23994
  const borderFills = parseBorderFills(headerDoc);
23175
- const charPrs = parseCharPrs(headerDoc);
23995
+ const charPrs = parseCharPrs(headerDoc, parseHangulFonts(headerDoc));
23176
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));
23177
23997
  const tables = [];
23178
23998
  let tableIndex = 0;
@@ -23183,7 +24003,7 @@ async function hwpxToProfile(input) {
23183
24003
  tables.push(parseTable3(tbl2, tableIndex++, borderFills, charPrs));
23184
24004
  }
23185
24005
  }
23186
- return { schema_version: "0.2.0", tables };
24006
+ return { schema_version: "0.3.0", tables };
23187
24007
  }
23188
24008
 
23189
24009
  // src/hwpx/font-catalog.ts
@@ -23323,7 +24143,7 @@ var COMMON_FREE = [
23323
24143
  "\uC724\uBA85\uC870"
23324
24144
  ];
23325
24145
  function norm(name) {
23326
- return name.replace(/\s+/g, "").toLowerCase();
24146
+ return name.normalize("NFC").replace(/\s+/g, "").toLowerCase();
23327
24147
  }
23328
24148
  var KNOWN = new Set([...HANCOM_BUNDLED, ...WINDOWS_DEFAULT, ...COMMON_FREE].map(norm));
23329
24149
  function isKnownFont(name) {
@@ -23353,7 +24173,7 @@ var RULES = [
23353
24173
  {
23354
24174
  code: "DATE_ZERO_PAD",
23355
24175
  severity: "error",
23356
- 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,
23357
24177
  message: "\uC6D4\xB7\uC77C \uC55E\uC758 '0'\uC740 \uD45C\uAE30\uD558\uC9C0 \uC54A\uC74C",
23358
24178
  suggest: "\uC608) 2025. 1. 6. (2025. 01. 06. \u2715)"
23359
24179
  },
@@ -23435,22 +24255,25 @@ var RULES = [
23435
24255
  {
23436
24256
  code: "COLON_SPACE",
23437
24257
  severity: "warning",
23438
- pattern: /\S\s+:\S|\S:(?!\/\/)[^\s\d]/g,
24258
+ pattern: /\S\s+:(?!\/\/)|\S:(?!\/\/)[^\s\d]/g,
23439
24259
  message: "\uC30D\uC810\uC740 \uC55E\uB9D0\uC5D0 \uBD99\uC774\uACE0 \uB4A4\uB294 \uD55C \uCE78 \uB744\uC6C0",
23440
24260
  suggest: "\uC608) \uC6D0\uC7A5: \uAE40\uAC11\uB3D9"
23441
24261
  }
23442
24262
  ];
23443
24263
  function lintGongmunText(text) {
23444
24264
  const findings = [];
23445
- let inFence = false;
24265
+ let fenceMarker = null;
23446
24266
  const lines = text.split(/\r?\n/);
23447
24267
  for (let i = 0; i < lines.length; i++) {
23448
24268
  const line = lines[i];
23449
- if (/^\s*(```|~~~)/.test(line)) {
23450
- 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;
23451
24274
  continue;
23452
24275
  }
23453
- if (inFence) continue;
24276
+ if (fenceMarker !== null) continue;
23454
24277
  for (const r of RULES) {
23455
24278
  r.pattern.lastIndex = 0;
23456
24279
  for (const m of line.matchAll(r.pattern)) {
@@ -26207,7 +27030,7 @@ async function parseHwp(buffer, options) {
26207
27030
  async function parsePdf(buffer, options) {
26208
27031
  let parsePdfDocument;
26209
27032
  try {
26210
- const mod = await import("./parser-AODAQQLQ.js");
27033
+ const mod = await import("./parser-46JGSTPR.js");
26211
27034
  parsePdfDocument = mod.parsePdfDocument;
26212
27035
  } catch {
26213
27036
  return {
@@ -26298,6 +27121,9 @@ async function fillForm(input, values, outputFormat = "markdown") {
26298
27121
  }
26299
27122
 
26300
27123
  export {
27124
+ PRESET_ALIAS,
27125
+ normalizeGongmunPreset,
27126
+ incompatibleGongmunWarnings,
26301
27127
  extractHwpxMetadataOnly,
26302
27128
  extractHwp5MetadataOnly,
26303
27129
  formatFillValue,
@@ -26309,8 +27135,6 @@ export {
26309
27135
  extractFormSchema,
26310
27136
  fillFormFields,
26311
27137
  fillHwpx,
26312
- PRESET_ALIAS,
26313
- normalizeGongmunPreset,
26314
27138
  markdownToHwpx,
26315
27139
  compare,
26316
27140
  diffBlocks,
@@ -26339,4 +27163,4 @@ export {
26339
27163
  parseHwpml,
26340
27164
  fillForm
26341
27165
  };
26342
- //# sourceMappingURL=chunk-K2SETHJ6.js.map
27166
+ //# sourceMappingURL=chunk-MLNHZN32.js.map