kordoc 4.0.3 → 4.0.5

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 → -QVHGOOCR.js} +9 -7
  3. package/dist/chunk-5RPYBC2Q.js +88 -0
  4. package/dist/chunk-5RPYBC2Q.js.map +1 -0
  5. package/dist/{chunk-6JSZBSOD.js → chunk-6BABRVOJ.js} +2 -2
  6. package/dist/{chunk-PNQDQ3SM.js → chunk-6PED5OOG.js} +34 -4
  7. package/dist/chunk-6PED5OOG.js.map +1 -0
  8. package/dist/{chunk-OVESZY5R.cjs → chunk-6R5FY47W.cjs} +38 -8
  9. package/dist/chunk-6R5FY47W.cjs.map +1 -0
  10. package/dist/{chunk-RO2GVZ3I.js → chunk-6WDPB4SN.js} +3 -3
  11. package/dist/{chunk-TQYX5E32.js → chunk-FKK4K53F.js} +34 -4
  12. package/dist/chunk-FKK4K53F.js.map +1 -0
  13. package/dist/{chunk-MQLKSRIK.js → chunk-OMERK4AW.js} +76 -25
  14. package/dist/chunk-OMERK4AW.js.map +1 -0
  15. package/dist/{chunk-AUJICYJG.js → chunk-QK2G34WE.js} +4 -3
  16. package/dist/chunk-QK2G34WE.js.map +1 -0
  17. package/dist/{chunk-K2SETHJ6.js → chunk-Z6Q3YG22.js} +1632 -765
  18. package/dist/chunk-Z6Q3YG22.js.map +1 -0
  19. package/dist/chunk-ZGMMLYAL.js +92 -0
  20. package/dist/chunk-ZGMMLYAL.js.map +1 -0
  21. package/dist/cli.js +119 -72
  22. package/dist/cli.js.map +1 -1
  23. package/dist/index.cjs +21162 -20243
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +53 -1
  26. package/dist/index.d.ts +53 -1
  27. package/dist/index.js +20987 -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-2V5SPTU7.cjs} +106 -17
  32. package/dist/parser-2V5SPTU7.cjs.map +1 -0
  33. package/dist/{parser-AODAQQLQ.js → parser-3MAYZUJX.js} +95 -6
  34. package/dist/parser-3MAYZUJX.js.map +1 -0
  35. package/dist/{parser-J7YREDNS.js → parser-YSHZC5FQ.js} +94 -5
  36. package/dist/parser-YSHZC5FQ.js.map +1 -0
  37. package/dist/profile-io-3EECPDHG.js +11 -0
  38. package/dist/render-MJSQ4TG2.js +10 -0
  39. package/dist/seal-R3POFNOU.js +10 -0
  40. package/dist/seal-R3POFNOU.js.map +1 -0
  41. package/dist/{watch-P65Z24AL.js → watch-7MHSCKEQ.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 → -QVHGOOCR.js.map} +0 -0
  55. /package/dist/{chunk-6JSZBSOD.js.map → chunk-6BABRVOJ.js.map} +0 -0
  56. /package/dist/{chunk-RO2GVZ3I.js.map → chunk-6WDPB4SN.js.map} +0 -0
  57. /package/dist/{render-JPP7YTV3.js.map → profile-io-3EECPDHG.js.map} +0 -0
  58. /package/dist/{seal-MDE4DXZQ.js.map → render-MJSQ4TG2.js.map} +0 -0
  59. /package/dist/{watch-P65Z24AL.js.map → watch-7MHSCKEQ.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-6PED5OOG.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-6WDPB4SN.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-OMERK4AW.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-QK2G34WE.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-6BABRVOJ.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,46 @@ 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
+ const cellSpanMode = spanModeOf(ctx.shared.kordocLayout);
1561
+ if (cellSpanMode && !ph?.prefix) {
1562
+ const spans = extractRunSpans(el, ctx, cellSpanMode, cellSpanMode !== "kordoc");
1563
+ if (spans && spans.map((s) => s.text).join("").replace(/[ \t]+/g, " ").trim() === text) {
1564
+ cellBlock.spans = spans;
1565
+ }
1566
+ }
1567
+ ;
1568
+ (cell2.blocks ??= []).push(cellBlock);
1135
1569
  } else if (!tableCtx) {
1570
+ if (/^─{10,}$/.test(text)) {
1571
+ blocks.push({ type: "separator", pageNumber: ctx.sectionNum });
1572
+ tableCtx = walkParagraphChildren(el, blocks, tableCtx, tableStack, ctx, depth + 1);
1573
+ break;
1574
+ }
1136
1575
  const block = { type: headingLevel ? "heading" : "paragraph", text, pageNumber: ctx.sectionNum };
1137
1576
  if (headingLevel) block.level = headingLevel;
1138
1577
  if (style) block.style = style;
1139
1578
  if (href) block.href = href;
1140
1579
  if (footnote) block.footnoteText = footnote;
1580
+ const ind = ctx.styleMap?.paraIndents.get(el.getAttribute("paraPrIDRef") ?? "");
1581
+ if (ind) {
1582
+ const eff = ind.left + Math.max(0, ind.intent);
1583
+ if (eff > 0) block.indent = eff;
1584
+ }
1585
+ if (ctx.shared.kordocLayout === "gongmun" && block.indent && /^(?:-|\d{1,3}\)) /.test(text)) {
1586
+ const depth2 = gongmunDepthFromIndent(el, block.indent, ctx);
1587
+ if (depth2) block.listDepth = depth2;
1588
+ }
1589
+ const spanMode = spanModeOf(ctx.shared.kordocLayout);
1590
+ if (!headingLevel && spanMode) {
1591
+ if (!ph?.prefix) {
1592
+ const spans = extractRunSpans(el, ctx, spanMode, spanMode === "gongmun");
1593
+ if (spans && spans.map((s) => s.text).join("").replace(/[ \t]+/g, " ").trim() === text) {
1594
+ block.spans = spans;
1595
+ }
1596
+ }
1597
+ if (spanMode !== "foreign" && el.getAttribute("paraPrIDRef") === KORDOC_PARA_QUOTE) block.quote = true;
1598
+ }
1141
1599
  blocks.push(block);
1142
1600
  } else {
1143
1601
  blocks.push({ type: "paragraph", text, pageNumber: ctx.sectionNum });
@@ -1219,6 +1677,34 @@ function mergeBlocksIntoCell(cell2, sink) {
1219
1677
  }
1220
1678
  }
1221
1679
  }
1680
+ function kordocTableChannel(tblEl, ctx) {
1681
+ const found = findKordocMarkedCell(tblEl, 0);
1682
+ if (!found) return null;
1683
+ const m = found.name.match(/^__kordoc_(?:h([1-6])|(toc|skip))$/);
1684
+ if (!m) return null;
1685
+ if (m[2]) return { kind: "skip" };
1686
+ const text = collectSubListText(found.cell, ctx).trim();
1687
+ return { kind: "heading", level: Number(m[1]), text };
1688
+ }
1689
+ function findKordocMarkedCell(el, depth) {
1690
+ if (depth > 6) return null;
1691
+ const children = el.childNodes;
1692
+ if (!children) return null;
1693
+ for (let i = 0; i < children.length; i++) {
1694
+ const ch = children[i];
1695
+ if (ch.nodeType !== 1) continue;
1696
+ const tag = (ch.tagName || ch.localName || "").replace(/^[^:]+:/, "");
1697
+ if (tag === "tc") {
1698
+ const name = ch.getAttribute("name") || "";
1699
+ if (name.startsWith("__kordoc_")) return { cell: ch, name };
1700
+ continue;
1701
+ }
1702
+ if (tag === "tbl" && depth > 0) continue;
1703
+ const found = findKordocMarkedCell(ch, depth + 1);
1704
+ if (found) return found;
1705
+ }
1706
+ return null;
1707
+ }
1222
1708
  function collectSubListText(el, ctx, depth = 0) {
1223
1709
  if (depth > 10) return "";
1224
1710
  const parts = [];
@@ -1254,6 +1740,15 @@ function walkParagraphChildren(node, blocks, tableCtx, tableStack, ctx, depth =
1254
1740
  const tag = el.tagName || el.localName || "";
1255
1741
  const localTag = tag.replace(/^[^:]+:/, "");
1256
1742
  if (localTag === "tbl") {
1743
+ if (!tableCtx) {
1744
+ const chan = kordocTableChannel(el, ctx);
1745
+ if (chan) {
1746
+ if (chan.kind === "heading" && chan.text) {
1747
+ blocks.push({ type: "heading", level: chan.level, text: chan.text, pageNumber: ctx.sectionNum });
1748
+ }
1749
+ continue;
1750
+ }
1751
+ }
1257
1752
  if (tableCtx) tableStack.push(tableCtx);
1258
1753
  const newTable = { rows: [], currentRow: [], cell: null };
1259
1754
  walkSection(el, blocks, newTable, tableStack, ctx, d + 1);
@@ -1587,6 +2082,92 @@ function extractParagraphInfo(para2, styleMap, ctx) {
1587
2082
  }
1588
2083
  return { text: cleanText, href, footnote, style };
1589
2084
  }
2085
+ var KORDOC_CHAR_CODE = "4";
2086
+ var KORDOC_PARA_QUOTE = "6";
2087
+ function spanModeOf(layout) {
2088
+ if (layout === "default") return "kordoc";
2089
+ if (layout === "gongmun") return "gongmun";
2090
+ if (!layout) return "foreign";
2091
+ return null;
2092
+ }
2093
+ function extractRunSpans(para2, ctx, mode, requireMixed) {
2094
+ const styleMap = ctx.styleMap;
2095
+ if (!styleMap) return null;
2096
+ const spans = [];
2097
+ let styled = false;
2098
+ const kids2 = para2.childNodes;
2099
+ if (!kids2) return null;
2100
+ for (let i = 0; i < kids2.length; i++) {
2101
+ const child = kids2[i];
2102
+ if (child.nodeType !== 1) continue;
2103
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
2104
+ if (tag === "linesegarray") continue;
2105
+ if (tag !== "run" && tag !== "r") return null;
2106
+ let text = "";
2107
+ const rkids = child.childNodes;
2108
+ for (let j = 0; j < (rkids?.length ?? 0); j++) {
2109
+ const rc = rkids[j];
2110
+ if (rc.nodeType !== 1) continue;
2111
+ const rtag = (rc.tagName || rc.localName || "").replace(/^[^:]+:/, "");
2112
+ if (rtag === "t") {
2113
+ const tkids = rc.childNodes;
2114
+ for (let k = 0; k < (tkids?.length ?? 0); k++) {
2115
+ const tk = tkids[k];
2116
+ if (tk.nodeType === 3) text += tk.textContent || "";
2117
+ else if (tk.nodeType === 1) return null;
2118
+ }
2119
+ } else if (rtag === "secPr" || rtag === "colPr") {
2120
+ } else if (rtag === "ctrl") {
2121
+ if (extractTextFromNode(rc)) return null;
2122
+ } else {
2123
+ return null;
2124
+ }
2125
+ }
2126
+ if (!text) continue;
2127
+ const prId = child.getAttribute("charPrIDRef") ?? "";
2128
+ const cp = styleMap.charProperties.get(prId);
2129
+ const span = { text };
2130
+ if (mode !== "foreign" && prId === KORDOC_CHAR_CODE) span.code = true;
2131
+ else {
2132
+ if (cp?.bold) span.bold = true;
2133
+ if (cp?.italic) span.italic = true;
2134
+ }
2135
+ if (span.bold || span.italic || span.code) styled = true;
2136
+ spans.push(span);
2137
+ }
2138
+ if (!styled || spans.length === 0) return null;
2139
+ const merged = [];
2140
+ for (const s of spans) {
2141
+ const last = merged[merged.length - 1];
2142
+ if (last && !!last.bold === !!s.bold && !!last.italic === !!s.italic && !!last.code === !!s.code) {
2143
+ last.text += s.text;
2144
+ } else {
2145
+ merged.push(s);
2146
+ }
2147
+ }
2148
+ if (requireMixed && !merged.some((s) => !(s.bold || s.italic || s.code))) return null;
2149
+ return merged;
2150
+ }
2151
+ function gongmunDepthFromIndent(para2, left, ctx) {
2152
+ const styleMap = ctx.styleMap;
2153
+ if (!styleMap) return null;
2154
+ let unit = 0;
2155
+ const kids2 = para2.childNodes;
2156
+ for (let i = 0; i < (kids2?.length ?? 0); i++) {
2157
+ const child = kids2[i];
2158
+ if (child.nodeType !== 1) continue;
2159
+ const tag = (child.tagName || child.localName || "").replace(/^[^:]+:/, "");
2160
+ if (tag !== "run" && tag !== "r") continue;
2161
+ const size = styleMap.charProperties.get(child.getAttribute("charPrIDRef") ?? "")?.fontSize;
2162
+ if (size) unit = size * 100;
2163
+ break;
2164
+ }
2165
+ if (!unit) return null;
2166
+ const r = left / unit;
2167
+ const nearInt = Math.round(r);
2168
+ const depth = Math.abs(r - nearInt) < 0.2 ? nearInt : Math.round(2 * r - 1);
2169
+ return depth >= 1 && depth < 8 ? depth : null;
2170
+ }
1590
2171
 
1591
2172
  // src/hwpx/images.ts
1592
2173
  function imageExtToMime(ext) {
@@ -1776,6 +2357,17 @@ function extractFromBrokenZip(buffer) {
1776
2357
  const markdown = blocksToMarkdown(blocks);
1777
2358
  return { markdown, blocks, warnings: warnings.length > 0 ? warnings : void 0 };
1778
2359
  }
2360
+ async function readKordocLayout(zip) {
2361
+ const file = zip.file("Contents/content.hpf") ?? zip.file("content.hpf");
2362
+ if (!file) return null;
2363
+ try {
2364
+ const xml = await file.async("text");
2365
+ if (!/<opf:meta\b[^>]*name="generator"[^>]*content="kordoc"/.test(xml)) return null;
2366
+ return xml.match(/<opf:meta\b[^>]*name="kordoc-layout"[^>]*content="([^"]*)"/)?.[1] ?? null;
2367
+ } catch {
2368
+ return null;
2369
+ }
2370
+ }
1779
2371
  async function resolveSectionPaths(zip) {
1780
2372
  const manifestPaths = ["Contents/content.hpf", "content.hpf"];
1781
2373
  for (const mp of manifestPaths) {
@@ -1906,6 +2498,7 @@ async function parseHwpxDocument(buffer, options) {
1906
2498
  const totalTarget = pageFilter ? pageFilter.size : sectionPaths.length;
1907
2499
  const blocks = [];
1908
2500
  const shared = createSectionShared();
2501
+ shared.kordocLayout = await readKordocLayout(zip);
1909
2502
  let parsedSections = 0;
1910
2503
  for (let si = 0; si < sectionPaths.length; si++) {
1911
2504
  if (pageFilter && !pageFilter.has(si + 1)) continue;
@@ -19738,364 +20331,29 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
19738
20331
  for (const k of l.matchKeys) failedKeys.add(k);
19739
20332
  continue;
19740
20333
  }
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);
20334
+ for (const k of l.matchKeys) succeededKeys.add(k);
20335
+ splices.push(...paraSplices);
20336
+ }
20337
+ if (splices.length > 0) {
20338
+ splices.push(...allLinesegRemovalSplices(xml));
20339
+ replacements.set(sectionPaths[si], encoder.encode(applySplices(xml, splices)));
19955
20340
  }
19956
20341
  }
19957
- if (opts.approval && opts.approval.length > 6) {
19958
- throw new KordocError("approval must contain at most 6 labels");
20342
+ for (const k of failedKeys) {
20343
+ if (!succeededKeys.has(k)) matchedLabels.delete(k);
19959
20344
  }
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";
20345
+ const cleanFilled = filled.filter((f) => f !== null);
20346
+ const unmatched = resolveUnmatched(normalizedValues, matchedLabels, values);
20347
+ const out = replacements.size > 0 ? patchZipEntries(u8, replacements) : new Uint8Array(u8);
19971
20348
  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
20349
+ buffer: out.buffer.slice(out.byteOffset, out.byteOffset + out.byteLength),
20350
+ filled: cleanFilled,
20351
+ unmatched
20008
20352
  };
20009
20353
  }
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
- }
20354
+
20355
+ // src/hwpx/generator.ts
20356
+ import JSZip6 from "jszip";
20099
20357
 
20100
20358
  // src/hwpx/gen-ids.ts
20101
20359
  var NS_SECTION = "http://www.hancom.co.kr/hwpml/2011/section";
@@ -20140,7 +20398,7 @@ function resolveTheme(theme) {
20140
20398
  };
20141
20399
  }
20142
20400
  function escapeXml(text) {
20143
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20401
+ return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
20144
20402
  }
20145
20403
  function headingParaPrId(level) {
20146
20404
  if (level === 1) return PARA_H1;
@@ -20159,7 +20417,7 @@ function charPr(id, height, bold, italic, fontId = 0, textColor = DEFAULT_TEXT_C
20159
20417
  const italicAttr = italic ? ` italic="1"` : "";
20160
20418
  const effFont = fontId;
20161
20419
  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}>
20420
+ return ` <hh:charPr id="${id}" height="${height}" textColor="${escapeXml(textColor)}" shadeColor="none" useFontSpace="0" useKerning="0" symMark="NONE" borderFillIDRef="1"${boldAttr}${italicAttr}>
20163
20421
  <hh:fontRef hangul="${effFont}" latin="${effFont}" hanja="${effFont}" japanese="${effFont}" other="${effFont}" symbol="${effFont}" user="${effFont}"/>
20164
20422
  <hh:ratio hangul="${ratioPct}" latin="${ratioPct}" hanja="${ratioPct}" japanese="100" other="100" symbol="100" user="100"/>
20165
20423
  <hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
@@ -20205,7 +20463,7 @@ var GJ_CHAR_BAR = 24;
20205
20463
  var GJ_CHAR_BODY_TITLE = 25;
20206
20464
  var GJ_CHAR_TITLE_BAR = 26;
20207
20465
  var GJ_CHAR_APPROVAL = 27;
20208
- var GJ_CHAR_COUNT = 17;
20466
+ var GJ_CHAR_COUNT = GJ_CHAR_APPROVAL - GJ_CHAR_DAE + 1;
20209
20467
  var GJ_PARA_CHAM = 20;
20210
20468
  var GJ_PARA_COVER = 21;
20211
20469
  var GJ_PARA_TOC_ITEM = 22;
@@ -20213,6 +20471,7 @@ var GJ_PARA_CHAPTER = 23;
20213
20471
  var GJ_PARA_BAR = 24;
20214
20472
  var GONGMUN_LIST_PLAIN_BASE = 25;
20215
20473
  var GONGMUN_PARA_APPROVAL = GONGMUN_LIST_PLAIN_BASE + GONGMUN_LIST_LEVELS;
20474
+ var GONGMUN_LIST_VARIANT_BASE = GONGMUN_PARA_APPROVAL + 1;
20216
20475
  var GJ_BF_CHAPTER_NUM = 3;
20217
20476
  var GJ_BF_CHAPTER_GAP = 4;
20218
20477
  var GJ_BF_CHAPTER_TITLE = 5;
@@ -20256,10 +20515,10 @@ var GONGMUN_TBL_CHAR_BOLD = 12;
20256
20515
  var GONGMUN_TBL_PT = 1200;
20257
20516
  var GONGMUN_TITLE_BAR_CHAR = 13;
20258
20517
  var GONGMUN_APPROVAL_CHAR = 14;
20259
- var CHAR_VARIANT_BASE = 11;
20518
+ var CHAR_VARIANT_BASE = CHAR_QUOTE + 1;
20260
20519
  function charVariantBase(richAssets, isGongmun = true) {
20261
20520
  if (!isGongmun) return CHAR_VARIANT_BASE;
20262
- return richAssets ? 11 + GJ_CHAR_COUNT : 11 + 4;
20521
+ return richAssets ? CHAR_VARIANT_BASE + GJ_CHAR_COUNT : CHAR_VARIANT_BASE + (GONGMUN_APPROVAL_CHAR - GONGMUN_TBL_CHAR + 1);
20263
20522
  }
20264
20523
  var GONGMUN_BODY_RATIO = 95;
20265
20524
 
@@ -20292,6 +20551,7 @@ function parseMarkdownToBlocks(md2) {
20292
20551
  const lines = md2.replace(/\r\n?/g, "\n").split("\n");
20293
20552
  const blocks = [];
20294
20553
  let i = 0;
20554
+ const listStack = [];
20295
20555
  while (i < lines.length) {
20296
20556
  const line = lines[i];
20297
20557
  if (!line.trim()) {
@@ -20380,7 +20640,8 @@ function parseMarkdownToBlocks(md2) {
20380
20640
  const tableRows = [];
20381
20641
  while (i < lines.length && lines[i].trimStart().startsWith("|")) {
20382
20642
  const row = lines[i];
20383
- if (/^[\s|:\-]+$/.test(row)) {
20643
+ const sepCells = row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
20644
+ if (sepCells.every((c) => /^\s*:?-+:?\s*$/.test(c))) {
20384
20645
  i++;
20385
20646
  continue;
20386
20647
  }
@@ -20394,17 +20655,36 @@ function parseMarkdownToBlocks(md2) {
20394
20655
  if (line.trimStart().startsWith("> ")) {
20395
20656
  const quoteLines = [];
20396
20657
  while (i < lines.length && (lines[i].trimStart().startsWith("> ") || lines[i].trimStart().startsWith(">"))) {
20397
- quoteLines.push(lines[i].replace(/^>\s?/, ""));
20658
+ quoteLines.push(lines[i].replace(/^>\s?/, "").trim());
20398
20659
  i++;
20399
20660
  }
20661
+ let joined = [];
20400
20662
  for (const ql of quoteLines) {
20401
- blocks.push({ type: "blockquote", text: ql.trim() || "" });
20663
+ if (ql) {
20664
+ joined.push(ql);
20665
+ continue;
20666
+ }
20667
+ if (joined.length) blocks.push({ type: "blockquote", text: joined.join("\n") });
20668
+ joined = [];
20402
20669
  }
20670
+ if (joined.length) blocks.push({ type: "blockquote", text: joined.join("\n") });
20403
20671
  continue;
20404
20672
  }
20405
- const listMatch = line.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
20673
+ const listLine = line.replace(/^[\t ]+/, (ws) => ws.replace(/\t/g, " "));
20674
+ const listMatch = listLine.match(/^(\s*)([-*+]|\d+[.)]) (.+)$/);
20406
20675
  if (listMatch) {
20407
- const indent = Math.floor(listMatch[1].length / 2);
20676
+ if (blocks.length === 0 || blocks[blocks.length - 1].type !== "list_item") listStack.length = 0;
20677
+ const phys = listMatch[1].length;
20678
+ let indent;
20679
+ if (listStack.length === 0) {
20680
+ indent = Math.floor(phys / 2);
20681
+ for (let d = 0; d < indent; d++) listStack.push(d * 2);
20682
+ listStack.push(phys);
20683
+ } else {
20684
+ while (listStack.length > 1 && phys < listStack[listStack.length - 1]) listStack.pop();
20685
+ if (phys > listStack[listStack.length - 1]) listStack.push(phys);
20686
+ indent = listStack.length - 1;
20687
+ }
20408
20688
  const ordered = /\d/.test(listMatch[2]);
20409
20689
  blocks.push({ type: "list_item", text: listMatch[3].trim(), ordered, indent, marker: listMatch[2] });
20410
20690
  i++;
@@ -20425,7 +20705,7 @@ function parseInlineMarkdown(text) {
20425
20705
  text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, (_, t, u) => t || u);
20426
20706
  text = text.replace(/~~([^~]+)~~/g, "$1");
20427
20707
  const spans = [];
20428
- const regex = /(`[^`]+`|\*{3}[^*]+\*{3}|\*{2}[^*]+\*{2}|\*[^*]+\*|_{2}[^_]+_{2}|_[^_]+_)/g;
20708
+ 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
20709
  let lastIdx = 0;
20430
20710
  for (const match of text.matchAll(regex)) {
20431
20711
  const idx = match.index;
@@ -20491,14 +20771,20 @@ function generateContainerXml() {
20491
20771
  </ocf:rootfiles>
20492
20772
  </ocf:container>`;
20493
20773
  }
20494
- function generateManifest(chartParts = []) {
20774
+ function generateManifest(chartParts = [], imageItems = [], layout = "default") {
20495
20775
  const chartItems = chartParts.map((p, i) => `
20496
20776
  <opf:item id="chart${i + 1}" href="${p.name}" media-type="application/xml"/>`).join("");
20777
+ const imgItems = imageItems.map((x) => `
20778
+ ${x}`).join("");
20497
20779
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
20498
20780
  <opf:package xmlns:opf="${NS_OPF}" xmlns:hpf="${NS_HPF}" xmlns:hh="${NS_HEAD}">
20781
+ <opf:metadata>
20782
+ <opf:meta name="generator" content="kordoc"/>
20783
+ <opf:meta name="kordoc-layout" content="${escapeXml(layout)}"/>
20784
+ </opf:metadata>
20499
20785
  <opf:manifest>
20500
20786
  <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}
20787
+ <opf:item id="section0" href="Contents/section0.xml" media-type="application/xml"/>${chartItems}${imgItems}
20502
20788
  </opf:manifest>
20503
20789
  <opf:spine>
20504
20790
  <opf:itemref idref="header" linear="no"/>
@@ -20506,6 +20792,17 @@ function generateManifest(chartParts = []) {
20506
20792
  </opf:spine>
20507
20793
  </opf:package>`;
20508
20794
  }
20795
+ function assertSequentialIds(rows, label, startId) {
20796
+ for (let i = 0; i < rows.length; i++) {
20797
+ const m = rows[i].match(/\bid="(\d+)"/);
20798
+ const got = m ? Number(m[1]) : NaN;
20799
+ if (got !== startId + i) {
20800
+ throw new KordocError(
20801
+ `\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)`
20802
+ );
20803
+ }
20804
+ }
20805
+ }
20509
20806
  function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls = []) {
20510
20807
  const measured = !!gongmun && usesReportFonts(gongmun.preset);
20511
20808
  const richAssets = !!gongmun && needsGaejosikAssets(gongmun);
@@ -20513,8 +20810,8 @@ function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls
20513
20810
  if (gongmun) {
20514
20811
  body = gongmun.bodyHeight;
20515
20812
  code = Math.max(body - 200, 900);
20516
- h1 = gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700;
20517
- h2 = 1600;
20813
+ h1 = Math.max(gongmun.preset === "report" || gongmun.preset === "plan" ? 2e3 : 1700, body);
20814
+ h2 = Math.max(1600, body);
20518
20815
  h3 = body;
20519
20816
  h4 = Math.min(h3, Math.max(body - 100, 1300));
20520
20817
  }
@@ -20582,6 +20879,12 @@ function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls
20582
20879
  // 결재란 라벨(맑은 고딕 10pt)
20583
20880
  );
20584
20881
  }
20882
+ const expectedBase = charVariantBase(richAssets, !!gongmun);
20883
+ if (rows.length !== expectedBase) {
20884
+ throw new KordocError(
20885
+ `\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`
20886
+ );
20887
+ }
20585
20888
  for (const r of ratioVariants) {
20586
20889
  rows.push(
20587
20890
  charPr(rows.length, body, false, false, bodyFont, theme.body, r),
@@ -20591,11 +20894,12 @@ function buildCharProperties(theme, gongmun, ratioVariants = [], extraCharPrXmls
20591
20894
  );
20592
20895
  }
20593
20896
  rows.push(...extraCharPrXmls);
20897
+ assertSequentialIds(rows, "charPr", 0);
20594
20898
  return `<hh:charProperties itemCnt="${rows.length}">
20595
20899
  ${rows.join("\n")}
20596
20900
  </hh:charProperties>`;
20597
20901
  }
20598
- function buildParaProperties(gongmun) {
20902
+ function buildParaProperties(gongmun, listIndentVariants = []) {
20599
20903
  if (!gongmun) {
20600
20904
  const base2 = [
20601
20905
  paraPr(0, { keepWord: true }),
@@ -20607,6 +20911,7 @@ function buildParaProperties(gongmun) {
20607
20911
  paraPr(6, { align: "LEFT", lineSpacing: 150, indent: 600, keepWord: true }),
20608
20912
  paraPr(7, { align: "LEFT", lineSpacing: 160, indent: 600, keepWord: true })
20609
20913
  ];
20914
+ assertSequentialIds(base2, "paraPr", 0);
20610
20915
  return `<hh:paraProperties itemCnt="${base2.length}">
20611
20916
  ${base2.join("\n")}
20612
20917
  </hh:paraProperties>`;
@@ -20653,6 +20958,14 @@ ${base2.join("\n")}
20653
20958
  base.push(paraPr(GONGMUN_LIST_PLAIN_BASE + d, { align: "JUSTIFY", lineSpacing: ls, left, indent: 0, spaceBefore: sectionGap, keepWord: true }));
20654
20959
  }
20655
20960
  base.push(paraPr(GONGMUN_PARA_APPROVAL, { align: "CENTER", lineSpacing: 100, keepWord: true }));
20961
+ for (let vi = 0; vi < listIndentVariants.length; vi++) {
20962
+ const { depth, widthHu } = listIndentVariants[vi];
20963
+ const { left } = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
20964
+ const sectionGap = gongmun.numbering === "gaejosik" || gongmun.numbering === "report" ? gaejosikSpaceBefore(depth, gongmun.bodyHeight) : 0;
20965
+ const keepNext = (gongmun.numbering === "gaejosik" || gongmun.numbering === "report") && depth === 0;
20966
+ base.push(paraPr(GONGMUN_LIST_VARIANT_BASE + vi, { align: "JUSTIFY", lineSpacing: ls, left, indent: -widthHu, spaceBefore: sectionGap, keepWord: true, keepWithNext: keepNext }));
20967
+ }
20968
+ assertSequentialIds(base, "paraPr", 0);
20656
20969
  return `<hh:paraProperties itemCnt="${base.length}">
20657
20970
  ${base.join("\n")}
20658
20971
  </hh:paraProperties>`;
@@ -20673,7 +20986,10 @@ function fontEntry(id, face, weight = 6) {
20673
20986
  <hh:typeInfo familyType="FCAT_GOTHIC" weight="${weight}" proportion="4" contrast="0" strokeVariation="1" armStyle="1" letterform="1" midline="1" xHeight="1"/>
20674
20987
  </hh:font>`;
20675
20988
  }
20676
- function buildFontFaces(gongmun, bodyFace) {
20989
+ function staticFontNext(gongmun) {
20990
+ return gongmun && needsGaejosikAssets(gongmun) ? 8 : 3;
20991
+ }
20992
+ function buildFontFaces(gongmun, bodyFace, extraFonts = []) {
20677
20993
  if (gongmun && needsGaejosikAssets(gongmun)) {
20678
20994
  const ov = gongmun.fonts;
20679
20995
  const bodyDefault = usesReportFonts(gongmun.preset) ? gongmun.preset !== "gaejosik" && gongmun.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD734\uBA3C\uBA85\uC870" : bodyFace;
@@ -20687,21 +21003,27 @@ function buildFontFaces(gongmun, bodyFace) {
20687
21003
  "\uD55C\uC591\uC2E0\uBA85\uC870",
20688
21004
  ov.table ?? "\uB9D1\uC740 \uACE0\uB515"
20689
21005
  ];
20690
- const list = faces.map((f, i) => fontEntry(i, f, i === 2 || i === 3 ? 9 : 6)).join("\n");
21006
+ if (faces.length !== staticFontNext(gongmun)) {
21007
+ throw new Error(`fontface \uC815\uC801 \uAC1C\uC218 \uB4DC\uB9AC\uD504\uD2B8: ${faces.length} \u2260 staticFontNext ${staticFontNext(gongmun)}`);
21008
+ }
21009
+ 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
21010
  const langs = ["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
20692
21011
  return `<hh:fontfaces itemCnt="${langs.length}">
20693
- ` + langs.map((l) => ` <hh:fontface lang="${l}" fontCnt="${faces.length}">
21012
+ ` + langs.map((l) => ` <hh:fontface lang="${l}" fontCnt="${faces.length + extraFonts.length}">
20694
21013
  ${list}
20695
21014
  </hh:fontface>`).join("\n") + `
20696
21015
  </hh:fontfaces>`;
20697
21016
  }
21017
+ const extraH = extraFonts.map((f, i) => fontEntry(3 + i, f)).join("\n");
21018
+ const extraBlock = extraH ? `
21019
+ ${extraH}` : "";
20698
21020
  return `<hh:fontfaces itemCnt="7">
20699
- <hh:fontface lang="HANGUL" fontCnt="3">
21021
+ <hh:fontface lang="HANGUL" fontCnt="${3 + extraFonts.length}">
20700
21022
  ${fontEntry(0, bodyFace)}
20701
21023
  ${fontEntry(1, "\uD568\uCD08\uB86C\uB3CB\uC6C0")}
20702
- ${fontEntry(2, "HY\uACAC\uACE0\uB515", 9)}
21024
+ ${fontEntry(2, "HY\uACAC\uACE0\uB515", 9)}${extraBlock}
20703
21025
  </hh:fontface>
20704
- <hh:fontface lang="LATIN" fontCnt="3">
21026
+ <hh:fontface lang="LATIN" fontCnt="${3 + extraFonts.length}">
20705
21027
  <hh:font id="0" face="Times New Roman" type="TTF" isEmbedded="0">
20706
21028
  <hh:typeInfo familyType="FCAT_OLDSTYLE" weight="5" proportion="4" contrast="2" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="4"/>
20707
21029
  </hh:font>
@@ -20710,7 +21032,7 @@ ${fontEntry(2, "HY\uACAC\uACE0\uB515", 9)}
20710
21032
  </hh:font>
20711
21033
  <hh:font id="2" face="Arial Black" type="TTF" isEmbedded="0">
20712
21034
  <hh:typeInfo familyType="FCAT_GOTHIC" weight="9" proportion="0" contrast="0" strokeVariation="0" armStyle="0" letterform="0" midline="0" xHeight="0"/>
20713
- </hh:font>
21035
+ </hh:font>${extraBlock}
20714
21036
  </hh:fontface>
20715
21037
  <hh:fontface lang="HANJA" fontCnt="1">
20716
21038
  ${fontEntry(0, "\uD568\uCD08\uB86C\uBC14\uD0D5")}
@@ -20729,7 +21051,7 @@ ${fontEntry(0, "\uAD74\uB9BC")}
20729
21051
  </hh:fontface>
20730
21052
  </hh:fontfaces>`;
20731
21053
  }
20732
- function buildBorderFills(gongmun, extra = []) {
21054
+ function staticBorderFillItems(gongmun) {
20733
21055
  const thin = ["0.12 mm", "#000000"];
20734
21056
  const items = [
20735
21057
  borderFillEntry(1, {}),
@@ -20760,7 +21082,14 @@ function buildBorderFills(gongmun, extra = []) {
20760
21082
  if (gongmun) {
20761
21083
  items.push(borderFillEntry(items.length + 1, { l: thin, r: thin, t: thin, b: thin }, "#E6E6E6"));
20762
21084
  }
20763
- items.push(...extra);
21085
+ return items;
21086
+ }
21087
+ function staticBorderFillNext(gongmun) {
21088
+ return staticBorderFillItems(gongmun).length + 1;
21089
+ }
21090
+ function buildBorderFills(gongmun, extra = []) {
21091
+ const items = [...staticBorderFillItems(gongmun), ...extra];
21092
+ assertSequentialIds(items, "borderFill", 1);
20764
21093
  return `<hh:borderFills itemCnt="${items.length}">
20765
21094
  ${items.join("\n")}
20766
21095
  </hh:borderFills>`;
@@ -20778,15 +21107,15 @@ function buildStyles(gongmun) {
20778
21107
  ${items.join("\n ")}
20779
21108
  </hh:styles>`;
20780
21109
  }
20781
- function generateHeaderXml(theme, gongmun, ratioVariants = [], extraBorderFills = [], extraCharPrXmls = []) {
21110
+ function generateHeaderXml(theme, gongmun, ratioVariants = [], extraBorderFills = [], extraCharPrXmls = [], listIndentVariants = [], extraFonts = []) {
20782
21111
  const bodyFace = gongmun?.fonts.body ?? (gongmun?.bodyFont === "gothic" ? "\uB9D1\uC740 \uACE0\uB515" : "\uD568\uCD08\uB86C\uBC14\uD0D5");
20783
21112
  const charPropsXml = buildCharProperties(theme, gongmun, ratioVariants, extraCharPrXmls);
20784
- const paraPropsXml = buildParaProperties(gongmun);
21113
+ const paraPropsXml = buildParaProperties(gongmun, gongmun ? listIndentVariants : []);
20785
21114
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
20786
21115
  <hh:head xmlns:hh="${NS_HEAD}" xmlns:hp="${NS_PARA}" xmlns:hc="${NS_CORE}" version="1.4" secCnt="1">
20787
21116
  <hh:beginNum page="1" footnote="1" endnote="1" pic="1" tbl="1" equation="1"/>
20788
21117
  <hh:refList>
20789
- ${buildFontFaces(gongmun, bodyFace)}
21118
+ ${buildFontFaces(gongmun, bodyFace, extraFonts)}
20790
21119
  ${buildBorderFills(gongmun, extraBorderFills)}
20791
21120
  ${charPropsXml}
20792
21121
  <hh:tabProperties itemCnt="0"/>
@@ -20860,7 +21189,6 @@ function chartSpec(t) {
20860
21189
  const key = CHART_ALIAS[t.toLowerCase()] ?? Number(t);
20861
21190
  return CHART_TYPES[key] ?? CHART_TYPES[0];
20862
21191
  }
20863
- var HU_PER_MM = 7200 / 25.4;
20864
21192
  var RESERVED_KEYS = /* @__PURE__ */ new Set(["type", "cat", "size", "colors", "point_colors", "title"]);
20865
21193
  function parseChartFence(text) {
20866
21194
  let type;
@@ -21019,7 +21347,7 @@ function plainRenderText(text) {
21019
21347
  function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
21020
21348
  const minRatio = gongmun.autoFitMinRatio;
21021
21349
  if (minRatio === null || minRatio >= GONGMUN_BODY_RATIO) return null;
21022
- const pageW = 59528 - mmToHwpunit(gongmun.margins.left) - mmToHwpunit(gongmun.margins.right);
21350
+ const pageW = A4_W_HU - mmToHwpunit(gongmun.margins.left) - mmToHwpunit(gongmun.margins.right);
21023
21351
  const ratioByBlock = /* @__PURE__ */ new Map();
21024
21352
  const variants = [];
21025
21353
  for (let i = 0; i < blocks.length; i++) {
@@ -21027,14 +21355,14 @@ function computeGongmunFitPlan(blocks, gongmun, gongmunList) {
21027
21355
  let text;
21028
21356
  let firstW;
21029
21357
  let contW;
21030
- if (block.type === "list_item" && gongmunList.has(i)) {
21031
- const { marker, depth } = gongmunList.get(i);
21358
+ if (block.type === "list_item" && gongmunList.items.has(i)) {
21359
+ const { marker, depth, indentVariant } = gongmunList.items.get(i);
21032
21360
  if (gongmun.numbering === "gaejosik" && (depth === 0 || (block.text || "").trimStart().startsWith("\u203B"))) continue;
21033
21361
  const content = plainRenderText(block.text || "");
21034
21362
  text = marker ? `${marker} ${content}` : content;
21035
21363
  const li = levelIndent(depth, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
21036
21364
  const left = li.left;
21037
- const indent = marker ? li.indent : 0;
21365
+ const indent = marker ? indentVariant !== void 0 ? -gongmunList.indentVariants[indentVariant].widthHu : li.indent : 0;
21038
21366
  firstW = pageW - left - Math.max(indent, 0);
21039
21367
  contW = pageW - left - Math.max(-indent, 0);
21040
21368
  } else if (block.type === "paragraph") {
@@ -21060,9 +21388,55 @@ function variantMapper(fit, blockIdx, base = CHAR_VARIANT_BASE) {
21060
21388
  const vi = fit.variants.indexOf(r);
21061
21389
  return (id) => id >= 0 && id <= 3 ? base + vi * 4 + id : id;
21062
21390
  }
21391
+ var ORDINAL_SYLLABLES = new Set(Array.from({ length: 84 }, (_, i) => hangulOrdinal(i)));
21392
+ function literalMarkerDepth(text) {
21393
+ const t = text.trimStart();
21394
+ let m;
21395
+ if ((m = /^([가-힣])\.\s+(.*)$/s.exec(t)) && ORDINAL_SYLLABLES.has(m[1])) return { depth: 1, content: m[2] };
21396
+ if (m = /^(\d{1,3})\)\s+(.*)$/s.exec(t)) return { depth: 2, content: m[2] };
21397
+ if ((m = /^([가-힣])\)\s+(.*)$/s.exec(t)) && ORDINAL_SYLLABLES.has(m[1])) return { depth: 3, content: m[2] };
21398
+ if (m = /^\((\d{1,3})\)\s+(.*)$/s.exec(t)) return { depth: 4, content: m[2] };
21399
+ if ((m = /^\(([가-힣])\)\s+(.*)$/s.exec(t)) && ORDINAL_SYLLABLES.has(m[1])) return { depth: 5, content: m[2] };
21400
+ if (m = /^([①-⑳㉑-㉟㊱-㊿])\s+(.*)$/s.exec(t)) return { depth: 6, content: m[2] };
21401
+ if (m = /^([㉮-㉻])\s+(.*)$/s.exec(t)) return { depth: 7, content: m[2] };
21402
+ if (m = /^□\s+(.*)$/s.exec(t)) return { depth: 0, content: m[1] };
21403
+ if (m = /^[○ㅇ]\s+(.*)$/s.exec(t)) return { depth: 1, content: m[1] };
21404
+ if (m = /^ㆍ\s+(.*)$/s.exec(t)) return { depth: 3, content: m[1] };
21405
+ return null;
21406
+ }
21407
+ function reclassifyLiteralItems(blocks, depthOffset) {
21408
+ for (const b of blocks) {
21409
+ if (b.type !== "paragraph") continue;
21410
+ const raw = (b.text || "").trimStart();
21411
+ if (!raw || raw.startsWith("\u203B") || /^\*\s/.test(raw) || /^<(center|right)>/i.test(raw)) continue;
21412
+ const lit = literalMarkerDepth(raw);
21413
+ if (!lit || !lit.content.trim()) continue;
21414
+ b.type = "list_item";
21415
+ b.text = lit.content;
21416
+ b.indent = Math.max(lit.depth - depthOffset, 0);
21417
+ b.ordered = lit.depth !== 0;
21418
+ }
21419
+ }
21063
21420
  function precomputeGongmunList(blocks, gongmun) {
21064
21421
  const result = /* @__PURE__ */ new Map();
21422
+ const indentVariants = [];
21423
+ const variantIdx = /* @__PURE__ */ new Map();
21424
+ const variantOf = (marker, depth) => {
21425
+ if (!marker || gongmun.numbering !== "standard") return void 0;
21426
+ const w = markerWidth(marker, gongmun.bodyHeight);
21427
+ const rep = -levelIndent(depth, gongmun.bodyHeight, gongmun.numbering, gongmun.sizes, gongmun.bullet2, usesAsteriskThird(gongmun.preset)).indent;
21428
+ if (w === rep) return void 0;
21429
+ const key = `${depth}:${w}`;
21430
+ let vi = variantIdx.get(key);
21431
+ if (vi === void 0) {
21432
+ vi = indentVariants.length;
21433
+ indentVariants.push({ depth, widthHu: w });
21434
+ variantIdx.set(key, vi);
21435
+ }
21436
+ return vi;
21437
+ };
21065
21438
  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;
21439
+ reclassifyLiteralItems(blocks, depthOffset);
21066
21440
  let i = 0;
21067
21441
  while (i < blocks.length) {
21068
21442
  if (blocks[i].type !== "list_item") {
@@ -21089,14 +21463,21 @@ function precomputeGongmunList(blocks, gongmun) {
21089
21463
  break;
21090
21464
  }
21091
21465
  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);
21466
+ const measured = usesReportFonts(gongmun.preset);
21467
+ const isRef = (bi) => measured && ((blocks[bi].text || "").trimStart().startsWith("\u203B") || blocks[bi].marker === "*");
21468
+ const activeK = run.map((_, k) => k).filter((k) => !isRef(run[k]));
21469
+ const activeDepths = activeK.map((k) => depths[k]);
21470
+ const suppress = gongmun.suppressSingle && gongmun.numbering === "standard" ? computeSuppression(activeDepths) : activeDepths.map(() => false);
21093
21471
  const numberer = new GongmunNumberer(gongmun.numbering, gongmun.bullet2, usesAsteriskThird(gongmun.preset));
21094
21472
  run.forEach((bi, k) => {
21095
- const marker = numberer.next(depths[k], suppress[k]);
21096
- result.set(bi, { marker, depth: depths[k] });
21473
+ if (isRef(run[k])) result.set(bi, { marker: "", depth: depths[k] });
21474
+ });
21475
+ activeK.forEach((k, j) => {
21476
+ const marker = numberer.next(activeDepths[j], suppress[j]);
21477
+ result.set(run[k], { marker, depth: depths[k], indentVariant: variantOf(marker, depths[k]) });
21097
21478
  });
21098
21479
  }
21099
- return result;
21480
+ return { items: result, indentVariants };
21100
21481
  }
21101
21482
 
21102
21483
  // src/hwpx/gen-gaejosik.ts
@@ -21114,26 +21495,29 @@ function emptyPara(paraPrId = PARA_NORMAL, pageBreak = false, charPrId = CHAR_NO
21114
21495
  const brk = pageBreak ? ` pageBreak="1"` : "";
21115
21496
  return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"${brk}><hp:run charPrIDRef="${charPrId}"><hp:t></hp:t></hp:run></hp:p>`;
21116
21497
  }
21117
- var gjTableId = 92e5;
21498
+ var gjTableId = GJ_TABLE_ID_BASE;
21499
+ function resetGjTableIds() {
21500
+ gjTableId = GJ_TABLE_ID_BASE;
21501
+ }
21118
21502
  function cell(opts) {
21119
21503
  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>`;
21504
+ 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
21505
  }
21122
- function table(rows, w, h, cols, tblBf = 1) {
21123
- const outLR = w + 566 > 48e3 ? 0 : 283;
21506
+ function table(rows, w, h, cols, tblBf = 1, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21507
+ const outLR = w + 566 > bodyWidth ? 0 : 283;
21124
21508
  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
21509
  }
21126
- function buildGaejosikCover(title, gongmun) {
21127
- const g = coverGeom(gongmun.bodyHeight, gongmun.sizes);
21510
+ function buildGaejosikCover(title, gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21511
+ const g = coverGeom(gongmun.bodyHeight, gongmun.sizes, bodyWidth);
21128
21512
  const sz = gaejosikSizes(gongmun.bodyHeight, gongmun.sizes);
21129
21513
  const innerW = g.totalW - 282;
21130
21514
  const titleCharPr = simulateWrap(title, innerW, innerW, sz.coverTitle, 100, "keep").lines > 2 ? GJ_CHAR_COVER_SUB : GJ_CHAR_COVER_TITLE;
21131
21515
  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
21516
  const barEmpty = emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR);
21133
21517
  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 });
21518
+ const titleRow = cell({ bf: 1, col: 0, colSpan: 3, row: 1, w: g.totalW, h: g.titleH, paras: titlePara, name: "__kordoc_h1" });
21135
21519
  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);
21520
+ const tbl2 = table([topRow, titleRow, botRow], g.totalW, g.barH + g.titleH + g.botBarH, 3, 1, bodyWidth);
21137
21521
  const host = `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${tbl2}</hp:run></hp:p>`;
21138
21522
  const date = gongmun.cover?.date ?? formatGaejosikDate(/* @__PURE__ */ new Date());
21139
21523
  const org = gongmun.cover?.org ?? "";
@@ -21147,48 +21531,51 @@ function buildGaejosikCover(title, gongmun) {
21147
21531
  ...org ? [...Array(4).fill(subEmpty), sub(org)] : []
21148
21532
  ];
21149
21533
  }
21150
- function buildTocBanner(gongmun) {
21534
+ function buildTocBanner(gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21151
21535
  const g = tocBannerGeom(gongmun.bodyHeight, gongmun.sizes);
21152
21536
  const bfs = [GJ_BF_BAR_DARK, 1, GJ_BF_TOC_STRIPE, 1, GJ_BF_TOC_STRIPE, 1, GJ_BF_BAR_DARK];
21153
21537
  const filler = emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR);
21154
21538
  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
21539
  const row = g.colWs.map(
21156
- (w, c) => cell({ bf: bfs[c], col: c, w, h: g.h, paras: c === 3 ? label : filler })
21540
+ (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
21541
  ).join("");
21158
- const banner = table([row], g.colWs.reduce((a, b) => a + b, 0), g.h, 7, 2);
21542
+ const banner = table([row], g.colWs.reduce((a, b) => a + b, 0), g.h, 7, 2, bodyWidth);
21159
21543
  return `<hp:p paraPrIDRef="${GJ_PARA_BAR}" styleIDRef="0" pageBreak="1"><hp:run charPrIDRef="${CHAR_NORMAL}">${banner}</hp:run></hp:p>`;
21160
21544
  }
21161
- function buildGaejosikToc(chapters, gongmun) {
21162
- const label = buildTocBanner(gongmun);
21545
+ function buildGaejosikToc(chapters, gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21546
+ const boxW = Math.round(TOC_GEOM.boxW * bodyWidth / GAEJOSIK_BASE_WIDTH);
21547
+ const label = buildTocBanner(gongmun, bodyWidth);
21163
21548
  const items = chapters.map(
21164
21549
  (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
21550
  ).join("");
21166
21551
  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,
21552
+ [cell({ bf: GJ_BF_TOC_BOX, col: 0, w: boxW, h: TOC_GEOM.boxH, paras: items, name: "__kordoc_toc" })],
21553
+ boxW,
21169
21554
  TOC_GEOM.boxH,
21170
- 1
21555
+ 1,
21556
+ 1,
21557
+ bodyWidth
21171
21558
  );
21172
21559
  const host = `<hp:p paraPrIDRef="${GJ_PARA_BAR}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${box}</hp:run></hp:p>`;
21173
21560
  return [label, emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR), host];
21174
21561
  }
21175
- function buildGaejosikBodyTitle(title, gongmun) {
21176
- const g = COVER_GEOM;
21562
+ function buildGaejosikBodyTitle(title, gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH) {
21563
+ const g = coverGeom(gongmun.bodyHeight, gongmun.sizes, bodyWidth);
21177
21564
  const bt = bodyTitleGeom(gongmun.bodyHeight, gongmun.sizes);
21178
21565
  const barEmpty = emptyPara(GJ_PARA_BAR, false, GJ_CHAR_BAR);
21179
21566
  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
21567
  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 });
21568
+ const titleRow = cell({ bf: 1, col: 0, colSpan: 3, row: 1, w: g.totalW, h: bt.titleH, paras: titlePara, name: "__kordoc_skip" });
21182
21569
  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);
21570
+ const box = table([topRow, titleRow, botRow], g.totalW, bt.barH * 2 + bt.titleH, 3, 1, bodyWidth);
21184
21571
  return `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${box}</hp:run></hp:p>`;
21185
21572
  }
21186
- function buildGaejosikChapter(n, title, gongmun) {
21187
- const g = gongmun ? chapterGeom(gongmun.bodyHeight, gongmun.sizes) : chapterGeom(1500);
21573
+ function buildGaejosikChapter(n, title, gongmun, bodyWidth = GAEJOSIK_BASE_WIDTH, srcLevel = 2) {
21574
+ const g = gongmun ? chapterGeom(gongmun.bodyHeight, gongmun.sizes, bodyWidth) : chapterGeom(1500, {}, bodyWidth);
21188
21575
  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
21576
  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);
21577
+ 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)}` });
21578
+ const tbl2 = table([row], g.numW + g.gapW + g.titleW, g.rowH, 3, 1, bodyWidth);
21192
21579
  return `<hp:p paraPrIDRef="${GJ_PARA_CHAPTER}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${tbl2}</hp:run></hp:p>`;
21193
21580
  }
21194
21581
 
@@ -21556,16 +21943,22 @@ var AUTONUM_PREFIX_RE = /^(?:[0-90-9a-zA-Z가-힣]{1,6}[.)\]:]|[([][0-90-
21556
21943
  function htmlCellInnerToLines(inner) {
21557
21944
  let hadNonText = false;
21558
21945
  let work = inner;
21946
+ const imgSrcs = [];
21559
21947
  if (/<table[\s>]/i.test(work)) {
21560
21948
  hadNonText = true;
21561
21949
  work = removeNestedTables(work);
21562
21950
  }
21563
21951
  if (/<img\s/i.test(work)) {
21564
21952
  hadNonText = true;
21565
- work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, "");
21953
+ work = work.replace(/<img\s(?:"[^"]*"|'[^']*'|[^>"'])*?>/gi, (tag) => {
21954
+ const m = /\bsrc\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(tag);
21955
+ const src = m?.[1] ?? m?.[2];
21956
+ if (src) imgSrcs.push(src);
21957
+ return "";
21958
+ });
21566
21959
  }
21567
21960
  const lines = work.split(/<br\s*\/?>/gi).map((s) => s.trim()).filter((s) => s.length > 0);
21568
- return { lines, hadNonText };
21961
+ return { lines, hadNonText, imgSrcs };
21569
21962
  }
21570
21963
  function extractTopLevelTables(html) {
21571
21964
  const result = [];
@@ -21660,13 +22053,19 @@ function dataCellSpec(opts) {
21660
22053
  function normalizeAnchor(s) {
21661
22054
  return s.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "").slice(0, 24);
21662
22055
  }
21663
- function takeProfile(remap, rows, cols, anchor, seq) {
22056
+ function normalizeRowAnchor(cells) {
22057
+ const j = cells.map(normalizeAnchor).join("|");
22058
+ return /[\p{L}\p{N}]/u.test(j) ? j.slice(0, 64) : "";
22059
+ }
22060
+ function takeProfile(remap, rows, cols, anchor, seq, rowAnchor = "") {
21664
22061
  if (!remap) return null;
21665
22062
  for (const t of remap.tables) {
21666
22063
  if (t.used) continue;
21667
22064
  if (t.rows !== rows || t.cols !== cols) continue;
21668
22065
  if (t.anchor && anchor) {
21669
22066
  if (t.anchor !== anchor) continue;
22067
+ } else if (t.anchorRow && rowAnchor) {
22068
+ if (t.anchorRow !== rowAnchor) continue;
21670
22069
  } else if (t.index !== seq) {
21671
22070
  continue;
21672
22071
  }
@@ -21681,10 +22080,10 @@ function parseHu(s) {
21681
22080
  return Number.isFinite(n) ? n : void 0;
21682
22081
  }
21683
22082
  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"/>`;
22083
+ 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
22084
  }
21686
22085
  function borderFillDefToXml(id, def) {
21687
- const fill = def.fill?.faceColor ? `<hh:fillBrush><hh:winBrush faceColor="${def.fill.faceColor}" hatchColor="#000000" alpha="0"/></hh:fillBrush>` : "";
22086
+ const fill = def.fill?.faceColor ? `<hh:fillBrush><hh:winBrush faceColor="${escapeXml(def.fill.faceColor)}" hatchColor="#000000" alpha="0"/></hh:fillBrush>` : "";
21688
22087
  return ` <hh:borderFill id="${id}" threeD="0" shadow="0" centerLine="NONE" breakCellSeparateLine="0">
21689
22088
  <hh:slash type="NONE" Crooked="0" isCounter="0"/>
21690
22089
  <hh:backSlash type="NONE" Crooked="0" isCounter="0"/>
@@ -21695,28 +22094,45 @@ function borderFillDefToXml(id, def) {
21695
22094
  ${fill}` : ""}
21696
22095
  </hh:borderFill>`;
21697
22096
  }
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;
22097
+ var LEGACY_PROFILE_FONT_MAX = 2;
22098
+ function profileCharPrXml(id, def, namedFontId) {
22099
+ const height = Math.max(parseHu(def.height_hwpunit) ?? 1e3, 100);
22100
+ const color = escapeXml(def.textColor ?? "#000000");
22101
+ let font = 0;
22102
+ let restFont = 0;
22103
+ if (namedFontId != null) {
22104
+ font = namedFontId;
22105
+ } else {
22106
+ const rawFont = def.fontRef_hangul != null ? parseInt(def.fontRef_hangul, 10) || 0 : 0;
22107
+ font = rawFont >= 0 && rawFont <= LEGACY_PROFILE_FONT_MAX ? rawFont : 0;
22108
+ restFont = font;
22109
+ }
21704
22110
  const boldAttr = def.bold ? ` bold="1"` : "";
21705
22111
  const italicAttr = def.italic ? ` italic="1"` : "";
21706
22112
  const underline = def.underline ? `
21707
22113
  <hh:underline type="BOTTOM" shape="SOLID" color="${color}"/>` : "";
21708
22114
  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}"/>
22115
+ <hh:fontRef hangul="${font}" latin="${font}" hanja="${restFont}" japanese="${restFont}" other="${restFont}" symbol="${restFont}" user="${restFont}"/>
21710
22116
  <hh:ratio hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
21711
22117
  <hh:spacing hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>
21712
22118
  <hh:relSz hangul="100" latin="100" hanja="100" japanese="100" other="100" symbol="100" user="100"/>
21713
22119
  <hh:offset hangul="0" latin="0" hanja="0" japanese="0" other="0" symbol="0" user="0"/>${underline}
21714
22120
  </hh:charPr>`;
21715
22121
  }
21716
- function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21717
- const remap = { borderFillXmls: [], charPrXmls: [], tables: [] };
22122
+ function buildProfileRemap(profile, charPrBase, borderFillBase = 3, fontBase = 3) {
22123
+ const remap = { borderFillXmls: [], charPrXmls: [], fontFaces: [], tables: [] };
21718
22124
  let bfNext = borderFillBase;
21719
22125
  let charNext = charPrBase;
22126
+ const fontIds = /* @__PURE__ */ new Map();
22127
+ const fontIdOf = (name) => {
22128
+ let id = fontIds.get(name);
22129
+ if (id == null) {
22130
+ id = fontBase + remap.fontFaces.length;
22131
+ remap.fontFaces.push(name);
22132
+ fontIds.set(name, id);
22133
+ }
22134
+ return id;
22135
+ };
21720
22136
  for (const t of profile.tables) {
21721
22137
  const localBf = {};
21722
22138
  for (const [key, def] of Object.entries(t.used_border_fills ?? {})) {
@@ -21727,7 +22143,7 @@ function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21727
22143
  const localChar = {};
21728
22144
  for (const [key, def] of Object.entries(t.used_char_prs ?? {})) {
21729
22145
  const gid = charNext++;
21730
- remap.charPrXmls.push(profileCharPrXml(gid, def));
22146
+ remap.charPrXmls.push(profileCharPrXml(gid, def, def.fontName_hangul ? fontIdOf(def.fontName_hangul) : void 0));
21731
22147
  localChar[key] = gid;
21732
22148
  }
21733
22149
  let colWidths;
@@ -21741,6 +22157,7 @@ function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21741
22157
  cols: t.cols,
21742
22158
  // 재정규화 — 추출기는 정규화해 담지만 손편집된 프로필 JSON도 같은 키 공간으로
21743
22159
  anchor: t.anchor_text ? normalizeAnchor(t.anchor_text) : void 0,
22160
+ anchorRow: t.anchor_row ? normalizeRowAnchor(t.anchor_row.split("|")) || void 0 : void 0,
21744
22161
  width: parseHu(t.width_hwpunit),
21745
22162
  colWidths,
21746
22163
  cellBf: /* @__PURE__ */ new Map(),
@@ -21758,17 +22175,208 @@ function buildProfileRemap(profile, charPrBase, borderFillBase = 3) {
21758
22175
  const h = parseHu(cell2.height_hwpunit);
21759
22176
  if (h != null) tr.cellH.set(k, h);
21760
22177
  }
21761
- remap.tables.push(tr);
22178
+ remap.tables.push(tr);
22179
+ }
22180
+ return remap;
22181
+ }
22182
+
22183
+ // src/hwpx/gen-image.ts
22184
+ var PLACEHOLDER_BMP = Uint8Array.from([
22185
+ 66,
22186
+ 77,
22187
+ 58,
22188
+ 0,
22189
+ 0,
22190
+ 0,
22191
+ 0,
22192
+ 0,
22193
+ 0,
22194
+ 0,
22195
+ 54,
22196
+ 0,
22197
+ 0,
22198
+ 0,
22199
+ 40,
22200
+ 0,
22201
+ 0,
22202
+ 0,
22203
+ 1,
22204
+ 0,
22205
+ 0,
22206
+ 0,
22207
+ 1,
22208
+ 0,
22209
+ 0,
22210
+ 0,
22211
+ 1,
22212
+ 0,
22213
+ 24,
22214
+ 0,
22215
+ 0,
22216
+ 0,
22217
+ 0,
22218
+ 0,
22219
+ 4,
22220
+ 0,
22221
+ 0,
22222
+ 0,
22223
+ 19,
22224
+ 11,
22225
+ 0,
22226
+ 0,
22227
+ 19,
22228
+ 11,
22229
+ 0,
22230
+ 0,
22231
+ 0,
22232
+ 0,
22233
+ 0,
22234
+ 0,
22235
+ 0,
22236
+ 0,
22237
+ 0,
22238
+ 0,
22239
+ 255,
22240
+ 255,
22241
+ 255,
22242
+ 0
22243
+ ]);
22244
+ var PLACEHOLDER_PNG = Uint8Array.from([
22245
+ 137,
22246
+ 80,
22247
+ 78,
22248
+ 71,
22249
+ 13,
22250
+ 10,
22251
+ 26,
22252
+ 10,
22253
+ 0,
22254
+ 0,
22255
+ 0,
22256
+ 13,
22257
+ 73,
22258
+ 72,
22259
+ 68,
22260
+ 82,
22261
+ 0,
22262
+ 0,
22263
+ 0,
22264
+ 1,
22265
+ 0,
22266
+ 0,
22267
+ 0,
22268
+ 1,
22269
+ 8,
22270
+ 6,
22271
+ 0,
22272
+ 0,
22273
+ 0,
22274
+ 31,
22275
+ 21,
22276
+ 196,
22277
+ 137,
22278
+ 0,
22279
+ 0,
22280
+ 0,
22281
+ 11,
22282
+ 73,
22283
+ 68,
22284
+ 65,
22285
+ 84,
22286
+ 120,
22287
+ 156,
22288
+ 99,
22289
+ 0,
22290
+ 1,
22291
+ 0,
22292
+ 0,
22293
+ 5,
22294
+ 0,
22295
+ 1,
22296
+ 13,
22297
+ 10,
22298
+ 45,
22299
+ 180,
22300
+ 0,
22301
+ 0,
22302
+ 0,
22303
+ 0,
22304
+ 73,
22305
+ 69,
22306
+ 78,
22307
+ 68,
22308
+ 174,
22309
+ 66,
22310
+ 96,
22311
+ 130
22312
+ ]);
22313
+ var IMAGE_EXT_MIME = {
22314
+ png: "image/png",
22315
+ jpg: "image/jpeg",
22316
+ jpeg: "image/jpeg",
22317
+ gif: "image/gif",
22318
+ bmp: "image/bmp",
22319
+ tif: "image/tiff",
22320
+ tiff: "image/tiff"
22321
+ };
22322
+ var ImageRegistry = class {
22323
+ byUrl = /* @__PURE__ */ new Map();
22324
+ ids = /* @__PURE__ */ new Set();
22325
+ parts = [];
22326
+ picSeq = 0;
22327
+ /** url 등록(중복 시 기존 파트) — 수용 불가면 null */
22328
+ take(url) {
22329
+ const cached = this.byUrl.get(url);
22330
+ if (cached !== void 0) return cached;
22331
+ let part = null;
22332
+ const m = /^([A-Za-z0-9._-]+)\.([A-Za-z0-9]+)$/.exec(url);
22333
+ const mime = m ? IMAGE_EXT_MIME[m[2].toLowerCase()] : void 0;
22334
+ if (m && mime && !url.includes("..")) {
22335
+ let itemId = m[1].replace(/[^A-Za-z0-9_]/g, "_");
22336
+ let n = 1;
22337
+ while (this.ids.has(itemId)) itemId = `${m[1].replace(/[^A-Za-z0-9_]/g, "_")}_${n++}`;
22338
+ this.ids.add(itemId);
22339
+ part = {
22340
+ name: `BinData/${url}`,
22341
+ itemId,
22342
+ mime,
22343
+ data: m[2].toLowerCase() === "bmp" ? PLACEHOLDER_BMP : PLACEHOLDER_PNG
22344
+ };
22345
+ this.parts.push(part);
22346
+ }
22347
+ this.byUrl.set(url, part);
22348
+ return part;
21762
22349
  }
21763
- return remap;
22350
+ /** manifest <opf:item> 조각들 (실측: isEmbeded="1") */
22351
+ manifestItems() {
22352
+ return this.parts.map((p) => `<opf:item id="${p.itemId}" href="${p.name}" media-type="${p.mime}" isEmbeded="1"/>`);
22353
+ }
22354
+ /** 인라인 <hp:pic> XML — 실측 저장본 미러, treatAsChar=1 (셀·문단 안 배치) */
22355
+ inlinePicXml(part) {
22356
+ const id = PIC_ID_BASE + ++this.picSeq;
22357
+ const s = 1130;
22358
+ 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>`;
22359
+ }
22360
+ };
22361
+ var MD_IMAGE_RE = /!\[[^\]]*\]\(([^)\s]+)\)/g;
22362
+ function splitImageRefs(text) {
22363
+ const urls = [];
22364
+ const out = text.replace(MD_IMAGE_RE, (_, url) => {
22365
+ urls.push(url);
22366
+ return "";
22367
+ });
22368
+ return { text: out, urls };
21764
22369
  }
21765
22370
 
21766
22371
  // src/hwpx/gen-table.ts
21767
- var TABLE_ID_BASE = 1e3;
22372
+ var TABLE_ID_BASE = DATA_TABLE_ID_BASE;
21768
22373
  var tableIdCounter = TABLE_ID_BASE;
21769
22374
  function nextTableId() {
21770
22375
  return ++tableIdCounter;
21771
22376
  }
22377
+ function resetTableIds() {
22378
+ tableIdCounter = TABLE_ID_BASE;
22379
+ }
21772
22380
  var DATA_TABLE_INSET = 1800;
21773
22381
  function anchorOfMarkdownCell(cell2) {
21774
22382
  return normalizeAnchor(cell2.replace(/!\[[^\]]*\]\([^)]*\)/g, ""));
@@ -21787,7 +22395,7 @@ function profileColWidths(tp, colCnt) {
21787
22395
  var CELL_PAD = 1200;
21788
22396
  function cellContentWidth(text, charHeight) {
21789
22397
  let max = 0;
21790
- for (const seg of text.replace(/\*\*|__|`/g, "").split(/<br\s*\/?>/i)) {
22398
+ for (const seg of text.replace(/!\[[^\]]*\]\([^)\s]+\)/g, "").replace(/\*\*|__|`/g, "").split(/<br\s*\/?>/i)) {
21791
22399
  const w = measureTextWidth(seg.trim(), charHeight, 100);
21792
22400
  if (w > max) max = w;
21793
22401
  }
@@ -21795,7 +22403,7 @@ function cellContentWidth(text, charHeight) {
21795
22403
  }
21796
22404
  function cellMinWordWidth(text, charHeight) {
21797
22405
  let max = 0;
21798
- for (const seg of text.replace(/\*\*|__|`/g, "").split(/<br\s*\/?>/i)) {
22406
+ for (const seg of text.replace(/!\[[^\]]*\]\([^)\s]+\)/g, "").replace(/\*\*|__|`/g, "").split(/<br\s*\/?>/i)) {
21799
22407
  for (const word of seg.trim().split(/\s+/)) {
21800
22408
  const w = measureTextWidth(word, charHeight, 100);
21801
22409
  if (w > max) max = w;
@@ -21825,7 +22433,7 @@ function computeColWidths(colMax, totalWidth, colMinWord = []) {
21825
22433
  const sumFreeRaw = [...free].reduce((a, i) => a + raw[i], 0);
21826
22434
  if (budget > sumFreeRaw * 1.6) {
21827
22435
  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));
22436
+ for (let i = 0; i < colCnt; i++) widths[i] = Math.min(cap, Math.round(raw[i]) + Math.floor(raw[i] / sumRaw * (totalWidth - sumRaw)));
21829
22437
  free.clear();
21830
22438
  } else {
21831
22439
  for (; ; ) {
@@ -21846,7 +22454,23 @@ function computeColWidths(colMax, totalWidth, colMinWord = []) {
21846
22454
  }
21847
22455
  let rem = totalWidth - widths.reduce((a, b) => a + b, 0);
21848
22456
  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]]++;
22457
+ for (let k = 0, skipped = 0; rem > 0; k = (k + 1) % colCnt, rem--) {
22458
+ while (widths[order[k]] >= cap && skipped < colCnt) {
22459
+ skipped++;
22460
+ k = (k + 1) % colCnt;
22461
+ }
22462
+ skipped = 0;
22463
+ widths[order[k]]++;
22464
+ }
22465
+ for (let k = 0, spins = 0; rem < 0; k = (k + 1) % colCnt, rem++) {
22466
+ while (widths[order[k]] <= 1 && spins < colCnt) {
22467
+ spins++;
22468
+ k = (k + 1) % colCnt;
22469
+ }
22470
+ if (widths[order[k]] <= 1) break;
22471
+ spins = 0;
22472
+ widths[order[k]]--;
22473
+ }
21850
22474
  return widths;
21851
22475
  }
21852
22476
  function estimateRowHeight(cells, widths, charHeight) {
@@ -21861,13 +22485,14 @@ function estimateRowHeight(cells, widths, charHeight) {
21861
22485
  });
21862
22486
  return maxLines * Math.round(charHeight * 1.6) + 282;
21863
22487
  }
21864
- function generateTable(rows, theme, style = null, remap = null, seq = 0) {
22488
+ function generateTable(rows, theme, style = null, remap = null, seq = 0, images = null) {
21865
22489
  const rowCnt = rows.length;
21866
22490
  const colCnt = Math.max(...rows.map((r) => r.length), 1);
21867
22491
  const reg = style?.bfRegistry ?? null;
21868
22492
  const totalW = style ? reg ? style.totalWidth - DATA_TABLE_INSET : style.totalWidth : 44e3;
21869
22493
  const measureH = style?.charHeight ?? 1e3;
21870
- const prof = takeProfile(remap, rowCnt, colCnt, anchorOfMarkdownCell(rows[0]?.[0] ?? ""), seq);
22494
+ const rowAnchor = normalizeRowAnchor((rows[0] ?? []).map((c) => c.replace(/!\[[^\]]*\]\([^)]*\)/g, "")));
22495
+ const prof = takeProfile(remap, rowCnt, colCnt, anchorOfMarkdownCell(rows[0]?.[0] ?? ""), seq, rowAnchor);
21871
22496
  const colMax = Array(colCnt).fill(0);
21872
22497
  const colMaxBody = Array(colCnt).fill(0);
21873
22498
  const colMinWord = Array(colCnt).fill(0);
@@ -21899,8 +22524,23 @@ function generateTable(rows, theme, style = null, remap = null, seq = 0) {
21899
22524
  const centered = isHeaderRow || colCentered[colIdx];
21900
22525
  const paraPrId = style ? centered ? style.tblCenterParaPr ?? style.centerParaPr : reg ? style.tblLeftParaPr ?? PARA_NORMAL : PARA_NORMAL : PARA_NORMAL;
21901
22526
  const p = cell2.split(/<br\s*\/?>/i).map((seg) => {
22527
+ let picRuns = "";
22528
+ if (images) {
22529
+ const { text: rest, urls } = splitImageRefs(seg);
22530
+ if (urls.length > 0) {
22531
+ const pics = urls.map((u) => {
22532
+ const part = images.take(u);
22533
+ return part ? images.inlinePicXml(part) : null;
22534
+ });
22535
+ if (pics.every(Boolean)) {
22536
+ seg = rest;
22537
+ picRuns = `<hp:run charPrIDRef="${cellCharPr}">${pics.join("")}</hp:run>`;
22538
+ }
22539
+ }
22540
+ }
21902
22541
  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>`;
22542
+ const body = (runs || (picRuns ? "" : `<hp:run charPrIDRef="${cellCharPr}"><hp:t></hp:t></hp:run>`)) + picRuns;
22543
+ return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0">${body}</hp:p>`;
21904
22544
  }).join("");
21905
22545
  const bf = prof?.cellBf.get(k) ?? (reg ? reg.get(dataCellSpec({
21906
22546
  row: rowIdx,
@@ -21942,23 +22582,37 @@ function layoutHtmlRows(rows) {
21942
22582
  colCnt = Math.max(colCnt, c);
21943
22583
  }
21944
22584
  }
22585
+ for (let r = 0; r < rows.length; r++) {
22586
+ for (let c = 0; c < colCnt; c++) {
22587
+ if (!occupied.has(`${r},${c}`)) {
22588
+ placed.push({ r, c, colSpan: 1, rowSpan: 1, inner: "", isHeader: rows[r].tag === "th" });
22589
+ occupied.add(`${r},${c}`);
22590
+ }
22591
+ }
22592
+ }
22593
+ placed.sort((a, b) => a.r - b.r || a.c - b.c);
21945
22594
  return { placed, rowCnt: rows.length, colCnt };
21946
22595
  }
21947
22596
  function unescapeHtml(s) {
21948
22597
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
21949
22598
  }
21950
- function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, remap = null, seq = 0) {
22599
+ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, remap = null, seq = 0, images = null) {
21951
22600
  const rows = parseHtmlTable(rawHtml);
21952
22601
  if (!rows || rows.length === 0) return null;
21953
22602
  const { placed, rowCnt, colCnt } = layoutHtmlRows(rows);
21954
22603
  if (rowCnt === 0 || colCnt === 0) return null;
21955
22604
  const measureH = style?.charHeight ?? 1e3;
21956
22605
  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);
22606
+ const row0 = Array.from({ length: colCnt }, () => "");
22607
+ for (const cell2 of placed) {
22608
+ if (cell2.r === 0 && cell2.c < colCnt) row0[cell2.c] = anchorOfHtmlCell(cell2.inner);
22609
+ }
22610
+ const prof = takeProfile(remap, rowCnt, colCnt, first ? anchorOfHtmlCell(first.inner) : "", seq, normalizeRowAnchor(row0));
21958
22611
  const colMax = Array(colCnt).fill(0);
21959
22612
  const colMaxBody = Array(colCnt).fill(0);
21960
22613
  const colMinWord = Array(colCnt).fill(0);
21961
- const cellLines = placed.map((cell2) => htmlCellInnerToLines(cell2.inner).lines);
22614
+ const cellParsed = placed.map((cell2) => htmlCellInnerToLines(cell2.inner));
22615
+ const cellLines = cellParsed.map((p) => p.lines);
21962
22616
  placed.forEach((cell2, i) => {
21963
22617
  const w = Math.max(...cellLines[i].map((l) => measureTextWidth(unescapeHtml(l).trim(), measureH, 100)), 0) / cell2.colSpan;
21964
22618
  const mw = cell2.colSpan === 1 ? Math.max(...cellLines[i].map((l) => cellMinWordWidth(unescapeHtml(l), measureH)), 0) : 0;
@@ -21980,7 +22634,32 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
21980
22634
  let htmlHeaderRows = 0;
21981
22635
  while (htmlHeaderRows < rows.length && rows[htmlHeaderRows].tag === "th") htmlHeaderRows++;
21982
22636
  const tableRowHeights = Array(rowCnt).fill(cellH);
21983
- const tcXmls = placed.map((cell2, i) => {
22637
+ const meta = placed.map((cell2, i) => {
22638
+ const rowSpan = Math.min(cell2.rowSpan, rowCnt - cell2.r);
22639
+ const lines = cellLines[i];
22640
+ let nestedH = 0;
22641
+ const nestedXmls = [];
22642
+ for (const nested of extractTopLevelTables(cell2.inner)) {
22643
+ const sw = spanW(cell2);
22644
+ const nestedW = Math.max(Math.min(Math.max(sw - 1020, 4e3), sw - 282), 500);
22645
+ const nestedXml = generateHtmlTableXml(nested, theme, nestedW, style ? { ...style, totalWidth: nestedW } : null);
22646
+ if (nestedXml) {
22647
+ nestedXmls.push(nestedXml);
22648
+ const szH = nestedXml.match(/<hp:sz [^>]*height="(\d+)"/)?.[1];
22649
+ nestedH += (szH ? Number(szH) : (nested.match(/<tr[\s>]/gi) ?? []).length * cellH) + 300;
22650
+ }
22651
+ }
22652
+ const usable = Math.max(spanW(cell2) - CELL_PAD, 1e3);
22653
+ let wrapLines = 0;
22654
+ for (const line of lines) wrapLines += Math.max(1, Math.ceil(measureTextWidth(unescapeHtml(line).trim(), measureH, 100) / usable));
22655
+ const lineH = style ? Math.round(measureH * 1.6) : 800;
22656
+ const contentH = Math.max(cellH * rowSpan, Math.max(wrapLines, 1) * lineH + nestedH);
22657
+ const cellHeight = Math.max(prof?.cellH.get(`${cell2.r},${cell2.c}`) ?? 0, contentH);
22658
+ const perRow = Math.ceil(cellHeight / rowSpan);
22659
+ for (let r = cell2.r; r < cell2.r + rowSpan; r++) tableRowHeights[r] = Math.max(tableRowHeights[r], perRow);
22660
+ return { cell: cell2, rowSpan, lines, nestedXmls, imgSrcs: cellParsed[i].imgSrcs };
22661
+ });
22662
+ const tcXmls = meta.map(({ cell: cell2, rowSpan, lines, nestedXmls, imgSrcs }) => {
21984
22663
  const k = `${cell2.r},${cell2.c}`;
21985
22664
  const isHeader = cell2.isHeader;
21986
22665
  const baseCharPr = style ? style.charPr : CHAR_NORMAL;
@@ -21989,31 +22668,42 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
21989
22668
  const charPrId = prof?.cellChar.get(k) ?? defaultCharPr;
21990
22669
  const centered = isHeader || colCentered[cell2.c];
21991
22670
  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;
22671
+ const picUrls = images ? [...imgSrcs] : [];
22672
+ const paras = [];
22673
+ for (const line of lines) {
22674
+ let text = unescapeHtml(line);
22675
+ if (images) {
22676
+ const { text: rest, urls } = splitImageRefs(text);
22677
+ if (urls.length > 0) {
22678
+ picUrls.push(...urls);
22679
+ text = rest;
22680
+ }
22681
+ }
22682
+ if (!images || text.trim() || picUrls.length === 0) {
22683
+ paras.push(`<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`);
22684
+ }
22685
+ }
22686
+ if (images && picUrls.length > 0) {
22687
+ const pics = picUrls.map((u) => {
22688
+ const part = images.take(u);
22689
+ return part ? images.inlinePicXml(part) : null;
22690
+ }).filter(Boolean);
22691
+ if (pics.length > 0) {
22692
+ paras.push(`<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}">${pics.join("")}</hp:run></hp:p>`);
22003
22693
  }
22004
22694
  }
22695
+ for (const nestedXml of nestedXmls) {
22696
+ paras.push(`<hp:p paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0">${nestedXml}</hp:run></hp:p>`);
22697
+ }
22005
22698
  if (paras.length === 0) {
22006
22699
  paras.push(`<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t></hp:t></hp:run></hp:p>`);
22007
22700
  }
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
- }
22701
+ let cellHeight = 0;
22702
+ for (let r = cell2.r; r < cell2.r + rowSpan; r++) cellHeight += tableRowHeights[r];
22703
+ const rowEnd = cell2.r + rowSpan - 1;
22014
22704
  const bf = prof?.cellBf.get(k) ?? (reg ? reg.get(dataCellSpec({
22015
22705
  row: cell2.r,
22016
- rowEnd: cell2.r + cell2.rowSpan - 1,
22706
+ rowEnd,
22017
22707
  col: cell2.c,
22018
22708
  colEnd: cell2.c + cell2.colSpan - 1,
22019
22709
  rowCnt,
@@ -22021,7 +22711,7 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
22021
22711
  headerRows: htmlHeaderRows,
22022
22712
  fill: isHeader ? style.headerFill ?? "#E6E6E6" : void 0
22023
22713
  })) : 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>`;
22714
+ 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
22715
  });
22026
22716
  const trXmls = [];
22027
22717
  for (let r = 0; r < rowCnt; r++) {
@@ -22033,12 +22723,15 @@ function generateHtmlTableXml(rawHtml, theme, totalWidth = 44e3, style = null, r
22033
22723
  }
22034
22724
 
22035
22725
  // src/hwpx/gen-gongmun-extra.ts
22036
- var extraTableId = 93e5;
22726
+ var extraTableId = EXTRA_TABLE_ID_BASE;
22727
+ function resetExtraTableIds() {
22728
+ extraTableId = EXTRA_TABLE_ID_BASE;
22729
+ }
22037
22730
  function tbl(rows, w, h, cols) {
22038
22731
  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
22732
  }
22040
22733
  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>`;
22734
+ 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
22735
  }
22043
22736
  function para(text, paraPrId, charPrId) {
22044
22737
  return `<hp:p paraPrIDRef="${paraPrId}" styleIDRef="0"><hp:run charPrIDRef="${charPrId}"><hp:t>${escapeXml(text)}</hp:t></hp:run></hp:p>`;
@@ -22065,7 +22758,7 @@ function buildApprovalTable(labels, reg, approvalCharPr) {
22065
22758
  return `<hp:p paraPrIDRef="${GONGMUN_RIGHT}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${table2}</hp:run></hp:p>`;
22066
22759
  }
22067
22760
  function hasEndMark(lastText) {
22068
- return /끝\s*\.?\s*$/.test(lastText.trim());
22761
+ return /(^|\s)끝\s*\.\s*$/.test(lastText.trim());
22069
22762
  }
22070
22763
  function buildEndMark() {
22071
22764
  return para(" \uB05D.", PARA_NORMAL, CHAR_NORMAL);
@@ -22080,7 +22773,7 @@ function buildTitleBox(title, titleCharPr, barCharPr, bodyWidth, reg) {
22080
22773
  const barPara = para("", GJ_PARA_BAR, barCharPr);
22081
22774
  const rows = [
22082
22775
  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) }),
22776
+ tc({ bf: mid, row: 1, col: 0, w, h: TITLE_BOX.titleH, paras: para(title, GONGMUN_TBL_CENTER, titleCharPr), name: "__kordoc_h1" }),
22084
22777
  tc({ bf: barBottom, row: 2, col: 0, w, h: TITLE_BOX.barH, paras: barPara })
22085
22778
  ];
22086
22779
  const table2 = tbl(rows, w, TITLE_BOX.barH * 2 + TITLE_BOX.titleH, 1);
@@ -22127,7 +22820,10 @@ function buildDocHead(g, ids) {
22127
22820
  out.push(blank());
22128
22821
  return out;
22129
22822
  }
22130
- var FOOT_SEP = "\u2500".repeat(46);
22823
+ function footSep(g) {
22824
+ const bodyMm = 210 - g.margins.left - g.margins.right;
22825
+ return "\u2500".repeat(Math.max(10, Math.round(bodyMm * (46 / 175))));
22826
+ }
22131
22827
  function buildDocFoot(g, ids) {
22132
22828
  const f = g.docFoot;
22133
22829
  const out = [blank()];
@@ -22135,7 +22831,7 @@ function buildDocFoot(g, ids) {
22135
22831
  out.push(para(f.sender, GONGMUN_CENTER, ids.sender), blank());
22136
22832
  }
22137
22833
  const foot = (text) => para(text, GONGMUN_TBL_LEFT, ids.foot);
22138
- out.push(foot(FOOT_SEP));
22834
+ out.push(foot(footSep(g)));
22139
22835
  if (f.drafter || f.reviewer || f.approver)
22140
22836
  out.push(foot(`\uAE30\uC548\uC790 ${f.drafter ?? ""} \uAC80\uD1A0\uC790 ${f.reviewer ?? ""} \uACB0\uC7AC\uAD8C\uC790 ${f.approver ?? ""}`));
22141
22837
  if (f.cooperator) out.push(foot(`\uD611\uC870\uC790 ${f.cooperator}`));
@@ -22284,6 +22980,54 @@ var COMMAND_MAP = {
22284
22980
  vdots: "VDOTS",
22285
22981
  ddots: "DDOTS"
22286
22982
  };
22983
+ var EQEDIT_FUNCTIONS = /* @__PURE__ */ new Set([
22984
+ "sin",
22985
+ "cos",
22986
+ "tan",
22987
+ "cot",
22988
+ "sec",
22989
+ "csc",
22990
+ "arcsin",
22991
+ "arccos",
22992
+ "arctan",
22993
+ "sinh",
22994
+ "cosh",
22995
+ "tanh",
22996
+ "coth",
22997
+ "log",
22998
+ "ln",
22999
+ "exp",
23000
+ "det",
23001
+ "gcd",
23002
+ "mod",
23003
+ "max",
23004
+ "min",
23005
+ "arg",
23006
+ "deg",
23007
+ "hom",
23008
+ "ker",
23009
+ "Pr"
23010
+ ]);
23011
+ {
23012
+ const reverse = /* @__PURE__ */ new Map();
23013
+ const prefer = (name, a, b) => {
23014
+ if (a === name) return a;
23015
+ if (b === name) return b;
23016
+ const aAlpha = /^[A-Za-z]+$/.test(a);
23017
+ const bAlpha = /^[A-Za-z]+$/.test(b);
23018
+ if (aAlpha !== bAlpha) return aAlpha ? a : b;
23019
+ return a < b ? a : b;
23020
+ };
23021
+ for (const [token, latex] of Object.entries(CONVERT_MAP)) {
23022
+ const m = /^\\([A-Za-z]+)$/.exec(latex);
23023
+ if (!m) continue;
23024
+ const name = m[1];
23025
+ if (name in COMMAND_MAP) continue;
23026
+ const prev = reverse.get(name);
23027
+ reverse.set(name, prev === void 0 ? token : prefer(name, token, prev));
23028
+ }
23029
+ for (const [name, token] of reverse) COMMAND_MAP[name] = token;
23030
+ }
22287
23031
  var ACCENT_COMMANDS = {
22288
23032
  bar: "bar",
22289
23033
  overline: "bar",
@@ -22377,8 +23121,20 @@ function readCommand(input, idx, depth) {
22377
23121
  const endIdx = input.indexOf(endTag, env.next);
22378
23122
  if (endIdx === -1) return { value: env.value, next: env.next };
22379
23123
  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 };
23124
+ const ENV_TOKEN = {
23125
+ matrix: "matrix",
23126
+ pmatrix: "pmatrix",
23127
+ bmatrix: "bmatrix",
23128
+ vmatrix: "dmatrix",
23129
+ cases: "cases",
23130
+ align: "eqalign",
23131
+ "align*": "eqalign",
23132
+ aligned: "eqalign"
23133
+ };
23134
+ const tok = ENV_TOKEN[env.value];
23135
+ if (tok) return { value: `{${tok}{${body}}}`, next: endIdx + endTag.length };
23136
+ if (env.value === "Bmatrix") {
23137
+ return { value: `LEFT { {matrix{${body}}} RIGHT }`, next: endIdx + endTag.length };
22382
23138
  }
22383
23139
  return { value: body, next: endIdx + endTag.length };
22384
23140
  }
@@ -22410,7 +23166,11 @@ function readCommand(input, idx, depth) {
22410
23166
  const tok = readGroupOrToken(input, start, depth);
22411
23167
  return { value: `"${tok.value}"`, next: tok.next };
22412
23168
  }
22413
- return { value: COMMAND_MAP[command] ?? command, next: name.next };
23169
+ const mapped = COMMAND_MAP[command];
23170
+ if (mapped !== void 0) return { value: mapped, next: name.next };
23171
+ if (EQEDIT_FUNCTIONS.has(command)) return { value: command, next: name.next };
23172
+ if (/^[A-Za-z]{2,}$/.test(command)) return { value: `"${command}"`, next: name.next };
23173
+ return { value: command, next: name.next };
22414
23174
  }
22415
23175
  function convertLatexFragment(input, depth) {
22416
23176
  if (depth > MAX_GROUP_DEPTH) return normalizeEqEdit(input);
@@ -22490,44 +23250,47 @@ function generateSecPr(gongmun) {
22490
23250
  m.header = gongmun.headerFooter;
22491
23251
  m.footer = gongmun.headerFooter;
22492
23252
  }
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>`;
23253
+ 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
23254
  const colPr = `<hp:ctrl><hp:colPr id="" type="NEWSPAPER" layout="LEFT" colCount="1" sameSz="1" sameGap="0"/></hp:ctrl>`;
22495
23255
  return secPr + colPr + (gongmun?.pageNumbers ? pageNumCtrl() : "");
22496
23256
  }
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;
23257
+ var SectionOpener = class {
23258
+ constructor(gongmun) {
23259
+ this.gongmun = gongmun;
23260
+ }
23261
+ pending = true;
23262
+ /** 아직 secPr를 나를 첫 문단이 안 나왔는가 */
23263
+ get isFirst() {
23264
+ return this.pending;
23265
+ }
23266
+ /** 일반 문단 XML의 첫 run에 secPr 주입 (소비) */
23267
+ inject(xml) {
23268
+ this.pending = false;
23269
+ return xml.replace(
23270
+ /<hp:run charPrIDRef="(\d+)">/,
23271
+ `<hp:run charPrIDRef="$1">${generateSecPr(this.gongmun)}`
23272
+ );
23273
+ }
23274
+ /**
23275
+ * 표·차트·수식처럼 첫 run에 직접 못 싣는 블록 앞에 secPr 전용 빈 문단을 방출 (소비).
23276
+ * 이미 소비됐으면 아무것도 안 한다.
23277
+ */
23278
+ emitCarrier(paraXmls) {
23279
+ if (!this.pending) return;
23280
+ this.pending = false;
23281
+ const secRun = `<hp:run charPrIDRef="0">${generateSecPr(this.gongmun)}<hp:t></hp:t></hp:run>`;
23282
+ paraXmls.push(`<hp:p paraPrIDRef="0" styleIDRef="0">${secRun}</hp:p>`);
23283
+ }
23284
+ /** 문서 폴백 — secPr를 실은 빈 단락 XML */
23285
+ emptyDoc() {
23286
+ this.pending = false;
23287
+ return `<hp:p paraPrIDRef="0" styleIDRef="0"><hp:run charPrIDRef="0">${generateSecPr(this.gongmun)}<hp:t></hp:t></hp:run></hp:p>`;
23288
+ }
23289
+ };
23290
+ function buildPreamble(blocks, ctx) {
23291
+ const { gongmun, dfIds, bfReg, tableStyle, richAssets, gjBodyW } = ctx;
22530
23292
  const preamble = [];
23293
+ let hasFrontPages = false;
22531
23294
  if (gongmun?.reportInfo && dfIds) {
22532
23295
  preamble.push(buildReportInfo(gongmun, dfIds));
22533
23296
  }
@@ -22545,278 +23308,340 @@ function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? prec
22545
23308
  }
22546
23309
  if (gongmun && (gongmun.cover || gongmun.toc)) {
22547
23310
  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
23311
  let coverTitle = "";
22550
23312
  const hide = (xml, hideHeader) => gongmun.pageNumbers ? xml.replace(/<hp:run charPrIDRef="(\d+)">/, `<hp:run charPrIDRef="$1">${pageHidingCtrl(hideHeader)}`) : xml;
22551
23313
  if (gongmun.cover && h1Idx >= 0) {
22552
23314
  coverTitle = (blocks[h1Idx].text || "").trim();
22553
- const coverPart = buildGaejosikCover(coverTitle, gongmun);
23315
+ const coverPart = buildGaejosikCover(coverTitle, gongmun, gjBodyW);
22554
23316
  coverPart[0] = hide(coverPart[0], true);
22555
23317
  preamble.push(...coverPart);
22556
- coverH1Idx = h1Idx;
23318
+ ctx.coverH1Idx = h1Idx;
22557
23319
  hasFrontPages = true;
22558
23320
  }
22559
- if (gongmun.toc && chapters.length > 0) {
22560
- const tocPart = buildGaejosikToc(chapters, gongmun);
23321
+ ctx.gjChapterIdxs = blocks.map((b, i) => b.type === "heading" && (b.level ?? 1) <= 2 && i !== ctx.coverH1Idx ? i : -1).filter((i) => i >= 0);
23322
+ if (gongmun.toc && ctx.gjChapterIdxs.length > 0) {
23323
+ const chapters = ctx.gjChapterIdxs.map((i) => stripChapterNumber(blocks[i].text || ""));
23324
+ const tocPart = buildGaejosikToc(chapters, gongmun, gjBodyW);
22561
23325
  tocPart[0] = hide(tocPart[0], false);
22562
23326
  preamble.push(...tocPart);
22563
23327
  hasFrontPages = true;
22564
23328
  }
22565
23329
  if (gongmun.bodyTitleBox && coverTitle && hasFrontPages) {
22566
- preamble.push(buildGaejosikBodyTitle(coverTitle, gongmun).replace(/^<hp:p /, `<hp:p pageBreak="1" `));
23330
+ preamble.push(buildGaejosikBodyTitle(coverTitle, gongmun, gjBodyW).replace(/^<hp:p /, `<hp:p pageBreak="1" `));
22567
23331
  }
22568
23332
  }
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);
23333
+ if (gongmun && !ctx.gaejosik && ctx.coverH1Idx < 0 && bfReg && (gongmun.preset === "report" || gongmun.preset === "plan" || gongmun.preset === "notice")) {
23334
+ ctx.titleBoxH1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
23335
+ } else if (gongmun?.press && ctx.coverH1Idx < 0 && dfIds) {
23336
+ ctx.pressH1Idx = blocks.findIndex((b) => b.type === "heading" && (b.level ?? 1) === 1);
22573
23337
  }
22574
23338
  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);
23339
+ preamble[0] = ctx.opener.inject(preamble[0].replace(` pageBreak="1"`, ""));
23340
+ ctx.pendingPageBreak = hasFrontPages && !preamble[preamble.length - 1].includes(`pageBreak="1"`);
23341
+ ctx.pendingNewNum = !!gongmun.pageNumbers && hasFrontPages;
23342
+ ctx.paraXmls.push(...preamble);
23343
+ }
23344
+ }
23345
+ function renderHeading(block, blockIdx, ctx) {
23346
+ const { gongmun, dfIds, tableStyle, bfReg, measured, richAssets, gjBodyW } = ctx;
23347
+ if (gongmun && blockIdx === ctx.coverH1Idx) return "";
23348
+ if (gongmun && blockIdx === ctx.pressH1Idx && dfIds) {
23349
+ const parts = [generateParagraph((block.text || "").trim(), GONGMUN_CENTER, dfIds.pressTitle)];
23350
+ for (const s of gongmun.press?.sub ?? []) {
23351
+ parts.push(generateParagraph(`- ${s} -`, GONGMUN_CENTER, dfIds.pressSub));
23352
+ }
23353
+ return parts.join("\n ");
23354
+ }
23355
+ if (gongmun && blockIdx === ctx.titleBoxH1Idx && tableStyle && bfReg) {
23356
+ return buildTitleBox(
23357
+ (block.text || "").trim(),
23358
+ measured ? GJ_CHAR_BODY_TITLE : CHAR_H1,
23359
+ richAssets ? GJ_CHAR_TITLE_BAR : GONGMUN_TITLE_BAR_CHAR,
23360
+ tableStyle.totalWidth,
23361
+ bfReg
23362
+ );
23363
+ }
23364
+ if (ctx.gaejosik) {
23365
+ const lvl = block.level || 1;
23366
+ if (lvl <= 2) {
23367
+ const no = ctx.gjChapterIdxs.indexOf(blockIdx);
23368
+ ctx.chapterNo = no >= 0 ? no + 1 : ctx.chapterNo + 1;
23369
+ return buildGaejosikChapter(ctx.chapterNo, stripChapterNumber(block.text || ""), gongmun, gjBodyW, lvl);
23370
+ } else if (lvl === 3) {
23371
+ return generateParagraph(
23372
+ `\u25A1 ${block.text || ""}`,
23373
+ GONGMUN_LIST_BASE,
23374
+ GJ_CHAR_DAE,
23375
+ (id) => id === CHAR_BOLD ? GJ_CHAR_DAE_BOLD : id
23376
+ );
23377
+ }
23378
+ return generateParagraph(`${gongmun.bullet2} ${block.text || ""}`, GONGMUN_LIST_BASE + 1, CHAR_NORMAL);
23379
+ }
23380
+ const pId = headingParaPrId(block.level || 1);
23381
+ const cId = headingCharPrId(block.level || 1);
23382
+ const styleId = gongmun ? Math.min(block.level || 1, 4) : 0;
23383
+ let hText = block.text || "";
23384
+ if (gongmun && (block.level || 1) === 2 && gongmun.h2Marker !== "none") {
23385
+ const title = stripChapterNumber(hText);
23386
+ hText = gongmun.h2Marker === "box" ? `\u25A1 ${title}` : `${++ctx.h2Seq}. ${title}`;
23387
+ }
23388
+ return generateParagraph(hText, pId, cId, void 0, styleId);
23389
+ }
23390
+ function renderParagraph(block, blockIdx, ctx) {
23391
+ const { gongmun, measured, fit, vBase } = ctx;
23392
+ if (ctx.images) {
23393
+ const { text: rest, urls } = splitImageRefs(block.text || "");
23394
+ if (urls.length > 0 && !rest.trim()) {
23395
+ const pics = urls.map((u) => {
23396
+ const part = ctx.images.take(u);
23397
+ return part ? ctx.images.inlinePicXml(part) : null;
23398
+ });
23399
+ if (pics.every(Boolean)) {
23400
+ return `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${pics.join("")}</hp:run></hp:p>`;
23401
+ }
23402
+ }
23403
+ }
23404
+ const pTrim = (block.text || "").trimStart();
23405
+ if (measured && (pTrim.startsWith("\u203B") || /^\*\s/.test(pTrim))) {
23406
+ return generateParagraph((block.text || "").trim(), GJ_PARA_CHAM, GJ_CHAR_CHAM, ctx.chamMap);
23407
+ }
23408
+ const ctr = gongmun && /^<center>([\s\S]*)<\/center>$/i.exec((block.text || "").trim());
23409
+ const rgt = gongmun && /^<right>([\s\S]*)<\/right>$/i.exec((block.text || "").trim());
23410
+ if (ctr) return generateParagraph(ctr[1].trim(), GONGMUN_CENTER);
23411
+ if (rgt) return generateParagraph(rgt[1].trim(), GONGMUN_RIGHT);
23412
+ return generateParagraph(block.text || "", PARA_NORMAL, CHAR_NORMAL, fit ? variantMapper(fit, blockIdx, vBase) : void 0);
23413
+ }
23414
+ function renderCodeBlock(block, blockIdx, ctx) {
23415
+ if (ctx.chartParts !== null && (block.lang || "").toLowerCase() === "chart") {
23416
+ const fence = parseChartFence(block.text || "");
23417
+ if (fence) {
23418
+ const partName = `Chart/chart${ctx.chartParts.length + 1}.xml`;
23419
+ ctx.chartParts.push({ name: partName, xml: buildChartSpaceXml(fence) });
23420
+ const chartEl = buildChartElementXml(partName, fence.widthHu, fence.heightHu, CHART_TABLE_ID_BASE + blockIdx);
23421
+ ctx.opener.emitCarrier(ctx.paraXmls);
23422
+ return `<hp:p paraPrIDRef="${PARA_NORMAL}" styleIDRef="0"><hp:run charPrIDRef="${CHAR_NORMAL}">${chartEl}</hp:run></hp:p>`;
23423
+ }
23424
+ }
23425
+ const codeLines = (block.text || "").split("\n");
23426
+ return codeLines.map((line) => generateParagraph(line || " ", PARA_CODE)).join("\n ");
23427
+ }
23428
+ function renderEquation(block, blockIdx, ctx) {
23429
+ ctx.opener.emitCarrier(ctx.paraXmls);
23430
+ return generateEquationParagraph(block.text || "", blockIdx);
23431
+ }
23432
+ function renderBlockquote(block, ctx) {
23433
+ if (ctx.measured) {
23434
+ const t = (block.text || "").replace(/\n+/g, " ").trim();
23435
+ return t ? generateParagraph(t.startsWith("\u203B") ? t : `\u203B ${t}`, GJ_PARA_CHAM, GJ_CHAR_CHAM, ctx.chamMap) : "";
23436
+ }
23437
+ const quoteChar = ctx.theme.hasQuoteOption ? CHAR_QUOTE : CHAR_NORMAL;
23438
+ return (block.text || "").split("\n").map((line) => generateParagraph(line, PARA_QUOTE, quoteChar)).join("\n ");
23439
+ }
23440
+ function renderListItem(block, blockIdx, ctx) {
23441
+ const { gongmun, gongmunList, measured, fit, vBase, dfIds } = ctx;
23442
+ if (gongmun && gongmunList) {
23443
+ const info = gongmunList.items.get(blockIdx);
23444
+ const depth = info?.depth ?? 0;
23445
+ const marker2 = info?.marker ?? "";
23446
+ const content = block.text || "";
23447
+ if (measured && content.trimStart().startsWith("\u203B")) {
23448
+ return generateParagraph(content.trim(), GJ_PARA_CHAM, GJ_CHAR_CHAM, ctx.chamMap);
23449
+ }
23450
+ if (measured && block.marker === "*") {
23451
+ return generateParagraph(`* ${content.trim()}`, GJ_PARA_CHAM, GJ_CHAR_CHAM, ctx.chamMap);
23452
+ }
23453
+ const text = marker2 ? `${marker2} ${content}` : content;
23454
+ const listParaPr = marker2 ? info?.indentVariant !== void 0 ? GONGMUN_LIST_VARIANT_BASE + info.indentVariant : GONGMUN_LIST_BASE + depth : GONGMUN_LIST_PLAIN_BASE + depth;
23455
+ let listCharPr = gongmun.numbering === "report" && depth === 0 ? CHAR_BOLD : CHAR_NORMAL;
23456
+ let mapId = fit ? variantMapper(fit, blockIdx, vBase) : void 0;
23457
+ if (measured && depth === 0 && gongmun.numbering !== "standard") {
23458
+ listCharPr = GJ_CHAR_DAE;
23459
+ mapId = (id) => id === CHAR_BOLD ? GJ_CHAR_DAE_BOLD : id;
23460
+ }
23461
+ if (gongmun.preset === "press") {
23462
+ if (depth === 0) listCharPr = CHAR_NORMAL;
23463
+ if (depth >= 2 && dfIds) {
23464
+ listCharPr = dfIds.small;
23465
+ mapId = void 0;
23466
+ }
23467
+ }
23468
+ return generateParagraph(text, listParaPr, listCharPr, mapId);
23469
+ }
23470
+ const indent = block.indent || 0;
23471
+ let marker;
23472
+ if (block.marker) {
23473
+ marker = `${block.marker} `;
23474
+ ctx.prevWasOrdered = !!block.ordered;
23475
+ } else if (block.ordered) {
23476
+ ctx.orderedCounters[indent] = (ctx.orderedCounters[indent] || 0) + 1;
23477
+ for (const k of Object.keys(ctx.orderedCounters)) {
23478
+ if (+k > indent) delete ctx.orderedCounters[+k];
23479
+ }
23480
+ marker = `${ctx.orderedCounters[indent]}. `;
23481
+ ctx.prevWasOrdered = true;
23482
+ } else {
23483
+ marker = "\xB7 ";
23484
+ if (ctx.prevWasOrdered) {
23485
+ for (const k of Object.keys(ctx.orderedCounters)) delete ctx.orderedCounters[+k];
23486
+ }
23487
+ ctx.prevWasOrdered = false;
23488
+ }
23489
+ const indentPrefix = " ".repeat(indent);
23490
+ return generateParagraph(indentPrefix + marker + (block.text || ""), PARA_LIST);
23491
+ }
23492
+ function renderHr(ctx) {
23493
+ 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>`;
23494
+ }
23495
+ function renderTable(block, ctx) {
23496
+ if (!block.rows) return "";
23497
+ ctx.opener.emitCarrier(ctx.paraXmls);
23498
+ return generateTable(block.rows, ctx.theme, ctx.tableStyle, ctx.remap, ctx.tableSeq++, ctx.images);
23499
+ }
23500
+ function renderHtmlTable(block, ctx) {
23501
+ const { tableStyle, bfReg } = ctx;
23502
+ const htmlW = tableStyle ? bfReg ? tableStyle.totalWidth - DATA_TABLE_INSET : tableStyle.totalWidth : 44e3;
23503
+ const tbl2 = generateHtmlTableXml(block.text || "", ctx.theme, htmlW, tableStyle, ctx.remap, ctx.tableSeq++, ctx.images);
23504
+ if (tbl2) {
23505
+ ctx.opener.emitCarrier(ctx.paraXmls);
23506
+ return `<hp:p paraPrIDRef="${tableStyle && bfReg ? GONGMUN_RIGHT : 0}" styleIDRef="0"><hp:run charPrIDRef="0">${tbl2}</hp:run></hp:p>`;
23507
+ }
23508
+ const plain = (block.text || "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
23509
+ return plain ? generateParagraph(plain) : "";
23510
+ }
23511
+ function appendPostamble(blocks, ctx) {
23512
+ const { gongmun, dfIds, bfReg, tableStyle, paraXmls } = ctx;
23513
+ if (gongmun?.noticeHead && dfIds) {
23514
+ paraXmls.push(...buildNoticeFoot(gongmun));
23515
+ }
23516
+ if (gongmun?.endMark && paraXmls.length > 0) {
23517
+ const lastBlock = [...blocks].reverse().find((b) => (b.text || "").trim() || b.type === "table" && b.rows?.length);
23518
+ const lastText = lastBlock && (lastBlock.type === "paragraph" || lastBlock.type === "list_item") ? lastBlock.text || "" : "";
23519
+ if (!hasEndMark(lastText)) paraXmls.push(buildEndMark());
23520
+ }
23521
+ if (gongmun?.docFoot && dfIds) {
23522
+ paraXmls.push(...buildDocFoot(gongmun, dfIds));
23523
+ }
23524
+ if (gongmun?.press && dfIds && bfReg && tableStyle) {
23525
+ paraXmls.push(...buildPressContact(gongmun, dfIds, tableStyle.totalWidth, bfReg));
22580
23526
  }
23527
+ }
23528
+ function blocksToSectionXml(blocks, theme, gongmun, gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null, fit = null, chartParts = null, bfReg = null, remap = null, dfIds = null, images = null) {
23529
+ resetTableIds();
23530
+ resetGjTableIds();
23531
+ resetExtraTableIds();
23532
+ const measured = !!gongmun && usesReportFonts(gongmun.preset);
23533
+ const richAssets = !!gongmun && needsGaejosikAssets(gongmun);
23534
+ const ctx = {
23535
+ theme,
23536
+ gongmun,
23537
+ gongmunList,
23538
+ fit,
23539
+ chartParts,
23540
+ bfReg,
23541
+ remap,
23542
+ dfIds,
23543
+ images,
23544
+ gaejosik: gongmun?.preset === "gaejosik",
23545
+ measured,
23546
+ richAssets,
23547
+ vBase: charVariantBase(richAssets, !!gongmun),
23548
+ // 공문서 표 스타일 — 본문 폭 맞춤 + 실측 정부 양식(헤더 음영·실측 프리셋 맑은 고딕 12pt)
23549
+ // bfReg가 있으면 실측 테두리 위계(외곽 0.4/내부 0.12/헤더 이중선)·셀 문단·우측 배치 적용
23550
+ tableStyle: gongmun ? {
23551
+ totalWidth: mmToHwpunit(210 - gongmun.margins.left - gongmun.margins.right),
23552
+ // 표 셀 크기 — 실측: 실측 프리셋 맑은 고딕 12pt(22·23), 비실측도 12pt(11·12,
23553
+ // 실결재 지배값 — 본문 크기 셀은 열폭 부족으로 서술 열 세로 신장, v4.0.2 QA)
23554
+ charPr: richAssets ? GJ_CHAR_TABLE : GONGMUN_TBL_CHAR,
23555
+ boldCharPr: richAssets ? GJ_CHAR_TABLE_BOLD : GONGMUN_TBL_CHAR_BOLD,
23556
+ charHeight: richAssets ? gaejosikSizes(gongmun.bodyHeight, gongmun.sizes).table : GONGMUN_TBL_PT,
23557
+ headerBf: gongmunTableHeaderBf(richAssets),
23558
+ centerParaPr: GONGMUN_CENTER,
23559
+ tblCenterParaPr: GONGMUN_TBL_CENTER,
23560
+ tblLeftParaPr: GONGMUN_TBL_LEFT,
23561
+ bfRegistry: bfReg ?? void 0,
23562
+ rightParaPr: GONGMUN_RIGHT
23563
+ } : null,
23564
+ // 개조식 장식표(표지·목차·장헤더·제목박스) 폭 스케일용 본문폭 — margins 오버라이드 대응
23565
+ gjBodyW: gongmun ? mmToHwpunit(210 - gongmun.margins.left - gongmun.margins.right) : GAEJOSIK_BASE_WIDTH,
23566
+ chamMap: (id) => id === CHAR_BOLD ? GJ_CHAR_CHAM_BOLD : id,
23567
+ opener: new SectionOpener(gongmun),
23568
+ paraXmls: [],
23569
+ // 표 방출 순번 — 생성 성공 여부와 무관하게 '시도' 기준으로 센다. 실패한 표가 이후
23570
+ // 표들의 순번을 밀면 앵커 없는 프로필(손편집·구버전)의 table_index 매칭이 어긋난다.
23571
+ tableSeq: 0,
23572
+ chapterNo: 0,
23573
+ h2Seq: 0,
23574
+ // h2 말머리 'number' 모드 아라비아 순번 (QA-2)
23575
+ coverH1Idx: -1,
23576
+ titleBoxH1Idx: -1,
23577
+ pressH1Idx: -1,
23578
+ gjChapterIdxs: [],
23579
+ pendingPageBreak: false,
23580
+ pendingNewNum: false,
23581
+ // 순서 있는 목록 카운터 — indent 레벨별 별도 유지. 다른 블록 만나면 해당 레벨 리셋.
23582
+ orderedCounters: {},
23583
+ prevWasOrdered: false
23584
+ };
23585
+ buildPreamble(blocks, ctx);
22581
23586
  for (let blockIdx = 0; blockIdx < blocks.length; blockIdx++) {
22582
23587
  const block = blocks[blockIdx];
22583
- let xml = "";
22584
23588
  if (block.type !== "list_item" || !block.ordered) {
22585
- if (prevWasOrdered) {
22586
- for (const k of Object.keys(orderedCounters)) delete orderedCounters[+k];
23589
+ if (ctx.prevWasOrdered) {
23590
+ for (const k of Object.keys(ctx.orderedCounters)) delete ctx.orderedCounters[+k];
22587
23591
  }
22588
- prevWasOrdered = false;
23592
+ ctx.prevWasOrdered = false;
22589
23593
  }
23594
+ let xml = "";
22590
23595
  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);
23596
+ case "heading":
23597
+ xml = renderHeading(block, blockIdx, ctx);
22637
23598
  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
- }
23599
+ case "paragraph":
23600
+ xml = renderParagraph(block, blockIdx, ctx);
22654
23601
  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 ");
23602
+ case "code_block":
23603
+ xml = renderCodeBlock(block, blockIdx, ctx);
22674
23604
  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);
23605
+ case "equation":
23606
+ xml = renderEquation(block, blockIdx, ctx);
22683
23607
  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
- );
23608
+ case "blockquote":
23609
+ xml = renderBlockquote(block, ctx);
22696
23610
  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);
23611
+ case "list_item":
23612
+ xml = renderListItem(block, blockIdx, ctx);
22751
23613
  break;
22752
- }
22753
23614
  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>`;
23615
+ xml = renderHr(ctx);
22755
23616
  break;
22756
23617
  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
- }
23618
+ xml = renderTable(block, ctx);
22765
23619
  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
- }
23620
+ case "html_table":
23621
+ xml = renderHtmlTable(block, ctx);
22780
23622
  break;
22781
- }
22782
23623
  }
22783
23624
  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;
23625
+ if (ctx.opener.isFirst && block.type !== "table") {
23626
+ xml = ctx.opener.inject(xml);
22790
23627
  }
22791
- if (pendingPageBreak) {
23628
+ if (ctx.pendingPageBreak) {
22792
23629
  xml = xml.replace(/^<hp:p /, `<hp:p pageBreak="1" `);
22793
- pendingPageBreak = false;
23630
+ ctx.pendingPageBreak = false;
22794
23631
  }
22795
- if (pendingNewNum) {
23632
+ if (ctx.pendingNewNum) {
22796
23633
  xml = xml.replace(/<hp:run charPrIDRef="(\d+)">/, `<hp:run charPrIDRef="$1">${newPageNumCtrl(1)}`);
22797
- pendingNewNum = false;
23634
+ ctx.pendingNewNum = false;
22798
23635
  }
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));
23636
+ ctx.paraXmls.push(xml);
22813
23637
  }
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>`);
23638
+ appendPostamble(blocks, ctx);
23639
+ if (ctx.paraXmls.length === 0) {
23640
+ ctx.paraXmls.push(ctx.opener.emptyDoc());
22816
23641
  }
22817
23642
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
22818
23643
  <hs:sec xmlns:hs="${NS_SECTION}" xmlns:hp="${NS_PARA}">
22819
- ${paraXmls.join("\n ")}
23644
+ ${ctx.paraXmls.join("\n ")}
22820
23645
  </hs:sec>`;
22821
23646
  }
22822
23647
 
@@ -22829,14 +23654,15 @@ async function markdownToHwpx(markdown, options) {
22829
23654
  const blocks = parseMarkdownToBlocks(markdown);
22830
23655
  const gongmunList = gongmun ? precomputeGongmunList(blocks, gongmun) : null;
22831
23656
  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;
23657
+ const staticBfEnd = staticBorderFillNext(gongmun);
23658
+ const remap = options?.profile ? buildProfileRemap(options.profile, charVariantBase(richAssets, !!gongmun) + (fit?.variants?.length ?? 0) * 4, staticBfEnd, staticFontNext(gongmun)) : null;
22834
23659
  const bfReg = gongmun ? new TableBfRegistry(staticBfEnd + (remap?.borderFillXmls.length ?? 0)) : null;
22835
23660
  const dfActive = !!gongmun && docframeActive(gongmun);
22836
23661
  const dfBase = charVariantBase(richAssets, !!gongmun) + (fit?.variants?.length ?? 0) * 4 + (remap?.charPrXmls.length ?? 0);
22837
23662
  const dfIds = dfActive ? docframeIds(dfBase) : null;
22838
23663
  const chartParts = [];
22839
- const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit, chartParts, bfReg, remap, dfIds);
23664
+ const images = new ImageRegistry();
23665
+ const sectionXml = blocksToSectionXml(blocks, theme, gongmun, gongmunList, fit, chartParts, bfReg, remap, dfIds, images);
22840
23666
  if (remap && remap.tables.length > 0) {
22841
23667
  const unused = remap.tables.filter((t) => !t.used).length;
22842
23668
  if (unused === remap.tables.length) {
@@ -22847,13 +23673,16 @@ async function markdownToHwpx(markdown, options) {
22847
23673
  const zip = new JSZip6();
22848
23674
  zip.file("mimetype", "application/hwp+zip", { compression: "STORE" });
22849
23675
  zip.file("META-INF/container.xml", generateContainerXml());
22850
- zip.file("Contents/content.hpf", generateManifest(chartParts));
23676
+ zip.file("Contents/content.hpf", generateManifest(chartParts, images.manifestItems(), gongmun ? "gongmun" : "default"));
23677
+ for (const part of images.parts) zip.file(part.name, part.data);
22851
23678
  zip.file("Contents/header.xml", generateHeaderXml(
22852
23679
  theme,
22853
23680
  gongmun,
22854
23681
  fit?.variants ?? [],
22855
23682
  extraBorderFills,
22856
- [...remap?.charPrXmls ?? [], ...dfActive ? docframeCharPrXmls(dfBase, richAssets) : []]
23683
+ [...remap?.charPrXmls ?? [], ...dfActive ? docframeCharPrXmls(dfBase, richAssets) : []],
23684
+ gongmunList?.indentVariants ?? [],
23685
+ remap?.fontFaces ?? []
22857
23686
  ));
22858
23687
  zip.file("Contents/section0.xml", sectionXml);
22859
23688
  for (const part of chartParts) zip.file(part.name, part.xml);
@@ -23033,7 +23862,19 @@ function parseBorderFills(headerDoc) {
23033
23862
  }
23034
23863
  return map;
23035
23864
  }
23036
- function parseCharPrs(headerDoc) {
23865
+ function parseHangulFonts(headerDoc) {
23866
+ const map = /* @__PURE__ */ new Map();
23867
+ for (const ff of elemsByLocal(headerDoc, "fontface")) {
23868
+ if (ff.getAttribute("lang") !== "HANGUL") continue;
23869
+ for (const font of elemsByLocal(ff, "font")) {
23870
+ const id = font.getAttribute("id");
23871
+ const face = font.getAttribute("face");
23872
+ if (id && face) map.set(id, face);
23873
+ }
23874
+ }
23875
+ return map;
23876
+ }
23877
+ function parseCharPrs(headerDoc, hangulFonts) {
23037
23878
  const map = /* @__PURE__ */ new Map();
23038
23879
  for (const cp of elemsByLocal(headerDoc, "charPr")) {
23039
23880
  const id = cp.getAttribute("id");
@@ -23048,7 +23889,11 @@ function parseCharPrs(headerDoc) {
23048
23889
  if (findChildByLocalName(cp, "underline")) def.underline = true;
23049
23890
  const fontRef = findChildByLocalName(cp, "fontRef");
23050
23891
  const hangul = fontRef?.getAttribute("hangul");
23051
- if (hangul) def.fontRef_hangul = hangul;
23892
+ if (hangul) {
23893
+ def.fontRef_hangul = hangul;
23894
+ const face = hangulFonts.get(hangul);
23895
+ if (face) def.fontName_hangul = face;
23896
+ }
23052
23897
  map.set(id, def);
23053
23898
  }
23054
23899
  return map;
@@ -23076,6 +23921,8 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23076
23921
  const usedBf = /* @__PURE__ */ new Set();
23077
23922
  const usedCp = /* @__PURE__ */ new Set();
23078
23923
  const colWidths = new Array(cols).fill(void 0);
23924
+ const spanCells = [];
23925
+ const row0Texts = /* @__PURE__ */ new Map();
23079
23926
  let anchorText = "";
23080
23927
  for (const tc2 of elemsByLocal(tbl2, "tc")) {
23081
23928
  if (nearestTable(tc2) !== tbl2) continue;
@@ -23085,6 +23932,7 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23085
23932
  const row = num(addr?.getAttribute("rowAddr")) ?? 0;
23086
23933
  const col = num(addr?.getAttribute("colAddr")) ?? 0;
23087
23934
  if (row === 0 && col === 0 && !anchorText) anchorText = directCellText(tc2);
23935
+ if (row === 0) row0Texts.set(col, directCellText(tc2));
23088
23936
  const colSpan = num(span?.getAttribute("colSpan")) ?? 1;
23089
23937
  const rowSpan = num(span?.getAttribute("rowSpan")) ?? 1;
23090
23938
  const bfId = tc2.getAttribute("borderFillIDRef") || void 0;
@@ -23103,7 +23951,20 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23103
23951
  usedCp.add(cpId);
23104
23952
  }
23105
23953
  cells.push(cell2);
23106
- if (row === 0 && colSpan === 1 && col < cols && w) colWidths[col] = w;
23954
+ const wNum = num(w);
23955
+ if (wNum != null && col < cols) {
23956
+ if (colSpan === 1) {
23957
+ if (colWidths[col] == null) colWidths[col] = wNum;
23958
+ } else spanCells.push({ col, colSpan, w: wNum });
23959
+ }
23960
+ }
23961
+ for (const s of spanCells) {
23962
+ const covered = Array.from({ length: s.colSpan }, (_, i) => s.col + i).filter((c) => c < cols);
23963
+ const unknown = covered.filter((c) => colWidths[c] == null);
23964
+ if (unknown.length === 0) continue;
23965
+ const known = covered.reduce((sum, c) => sum + (colWidths[c] ?? 0), 0);
23966
+ const each = Math.floor((s.w - known) / unknown.length);
23967
+ if (each > 0) for (const c of unknown) colWidths[c] = each;
23107
23968
  }
23108
23969
  const table2 = {
23109
23970
  table_index: tableIndex,
@@ -23114,8 +23975,10 @@ function parseTable3(tbl2, tableIndex, borderFills, charPrs) {
23114
23975
  };
23115
23976
  const anchor = normalizeAnchor(anchorText);
23116
23977
  if (anchor) table2.anchor_text = anchor;
23978
+ const rowAnchor = normalizeRowAnchor(Array.from({ length: cols }, (_, c) => row0Texts.get(c) ?? ""));
23979
+ if (rowAnchor) table2.anchor_row = rowAnchor;
23117
23980
  if (width) table2.width_hwpunit = width;
23118
- if (colWidths.every((w) => w != null)) table2.col_widths_hwpunit = colWidths;
23981
+ if (colWidths.every((w) => w != null)) table2.col_widths_hwpunit = colWidths.map(String);
23119
23982
  const cp = pick(charPrs, usedCp);
23120
23983
  if (Object.keys(cp).length) table2.used_char_prs = cp;
23121
23984
  return table2;
@@ -23172,7 +24035,7 @@ async function hwpxToProfile(input) {
23172
24035
  if (headerFile) headerXml = await headerFile.async("text");
23173
24036
  const headerDoc = parser.parseFromString(headerXml, "text/xml");
23174
24037
  const borderFills = parseBorderFills(headerDoc);
23175
- const charPrs = parseCharPrs(headerDoc);
24038
+ const charPrs = parseCharPrs(headerDoc, parseHangulFonts(headerDoc));
23176
24039
  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
24040
  const tables = [];
23178
24041
  let tableIndex = 0;
@@ -23183,7 +24046,7 @@ async function hwpxToProfile(input) {
23183
24046
  tables.push(parseTable3(tbl2, tableIndex++, borderFills, charPrs));
23184
24047
  }
23185
24048
  }
23186
- return { schema_version: "0.2.0", tables };
24049
+ return { schema_version: "0.3.0", tables };
23187
24050
  }
23188
24051
 
23189
24052
  // src/hwpx/font-catalog.ts
@@ -23323,7 +24186,7 @@ var COMMON_FREE = [
23323
24186
  "\uC724\uBA85\uC870"
23324
24187
  ];
23325
24188
  function norm(name) {
23326
- return name.replace(/\s+/g, "").toLowerCase();
24189
+ return name.normalize("NFC").replace(/\s+/g, "").toLowerCase();
23327
24190
  }
23328
24191
  var KNOWN = new Set([...HANCOM_BUNDLED, ...WINDOWS_DEFAULT, ...COMMON_FREE].map(norm));
23329
24192
  function isKnownFont(name) {
@@ -23353,7 +24216,7 @@ var RULES = [
23353
24216
  {
23354
24217
  code: "DATE_ZERO_PAD",
23355
24218
  severity: "error",
23356
- pattern: /\b\d{4}\.\s*0\d\.|\.\s*0\d\./g,
24219
+ pattern: /\b\d{4}\.\s*0\d\.|\b\d{4}\.\s*\d{1,2}\.\s*0\d/g,
23357
24220
  message: "\uC6D4\xB7\uC77C \uC55E\uC758 '0'\uC740 \uD45C\uAE30\uD558\uC9C0 \uC54A\uC74C",
23358
24221
  suggest: "\uC608) 2025. 1. 6. (2025. 01. 06. \u2715)"
23359
24222
  },
@@ -23435,22 +24298,25 @@ var RULES = [
23435
24298
  {
23436
24299
  code: "COLON_SPACE",
23437
24300
  severity: "warning",
23438
- pattern: /\S\s+:\S|\S:(?!\/\/)[^\s\d]/g,
24301
+ pattern: /\S\s+:(?!\/\/)|\S:(?!\/\/)[^\s\d]/g,
23439
24302
  message: "\uC30D\uC810\uC740 \uC55E\uB9D0\uC5D0 \uBD99\uC774\uACE0 \uB4A4\uB294 \uD55C \uCE78 \uB744\uC6C0",
23440
24303
  suggest: "\uC608) \uC6D0\uC7A5: \uAE40\uAC11\uB3D9"
23441
24304
  }
23442
24305
  ];
23443
24306
  function lintGongmunText(text) {
23444
24307
  const findings = [];
23445
- let inFence = false;
24308
+ let fenceMarker = null;
23446
24309
  const lines = text.split(/\r?\n/);
23447
24310
  for (let i = 0; i < lines.length; i++) {
23448
24311
  const line = lines[i];
23449
- if (/^\s*(```|~~~)/.test(line)) {
23450
- inFence = !inFence;
24312
+ const fence = line.match(/^\s*(```+|~~~+)/);
24313
+ if (fence) {
24314
+ const kind = fence[1][0];
24315
+ if (fenceMarker === null) fenceMarker = kind;
24316
+ else if (kind === fenceMarker) fenceMarker = null;
23451
24317
  continue;
23452
24318
  }
23453
- if (inFence) continue;
24319
+ if (fenceMarker !== null) continue;
23454
24320
  for (const r of RULES) {
23455
24321
  r.pattern.lastIndex = 0;
23456
24322
  for (const m of line.matchAll(r.pattern)) {
@@ -26207,7 +27073,7 @@ async function parseHwp(buffer, options) {
26207
27073
  async function parsePdf(buffer, options) {
26208
27074
  let parsePdfDocument;
26209
27075
  try {
26210
- const mod = await import("./parser-AODAQQLQ.js");
27076
+ const mod = await import("./parser-3MAYZUJX.js");
26211
27077
  parsePdfDocument = mod.parsePdfDocument;
26212
27078
  } catch {
26213
27079
  return {
@@ -26298,6 +27164,9 @@ async function fillForm(input, values, outputFormat = "markdown") {
26298
27164
  }
26299
27165
 
26300
27166
  export {
27167
+ PRESET_ALIAS,
27168
+ normalizeGongmunPreset,
27169
+ incompatibleGongmunWarnings,
26301
27170
  extractHwpxMetadataOnly,
26302
27171
  extractHwp5MetadataOnly,
26303
27172
  formatFillValue,
@@ -26309,8 +27178,6 @@ export {
26309
27178
  extractFormSchema,
26310
27179
  fillFormFields,
26311
27180
  fillHwpx,
26312
- PRESET_ALIAS,
26313
- normalizeGongmunPreset,
26314
27181
  markdownToHwpx,
26315
27182
  compare,
26316
27183
  diffBlocks,
@@ -26339,4 +27206,4 @@ export {
26339
27206
  parseHwpml,
26340
27207
  fillForm
26341
27208
  };
26342
- //# sourceMappingURL=chunk-K2SETHJ6.js.map
27209
+ //# sourceMappingURL=chunk-Z6Q3YG22.js.map