kordoc 4.24.3 → 4.24.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +5 -5
  2. package/dist/{-Q56U3KCA.js → -WND2HWVH.js} +11 -11
  3. package/dist/{chunk-3EWXRIPH.js → chunk-3LSHLWTX.js} +3 -3
  4. package/dist/{chunk-HNX2I7D6.js → chunk-44E52JJ7.js} +2 -2
  5. package/dist/{chunk-XNNMMYFO.js → chunk-47MTLP4U.js} +2 -2
  6. package/dist/{chunk-42FSKT7R.js → chunk-4XVL3OBP.js} +2 -2
  7. package/dist/{chunk-OAOM7IEI.js → chunk-5AHG3XY3.js} +11 -11
  8. package/dist/{chunk-6ZRZLV3J.cjs → chunk-6A4KYMQR.cjs} +2 -2
  9. package/dist/{chunk-6ZRZLV3J.cjs.map → chunk-6A4KYMQR.cjs.map} +1 -1
  10. package/dist/{chunk-EWCJCWTT.js → chunk-6CPBNK5O.js} +2 -2
  11. package/dist/{chunk-FZT34MUI.js → chunk-A5JDO52L.js} +4 -4
  12. package/dist/{chunk-4X73CGIL.js → chunk-AKIJTKEA.js} +2 -2
  13. package/dist/{chunk-YG4YOPK6.cjs → chunk-BODFTRWA.cjs} +3 -3
  14. package/dist/{chunk-YG4YOPK6.cjs.map → chunk-BODFTRWA.cjs.map} +1 -1
  15. package/dist/{chunk-XAZ5RGVG.js → chunk-BYYZUFPC.js} +11 -8
  16. package/dist/chunk-BYYZUFPC.js.map +1 -0
  17. package/dist/{chunk-Q2QKRE4P.js → chunk-D4YWVZHD.js} +2 -2
  18. package/dist/{chunk-33NZN73M.js → chunk-DTADAHJD.js} +2 -2
  19. package/dist/{chunk-XZOAE2Q7.js → chunk-E7N6BUJT.js} +12 -9
  20. package/dist/chunk-E7N6BUJT.js.map +1 -0
  21. package/dist/{chunk-I2QETSZN.js → chunk-FK3JPTHI.js} +2 -2
  22. package/dist/{chunk-AEEQN6L3.js → chunk-GGVHKYKZ.js} +2 -2
  23. package/dist/{chunk-PMX5CPK5.js → chunk-JHUW5PKO.js} +8 -7
  24. package/dist/chunk-JHUW5PKO.js.map +1 -0
  25. package/dist/{chunk-VFEI2GIE.cjs → chunk-KFQNG2EA.cjs} +3 -3
  26. package/dist/{chunk-VFEI2GIE.cjs.map → chunk-KFQNG2EA.cjs.map} +1 -1
  27. package/dist/{chunk-PHZDAHZC.js → chunk-LBOXKGAH.js} +3 -3
  28. package/dist/{chunk-JILIA5EV.cjs → chunk-LJ7U7MFN.cjs} +31 -28
  29. package/dist/chunk-LJ7U7MFN.cjs.map +1 -0
  30. package/dist/{chunk-W6O7KBOD.js → chunk-MC4DPT4I.js} +2 -2
  31. package/dist/{chunk-KXHNIHLC.js → chunk-MMPBJGWQ.js} +2 -2
  32. package/dist/{chunk-NKDPCALC.js → chunk-QTJTBCTZ.js} +4 -4
  33. package/dist/{chunk-N65EY5BI.cjs → chunk-TPKP3WQU.cjs} +6 -6
  34. package/dist/{chunk-N65EY5BI.cjs.map → chunk-TPKP3WQU.cjs.map} +1 -1
  35. package/dist/{chunk-EEXIFCXC.js → chunk-VBY3JQL2.js} +2 -2
  36. package/dist/{chunk-TZ753B6A.js → chunk-WMDGGQXJ.js} +2 -2
  37. package/dist/{chunk-HTG3VVET.cjs → chunk-WN5X4DT3.cjs} +24 -24
  38. package/dist/{chunk-HTG3VVET.cjs.map → chunk-WN5X4DT3.cjs.map} +1 -1
  39. package/dist/{chunk-HU7POUBS.js → chunk-YBCW544D.js} +2 -2
  40. package/dist/{chunk-J45OVEUM.js → chunk-YEYNBH5W.js} +112 -23
  41. package/dist/chunk-YEYNBH5W.js.map +1 -0
  42. package/dist/chunks-2UDUNVGQ.js +10 -0
  43. package/dist/cli.js +30 -30
  44. package/dist/{formula-2BPHF6O7.cjs → formula-6DJAY3KI.cjs} +9 -9
  45. package/dist/{formula-2BPHF6O7.cjs.map → formula-6DJAY3KI.cjs.map} +1 -1
  46. package/dist/{formula-IREEAC4T.js → formula-MPBZ7OMN.js} +3 -3
  47. package/dist/{formula-EA5GDCUD.js → formula-NA3LD35B.js} +4 -4
  48. package/dist/{image-ocr-LGOFUS3O.js → image-ocr-6TGDXUAS.js} +9 -9
  49. package/dist/{image-ocr-CCTE7HX3.cjs → image-ocr-REM4LZKX.cjs} +17 -17
  50. package/dist/{image-ocr-CCTE7HX3.cjs.map → image-ocr-REM4LZKX.cjs.map} +1 -1
  51. package/dist/{image-ocr-JQJC5HWK.js → image-ocr-THA5BASF.js} +6 -6
  52. package/dist/index.cjs +740 -650
  53. package/dist/index.cjs.map +1 -1
  54. package/dist/index.js +118 -28
  55. package/dist/index.js.map +1 -1
  56. package/dist/mcp.js +23 -23
  57. package/dist/{model-bundle-YW62Y6UD.js → model-bundle-3YDTJ6EQ.js} +5 -5
  58. package/dist/{models-AAL3IS2R.js → models-M7AX7VIN.js} +5 -5
  59. package/dist/{parse-worker-v2-3JAHV7NY.js → parse-worker-v2-3ZMFWDFW.js} +12 -12
  60. package/dist/{parser-HE45CCSG.cjs → parser-GIDYL5LH.cjs} +158 -158
  61. package/dist/{parser-HE45CCSG.cjs.map → parser-GIDYL5LH.cjs.map} +1 -1
  62. package/dist/{parser-ZBP5GOQK.js → parser-MNNJSFEZ.js} +9 -9
  63. package/dist/{parser-4FCKKJ2N.js → parser-TD4VZDGY.js} +12 -12
  64. package/dist/{pdf-ocr-HFAEOA7W.js → pdf-ocr-5HEVZKG5.js} +6 -6
  65. package/dist/{pdf-ocr-Q4TPGDDI.js → pdf-ocr-6BAOQ6CY.js} +9 -9
  66. package/dist/pdf-ocr-NOSNTEDS.cjs +31 -0
  67. package/dist/{pdf-ocr-7MDCLWAA.cjs.map → pdf-ocr-NOSNTEDS.cjs.map} +1 -1
  68. package/dist/{profile-io-EDQTKLLL.js → profile-io-H4WQIGN6.js} +3 -3
  69. package/dist/{rasterize-VD2XGHUJ.js → rasterize-EOPWJWBO.js} +3 -3
  70. package/dist/{redact-doc-TBIBQJCT.js → redact-doc-BBZVAA3I.js} +11 -11
  71. package/dist/{render-J5KAZAQH.js → render-ZZ4CGJ6J.js} +7 -7
  72. package/dist/seal-RRYFKTEQ.js +10 -0
  73. package/dist/{setup-5LYHCQRU.js → setup-UIPKBNLA.js} +3 -3
  74. package/dist/visual-DLLRTF6C.js +21 -0
  75. package/dist/{watch-YP4IWM75.js → watch-OXQ6O5HD.js} +12 -12
  76. package/package.json +1 -1
  77. package/dist/chunk-J45OVEUM.js.map +0 -1
  78. package/dist/chunk-JILIA5EV.cjs.map +0 -1
  79. package/dist/chunk-PMX5CPK5.js.map +0 -1
  80. package/dist/chunk-XAZ5RGVG.js.map +0 -1
  81. package/dist/chunk-XZOAE2Q7.js.map +0 -1
  82. package/dist/chunks-REYSCBPB.js +0 -10
  83. package/dist/pdf-ocr-7MDCLWAA.cjs +0 -31
  84. package/dist/seal-H2JNAZU7.js +0 -10
  85. package/dist/visual-7EO6GR3M.js +0 -21
  86. /package/dist/{-Q56U3KCA.js.map → -WND2HWVH.js.map} +0 -0
  87. /package/dist/{chunk-3EWXRIPH.js.map → chunk-3LSHLWTX.js.map} +0 -0
  88. /package/dist/{chunk-HNX2I7D6.js.map → chunk-44E52JJ7.js.map} +0 -0
  89. /package/dist/{chunk-XNNMMYFO.js.map → chunk-47MTLP4U.js.map} +0 -0
  90. /package/dist/{chunk-42FSKT7R.js.map → chunk-4XVL3OBP.js.map} +0 -0
  91. /package/dist/{chunk-OAOM7IEI.js.map → chunk-5AHG3XY3.js.map} +0 -0
  92. /package/dist/{chunk-EWCJCWTT.js.map → chunk-6CPBNK5O.js.map} +0 -0
  93. /package/dist/{chunk-FZT34MUI.js.map → chunk-A5JDO52L.js.map} +0 -0
  94. /package/dist/{chunk-4X73CGIL.js.map → chunk-AKIJTKEA.js.map} +0 -0
  95. /package/dist/{chunk-Q2QKRE4P.js.map → chunk-D4YWVZHD.js.map} +0 -0
  96. /package/dist/{chunk-33NZN73M.js.map → chunk-DTADAHJD.js.map} +0 -0
  97. /package/dist/{chunk-I2QETSZN.js.map → chunk-FK3JPTHI.js.map} +0 -0
  98. /package/dist/{chunk-AEEQN6L3.js.map → chunk-GGVHKYKZ.js.map} +0 -0
  99. /package/dist/{chunk-PHZDAHZC.js.map → chunk-LBOXKGAH.js.map} +0 -0
  100. /package/dist/{chunk-W6O7KBOD.js.map → chunk-MC4DPT4I.js.map} +0 -0
  101. /package/dist/{chunk-KXHNIHLC.js.map → chunk-MMPBJGWQ.js.map} +0 -0
  102. /package/dist/{chunk-NKDPCALC.js.map → chunk-QTJTBCTZ.js.map} +0 -0
  103. /package/dist/{chunk-EEXIFCXC.js.map → chunk-VBY3JQL2.js.map} +0 -0
  104. /package/dist/{chunk-TZ753B6A.js.map → chunk-WMDGGQXJ.js.map} +0 -0
  105. /package/dist/{chunk-HU7POUBS.js.map → chunk-YBCW544D.js.map} +0 -0
  106. /package/dist/{chunks-REYSCBPB.js.map → chunks-2UDUNVGQ.js.map} +0 -0
  107. /package/dist/{formula-IREEAC4T.js.map → formula-MPBZ7OMN.js.map} +0 -0
  108. /package/dist/{formula-EA5GDCUD.js.map → formula-NA3LD35B.js.map} +0 -0
  109. /package/dist/{image-ocr-LGOFUS3O.js.map → image-ocr-6TGDXUAS.js.map} +0 -0
  110. /package/dist/{image-ocr-JQJC5HWK.js.map → image-ocr-THA5BASF.js.map} +0 -0
  111. /package/dist/{model-bundle-YW62Y6UD.js.map → model-bundle-3YDTJ6EQ.js.map} +0 -0
  112. /package/dist/{models-AAL3IS2R.js.map → models-M7AX7VIN.js.map} +0 -0
  113. /package/dist/{parse-worker-v2-3JAHV7NY.js.map → parse-worker-v2-3ZMFWDFW.js.map} +0 -0
  114. /package/dist/{parser-ZBP5GOQK.js.map → parser-MNNJSFEZ.js.map} +0 -0
  115. /package/dist/{parser-4FCKKJ2N.js.map → parser-TD4VZDGY.js.map} +0 -0
  116. /package/dist/{pdf-ocr-HFAEOA7W.js.map → pdf-ocr-5HEVZKG5.js.map} +0 -0
  117. /package/dist/{pdf-ocr-Q4TPGDDI.js.map → pdf-ocr-6BAOQ6CY.js.map} +0 -0
  118. /package/dist/{profile-io-EDQTKLLL.js.map → profile-io-H4WQIGN6.js.map} +0 -0
  119. /package/dist/{rasterize-VD2XGHUJ.js.map → rasterize-EOPWJWBO.js.map} +0 -0
  120. /package/dist/{redact-doc-TBIBQJCT.js.map → redact-doc-BBZVAA3I.js.map} +0 -0
  121. /package/dist/{render-J5KAZAQH.js.map → render-ZZ4CGJ6J.js.map} +0 -0
  122. /package/dist/{seal-H2JNAZU7.js.map → seal-RRYFKTEQ.js.map} +0 -0
  123. /package/dist/{setup-5LYHCQRU.js.map → setup-UIPKBNLA.js.map} +0 -0
  124. /package/dist/{visual-7EO6GR3M.js.map → visual-DLLRTF6C.js.map} +0 -0
  125. /package/dist/{watch-YP4IWM75.js.map → watch-OXQ6O5HD.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  // src/utils.ts
2
- var VERSION = true ? "4.24.3" : "0.0.0-dev";
2
+ var VERSION = true ? "4.24.4" : "0.0.0-dev";
3
3
  function toArrayBuffer(buf) {
4
4
  if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) {
5
5
  return buf.buffer;
@@ -161,4 +161,4 @@ export {
161
161
  classifyError,
162
162
  partExtension
163
163
  };
164
- //# sourceMappingURL=chunk-I2QETSZN.js.map
164
+ //# sourceMappingURL=chunk-FK3JPTHI.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  assertNetworkAllowed
4
- } from "./chunk-KXHNIHLC.js";
4
+ } from "./chunk-MMPBJGWQ.js";
5
5
 
6
6
  // src/pdf/formula/models.ts
7
7
  import { createHash as createHash2 } from "crypto";
@@ -296,4 +296,4 @@ export {
296
296
  ensureFormulaModels,
297
297
  ensureSingleModel
298
298
  };
299
- //# sourceMappingURL=chunk-AEEQN6L3.js.map
299
+ //# sourceMappingURL=chunk-GGVHKYKZ.js.map
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  KordocError,
4
4
  unzipLimitBytes
5
- } from "./chunk-EWCJCWTT.js";
5
+ } from "./chunk-6CPBNK5O.js";
6
6
 
7
7
  // src/hwpx/parser-shared.ts
8
8
  import { DOMParser } from "@xmldom/xmldom";
@@ -192,7 +192,7 @@ function hwpFaceToCssStack(face) {
192
192
  const head = FONT_ALIASES[trimmed] ?? cssQuote(trimmed);
193
193
  return `${head},${generic}`;
194
194
  }
195
- function collectHangulFonts(root, lang = "HANGUL", hftIds) {
195
+ function collectHangulFonts(root, lang = "HANGUL", hftIds, substKoPub = false) {
196
196
  const map = /* @__PURE__ */ new Map();
197
197
  const findFaces = (el, depth) => {
198
198
  if (depth > 24) return null;
@@ -228,7 +228,7 @@ function collectHangulFonts(root, lang = "HANGUL", hftIds) {
228
228
  const id = e.getAttribute("id");
229
229
  let face = e.getAttribute("face");
230
230
  let type = e.getAttribute("type");
231
- if (face && type === "TTF" && /^한양/.test(face)) {
231
+ if (face && type === "TTF" && (/^한양/.test(face) || substKoPub && /^KoPub/.test(face))) {
232
232
  const sub = Array.from(e.childNodes).find((c) => c.nodeType === 1 && (c.tagName || "").replace(/^[^:]+:/, "") === "substFont");
233
233
  const sf = sub?.getAttribute("face");
234
234
  if (sf) {
@@ -319,10 +319,11 @@ function parseRenderStyles(headXml, opts) {
319
319
  const hv = Number.parseFloat(root.getAttribute("version") ?? "");
320
320
  const oldUnit = Number.isFinite(hv) && hv < 1.4;
321
321
  const hftHangul = /* @__PURE__ */ new Set(), hftLatin = /* @__PURE__ */ new Set();
322
- const hangulFonts = collectHangulFonts(root, "HANGUL", hftHangul);
323
- const latinFonts = collectHangulFonts(root, "LATIN", hftLatin);
322
+ const sk = !!opts?.substKoPub;
323
+ const hangulFonts = collectHangulFonts(root, "HANGUL", hftHangul, sk);
324
+ const latinFonts = collectHangulFonts(root, "LATIN", hftLatin, sk);
324
325
  const hftHanja = /* @__PURE__ */ new Set(), hftJapanese = /* @__PURE__ */ new Set(), hftSymbol = /* @__PURE__ */ new Set();
325
- const langFonts = { hanja: [collectHangulFonts(root, "HANJA", hftHanja), hftHanja], japanese: [collectHangulFonts(root, "JAPANESE", hftJapanese), hftJapanese], symbol: [collectHangulFonts(root, "SYMBOL", hftSymbol), hftSymbol] };
326
+ const langFonts = { hanja: [collectHangulFonts(root, "HANJA", hftHanja, sk), hftHanja], japanese: [collectHangulFonts(root, "JAPANESE", hftJapanese, sk), hftJapanese], symbol: [collectHangulFonts(root, "SYMBOL", hftSymbol, sk), hftSymbol] };
326
327
  const autoTabPr = /* @__PURE__ */ new Set();
327
328
  const paraTabRef = /* @__PURE__ */ new Map();
328
329
  const paraBullet = /* @__PURE__ */ new Map();
@@ -494,4 +495,4 @@ export {
494
495
  hwpFaceToCssStack,
495
496
  parseRenderStyles
496
497
  };
497
- //# sourceMappingURL=chunk-PMX5CPK5.js.map
498
+ //# sourceMappingURL=chunk-JHUW5PKO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hwpx/parser-shared.ts","../src/shared/xml.ts","../src/render/head-styles.ts"],"sourcesContent":["/**\n * HWPX 파서 공유 상수/타입/유틸 (parser.ts에서 분리).\n * ZIP 한도, 섹션 공유 상태(SectionShared), walk 컨텍스트(WalkCtx), XML 유틸.\n */\n\nimport { DOMParser } from \"@xmldom/xmldom\"\nimport type JSZip from \"jszip\"\nimport { KordocError, unzipLimitBytes } from \"../utils.js\"\nimport type { CellContext, IRBlock, ParseWarning } from \"../types.js\"\n// WalkCtx.styleMap 타입 참조 — 타입 전용이라 styles.ts와의 순환은 컴파일 시 소거됨\nimport type { HwpxStyleMap } from \"./styles.js\"\nimport type { NoteNumberFormat } from \"./notes.js\"\nimport type { Edges } from \"../table/layout-frames.js\"\n\n// 256MB — rhwp 1만 건 실문서 서베이에서 section1.xml 단독 75.2MB(압축비 35:1) 정상\n// 문서가 확인됨 (rhwp #1917). 종전 100MB 총합 컷은 대형 실문서를 ZIP bomb 으로 오인 거부.\nexport const MAX_DECOMPRESS_SIZE = unzipLimitBytes(256 * 1024 * 1024)\n/** 손상 ZIP 복구 시 최대 엔트리 수 */\nexport const MAX_ZIP_ENTRIES = 500\n\n/** ZIP bomb 가드 전용 에러 — per-section catch가 XML fatalError(PARTIAL_PARSE로 강등)와\n * 구분해 이것만 재던진다. KordocError 서브클래스라 sanitizeError allowlist는 그대로 통과 */\nexport class ZipBombError extends KordocError {\n constructor(message: string) {\n super(message)\n this.name = \"ZipBombError\"\n }\n}\n\n/**\n * ZIP 엔트리를 압축 해제 상한까지만 푼다 — 중앙 디렉터리의 크기 필드는 위조할 수 있어 사전 검사(precheckZipSize)를 지나고,\n * 다 푼 뒤 재는 누적 검사로는 엔트리 하나가 상한의 몇 배를 메모리에 올리는 것을 못 막았다(크기 필드 100B·실제 800MB manifest →\n * RSS +1GB, rhwp #7602 의 엔트리별 읽기 상한과 같은 방어). used 는 이미 푼 누적량 — 누적 집계는 호출부가 종전대로 한다.\n * 글은 JSZip async(\"text\") 와 같은 Buffer UTF-8 디코딩(BOM 보존)\n */\nexport function readZipEntry(file: JSZip.JSZipObject, type: \"text\", used?: number): Promise<string>\nexport function readZipEntry(file: JSZip.JSZipObject, type: \"uint8array\", used?: number): Promise<Uint8Array>\nexport function readZipEntry(file: JSZip.JSZipObject, type: \"text\" | \"uint8array\", used = 0): Promise<string | Uint8Array> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let size = 0\n // 중앙 디렉터리가 선언한 크기 — JSZip 도 다 푼 뒤 실제 크기와 다르면 오류를 낸다. 넘는 순간 끊으면 선언 크기 합을 보는\n // 사전 검사(precheckZipSize)가 실제 해제량의 상한이 된다(엔트리마다 작게 속여 여럿 담은 폭탄)\n const declared = (file as unknown as { _data?: { uncompressedSize?: unknown } })._data?.uncompressedSize\n const limit = typeof declared === \"number\" && declared >= 0 ? declared : Number.POSITIVE_INFINITY\n const stream = file.nodeStream(\"nodebuffer\")\n stream.on(\"data\", (chunk: Buffer) => {\n size += chunk.length\n if (used + size > MAX_DECOMPRESS_SIZE || size > limit) {\n stream.pause()\n reject(new ZipBombError(\"ZIP 압축 해제 크기 초과 (ZIP bomb 의심)\"))\n return\n }\n chunks.push(chunk)\n })\n stream.on(\"error\", reject)\n stream.on(\"end\", () => {\n if (type === \"text\") return resolve(Buffer.concat(chunks, size).toString(\"utf-8\"))\n // 바이트는 단독 버퍼로 — Buffer.concat 은 작은 크기를 공유 풀에서 잘라 줘 data.buffer 가 남의 64KB 가 된다\n const out = new Uint8Array(size)\n let at = 0\n for (const c of chunks) { out.set(c, at); at += c.length }\n resolve(out)\n })\n })\n}\n\n/** colSpan/rowSpan을 안전한 범위로 클램핑 */\nexport function clampSpan(val: number, max: number): number {\n return Math.max(1, Math.min(val, max))\n}\n\n// XML DOM 헬퍼 정본은 src/shared/xml.ts — 하위호환 재수출 (section-walker, render/* 등이\n// 이 모듈에서 import). MAX_XML_DEPTH 의미(요소 깊이 ≠ 표 중첩 단계)는 정본 주석 참조.\nexport { MAX_XML_DEPTH, findChildByLocalName, extractTextFromNode } from \"../shared/xml.js\"\n\n/** 셀 컨텍스트 확장 — 중첩표/이미지/다중문단 블록과 제목셀 여부를 IRCell로 전달 (v3.0) */\nexport interface CellCtxEx extends CellContext {\n blocks?: IRBlock[]\n /** 중첩표/이미지 등 구조 콘텐츠 존재 — true일 때만 IRCell.blocks로 attach */\n hasStructure?: boolean\n isHeader?: boolean\n /** 칸의 보이는 변 (borderFillIDRef → header borderFill) — IR 칸의 CELL_EDGES 로 옮긴다 (v4.17.0) */\n edges?: Edges\n /**\n * cell.text 평탄화 조립용 임시 상태 (#52 후속) — 인라인 흐름(글자취급 표·같은 문단\n * 텍스트)이 \"열려\" 있어 다음 인라인 항목을 `\\n`이 아니라 공백으로 이어야 하는지.\n * 문단 경계·블록/float 표에서 닫힌다(false). 최종 IRCell로는 나가지 않는 워크 전용 필드.\n */\n lineOpen?: boolean\n /**\n * keepEmptyParagraphs 조립용 임시 상태 (#57) — 이 셀에서 문단(빈 문단 포함)을 하나라도\n * 봤는지. 선두 빈 문단은 cell.text가 계속 \"\"라 truthiness로 구분할 수 없어 별도 추적한다.\n * 옵션 off일 땐 미사용. 최종 IRCell로는 나가지 않는 워크 전용 필드.\n */\n paraSeen?: boolean\n}\n\nexport interface TableState {\n rows: CellContext[][]\n currentRow: CellContext[]\n cell: CellCtxEx | null\n /** hp:caption 텍스트 — IRTable.caption으로 전달 (v3.0) */\n caption?: string\n /** hp:caption 내부 블록(중첩표 포함) — IRTable.captionBlocks로 전달 (#55) */\n captionBlocks?: IRBlock[]\n /**\n * 글자취급(treatAsChar=\"1\") 표 여부 — 부모 셀 텍스트 평탄화 시 앞뒤 텍스트와 같은 줄로\n * 공백 연결할지 판단 (#52 후속). 블록/float 표(기본 undefined)는 종전대로 `\\n`.\n */\n inline?: boolean\n /** hp:tbl id — IRTable.sourceId (렌더 region 조인 키, #76) */\n sourceId?: string\n}\n\n/** 섹션 간 공유 상태 — 자동번호 카운터, 머리말/꼬리말, 변경추적 */\nexport interface SectionShared {\n /** numbering id → 레벨별(1..10) 카운터. -1 = 미사용(start값으로 초기화 — 0은 start=\"0\"의 유효값) */\n numState: Map<string, number[]>\n pageText: { headers: string[]; footers: string[] }\n track: { deleteDepth: number; warned: boolean; deletedObjects: WeakSet<Element>; deletedImageRefs: Set<string> }\n /** content.hpf kordoc-layout 메타 (\"default\"|\"gongmun\") — 자사 생성 파일 왕복 채널\n * 게이트. null/미설정 = 외래 파일 (id 기반 인라인 강조·인용 복원 꺼짐) */\n kordocLayout?: string | null\n /** 표 후행 빈 열(앵커 있는 입력란) 보존 — ParseOptions.keepTrailingEmptyCols (#47) */\n keepTrailingEmptyCols?: boolean\n /** 빈 문단 보존 — ParseOptions.keepEmptyParagraphs (#57) */\n keepEmptyParagraphs?: boolean\n /** 미기입 누름틀 안내문도 마크다운에 — ParseOptions.includeFieldPlaceholders (#92) */\n includeFieldPlaceholders?: boolean\n /** 실제 페이지 경계 상태 (#66) — base: 이전 섹션까지 누적 페이지 수,\n * allUsable: 지금까지 파싱한 전 섹션이 조판 캐시(linesegarray)를 신뢰 가능 */\n pageState: { base: number; allUsable: boolean }\n /** 목차 항목(채움 탭 + 쪽 번호 줄) 글 — 표 칸 안 목차는 틀 표 풀기가 문단을 새로 만들어 블록 표시가 사라지므로 글로도 기억한다 (#121) */\n tocTexts: Set<string>\n}\n\nexport function createSectionShared(): SectionShared {\n return {\n numState: new Map(),\n pageText: { headers: [], footers: [] },\n track: { deleteDepth: 0, warned: false, deletedObjects: new WeakSet(), deletedImageRefs: new Set() },\n pageState: { base: 0, allUsable: true },\n tocTexts: new Set(),\n }\n}\n\n/** walk 함수들이 공유하는 파싱 컨텍스트 — 개별 optional 파라미터를 하나로 묶어 시그니처 안정화 */\nexport interface WalkCtx {\n styleMap?: HwpxStyleMap\n warnings?: ParseWarning[]\n sectionNum?: number\n shared: SectionShared\n /** secPr outlineShapeIDRef — 개요(OUTLINE) 문단이 사용하는 numbering id */\n outlineNumId?: string\n /** secPr footNotePr/endNotePr 번호 모양 — 각주·미주 본문 참조 부호 재구성 (notes.ts) */\n noteFormats?: { footnote?: NoteNumberFormat; endnote?: NoteNumberFormat }\n /** 현재 페이지 (#66) — top-level 문단 진입 시 paraPage로 갱신, 셀/중첩은 호스트 상속 */\n page?: number\n /** 섹션 프리패스 결과: top-level <hp:p> → 섹션 내 0-based 페이지 */\n paraPage?: Map<Element, number>\n /** 이전 섹션까지 누적 페이지 수 (전역 페이지 = pageBase + 섹션 내 페이지 + 1) */\n pageBase?: number\n}\n\n/** xmldom DOMParser 생성 — onError 콜백으로 malformed XML 경고 수집 */\nexport function createXmlParser(warnings?: ParseWarning[]): DOMParser {\n return new DOMParser({\n onError(level: \"warning\" | \"error\" | \"fatalError\", msg: string) {\n if (level === \"fatalError\") throw new KordocError(`XML 파싱 실패: ${msg}`)\n warnings?.push({ code: \"MALFORMED_XML\", message: `XML ${level === \"warning\" ? \"경고\" : \"오류\"}: ${msg}` })\n },\n })\n}\n\n/** 수집된 머리말/꼬리말을 본문 앞/뒤 문단으로 배치 */\nexport function applyPageText(blocks: IRBlock[], shared: SectionShared): void {\n const { headers, footers } = shared.pageText\n if (headers.length > 0) {\n blocks.unshift(...headers.map(t => ({ type: \"paragraph\" as const, text: t, pageNumber: 1 })))\n }\n if (footers.length > 0) {\n blocks.push(...footers.map(t => ({ type: \"paragraph\" as const, text: t })))\n }\n}\n\n","/**\n * XML DOM 공용 헬퍼 — hwpx(parser-shared)·hwpml(parser)·docx(equation)가 공유.\n *\n * xmldom Element를 네임스페이스 접두사 무시(localName 기준)로 다루는 최소 유틸.\n * 정본은 이 모듈 — hwpx/parser-shared.ts는 하위호환 재수출만 유지한다.\n */\n\n/** XML DOM 재귀 최대 깊이 — 악성 파일의 스택 오버플로 방지.\n * 좌표계가 다른 hwp5 MAX_NEST_DEPTH(8, 표 중첩 단계)·filler/소스맵 16\n * (표 중첩 단계)과 달리 이건 \"XML 요소\" 깊이라 표 1단이 여러 depth를\n * 소모한다 — 상수 통일 금지 (의미가 다름) */\nexport const MAX_XML_DEPTH = 200\n\n/** 요소의 localName (네임스페이스 접두사 제거) */\nexport function localName(el: Element): string {\n return (el.tagName || el.localName || \"\").replace(/^[^:]+:/, \"\")\n}\n\n/** 자식 중 지정된 localName(접두사 제거)을 가진 첫 번째 Element 반환 */\nexport function findChildByLocalName(parent: Element, name: string): Element | null {\n const children = parent.childNodes\n if (!children) return null\n for (let i = 0; i < children.length; i++) {\n const ch = children[i] as Element\n if (ch.nodeType !== 1) continue\n if (localName(ch) === name) return ch\n }\n return null\n}\n\n/** 자식 중 지정된 localName을 가진 모든 Element (직계만) */\nexport function childrenByLocalName(parent: Element, name: string): Element[] {\n const out: Element[] = []\n const children = parent.childNodes\n if (!children) return out\n for (let i = 0; i < children.length; i++) {\n const ch = children[i] as Element\n if (ch.nodeType !== 1) continue\n if (localName(ch) === name) out.push(ch)\n }\n return out\n}\n\n/** 직계 자식 Element 전부 (텍스트/주석 노드 제외) */\nexport function elementChildren(parent: Element): Element[] {\n const out: Element[] = []\n const children = parent.childNodes\n if (!children) return out\n for (let i = 0; i < children.length; i++) {\n if (children[i].nodeType === 1) out.push(children[i] as Element)\n }\n return out\n}\n\n/** 텍스트를 담는 노드 타입 — TEXT_NODE(3)과 CDATA_SECTION_NODE(4).\n * CDATA 를 빠뜨리면 `<hp:t><![CDATA[본문]]></hp:t>` 로 저장된 문서의 텍스트가\n * 경고 없이 통째로 사라진다 (rhwp f4c7de75 와 같은 결함). 한컴 저장본은 CDATA 를\n * 쓰지 않지만 제3자 도구 산출물·특수문자 다수 문단에서는 유효한 XML 표현이다. */\nfunction isTextNode(node: Node): boolean {\n return node.nodeType === 3 || node.nodeType === 4\n}\n\n/** 노드 내 모든 텍스트를 재귀적으로 추출 (요소 단위 trim — hwpx 기존 시맨틱).\n * MAX_XML_DEPTH 가드 — 악성 심층 XML 스택 오버플로 방지 */\nexport function extractTextFromNode(node: Node, depth: number = 0): string {\n let result = \"\"\n if (depth > MAX_XML_DEPTH) return result\n const children = node.childNodes\n if (!children) return result\n for (let i = 0; i < children.length; i++) {\n const child = children[i]\n if (isTextNode(child)) result += child.textContent || \"\"\n else if (child.nodeType === 1) result += extractTextFromNode(child, depth + 1)\n }\n return result.trim()\n}\n\n/** 노드 내 모든 텍스트를 공백 원형 그대로 추출 (trim 없음 — hwpml CHAR 텍스트 시맨틱) */\nexport function rawTextContent(node: Node, depth: number = 0): string {\n if (depth > MAX_XML_DEPTH) return \"\"\n const children = node.childNodes\n if (!children) return \"\"\n const parts: string[] = []\n for (let i = 0; i < children.length; i++) {\n const child = children[i]\n if (isTextNode(child)) parts.push(child.nodeValue || \"\")\n else if (child.nodeType === 1) parts.push(rawTextContent(child, depth + 1))\n }\n return parts.join(\"\")\n}\n","/**\n * 레이아웃 보존 렌더 — header.xml 스타일 테이블 (charPr/paraPr/borderFill).\n * 파서 IR과 무관하게 렌더에 필요한 속성만 추출한다.\n */\n\nimport { createXmlParser, findChildByLocalName } from \"../hwpx/parser-shared.js\"\n\n/** 언어 칸(영문·한자·일어·기호) 하나의 글꼴·장평·자간·상대크기 */\nexport interface RenderLangStyle { face?: string; ratio: number; spacing: number; rel: number; hft?: boolean }\n\nexport interface RenderCharStyle {\n /** 글자 크기 (1/100pt — 1000 = 10pt) */\n height: number\n bold: boolean\n italic: boolean\n underline: boolean\n /** #RRGGBB — 검정이면 undefined */\n color?: string\n /** 장평 % */\n ratio: number\n /** 자간 % */\n spacing: number\n /** CSS font-family 스택 — charPr fontRef(hangul)에서 해석. 없으면 root 기본 폴백 */\n fontFamily?: string\n /** HWP 원본 글꼴명 (fontfaces face) — reflow 폭 테이블 선택용 */\n face?: string\n /** 한글 상대크기 % (relSz hangul) — reflow 폭. 100 이면 생략 */\n rel?: number\n /** 영문 글자(한컴 언어 분류 '영문')의 글꼴·장평·자간·상대크기 — reflow 폭. 한글과 같으면 생략 */\n latin?: { face?: string; ratio: number; spacing: number; rel: number; hft?: boolean }\n /** 한자·일어·기호 글자(한컴 언어 분류)의 글꼴·장평·자간·상대크기 — reflow 폭. 한글과 같으면 생략 */\n hanja?: RenderLangStyle\n japanese?: RenderLangStyle\n symbol?: RenderLangStyle\n /** 한글 글꼴 항목이 한컴 HFT 글꼴(fontface font type=\"HFT\") — 같은 이름의 TTF 와 조판이 다를 수 있다 */\n hft?: boolean\n /** 빈칸을 글꼴의 빈칸 폭으로 잰다(charPr useFontSpace=\"1\") — 아니면 반각 */\n fontSpace?: boolean\n /** 위/아래 첨자 — 한컴은 글자 폭을 크기 16/25 로 잰다 */\n script?: \"sup\" | \"sub\"\n /** 글자 음영(charPr shadeColor) #RRGGBB — 없음·흰색은 생략 */\n shade?: string\n /** 글자 테두리·배경(charPr borderFillIDRef) — 테두리 위·아래 굵기만큼 줄이 높아진다 */\n borderFill?: string\n /** 한글 2024 이전 판으로 저장된 문서(version.xml appVersion 13 미만) — 조판 규칙 일부가 2024 와 다르다 */\n before2024?: boolean\n /** 큰따옴표 “ ” 를 반각으로 재는 새 빌드 저장본(한글 2022 4605·2024 3901 이상, hwpxQuoteHalf) */\n halfQuote?: boolean\n}\n\nexport type ParaAlign = \"JUSTIFY\" | \"LEFT\" | \"RIGHT\" | \"CENTER\" | \"DISTRIBUTE\" | \"DISTRIBUTE_SPACE\"\n\n/** reflow(Tier-2)용 문단 기하 — 줄간격·여백. paraPr `<hh:margin>`·`<hh:lineSpacing>`은\n * `<hp:switch>/<hp:case>` 안(손자)이라 재귀 탐색으로 뽑는다. 단위 HWPUNIT. */\nexport interface RenderParaGeom {\n /** PERCENT(기본 160=160%) / FIXED / BETWEEN_LINES / AT_LEAST */\n lineSpacingType: string\n lineSpacingValue: number\n /** 왼쪽 들여쓰기(HWPUNIT) */\n marginLeft: number\n marginRight: number\n /** 첫 줄 들여쓰기/내어쓰기 — 음수=둘째 줄부터 더 들어감(hanging) */\n marginIntent: number\n /** 문단 위 간격(prev) */\n spaceBefore: number\n /** 문단 아래 간격(next) */\n spaceAfter: number\n /** breakSetting의 한글 줄나눔 — 'keep'(어절)/'charAll'(글자). 없으면 호출자 모드.\n * 주의: 저장 속성 의미는 이름과 반대 — breakNonLatinWord \"BREAK_WORD\"=어절,\n * \"KEEP_WORD\"=글자 (한글 COM 실렌더 실측) */\n wrapMode?: \"keep\" | \"charAll\"\n /** 탭 정의(tabPrIDRef)가 내어쓰기용 자동 탭(autoTabLeft) — 첫 줄 탭이 내어쓰기 위치에 선다 */\n autoTabLeft?: boolean\n /** 공백 최소값(paraPr condense, %) — 줄 안 공백이 그만큼 줄어들 수 있어 줄 가용폭이 공백 폭 합 × condense% 늘어난다 */\n condense?: number\n /** 영문 단어를 글자 단위로 끊는다(breakLatinWord=\"BREAK_WORD\" — 한글 설정과 달리 이름 그대로. 한컴 저장본 좁은 칸 \"Unit P|rice\") */\n latinBreak?: boolean\n /** 한 줄로 입력(breakSetting lineWrap=\"SQUEEZE\") — 글이 넘쳐도 줄을 바꾸지 않는다(명시적 줄 나눔만) */\n squeeze?: boolean\n /** 외톨이줄 보호(breakSetting widowOrphan) — 쪽 끝에 첫 줄만 남기거나 다음 쪽에 마지막 줄만 넘기지 않는다 */\n widowOrphan?: boolean\n /** 문단 보호(keepLines) — 쪽에 걸치면 문단째 다음 쪽으로 / 다음 문단과 함께(keepWithNext) / 문단 앞에서 쪽 나눔(pageBreakBefore) */\n keepLines?: boolean\n keepWithNext?: boolean\n pageBreakBefore?: boolean\n /** 문단 세로 정렬(align vertical) — 줄 기준선: BASELINE 은 높이의 85%, CENTER 는 50%(한컴 저장 lineseg 실측) */\n vertAlign?: string\n /** 글꼴에 어울리는 줄 높이(paraPr fontLineHeight=\"1\") — 글이 있는 줄의 높이·기준선을 글꼴의 위·아래 높이로 정한다 */\n fontLineHeight?: boolean\n /** 글머리표(heading BULLET) — 첫 줄 앞에 글머리표 글자와 간격(textOffset)이 놓인다. autoIndent 면 둘째 줄부터도 그만큼 들어간다.\n * lineseg horzpos·horzsize 는 바뀌지 않는다(한컴 저장 lineseg 실측: ◦ 1100 + 50% → 1650 만큼 좁게 채움) */\n bullet?: { ch: string; charPrId?: string; offset: number; offsetPct: boolean; autoIndent: boolean; useInstWidth?: boolean }\n /** 문단 번호(heading NUMBER) — 번호 정의 id·수준(1부터)과 그 수준의 머리 형식(`^1.` 의 ^N 자리에 N 수준 번호). 앞머리 폭은 글머리표와 같다 */\n number?: { id: string; level: number; heads: Map<number, NumberHead>; charPrId?: string; offset: number; offsetPct: boolean; autoIndent: boolean }\n}\n\n/** 번호 정의 한 수준(hh:paraHead) */\nexport interface NumberHead { text: string; format: string; start: number }\n\nexport const DEFAULT_PARA_GEOM: RenderParaGeom = {\n lineSpacingType: \"PERCENT\", lineSpacingValue: 160,\n marginLeft: 0, marginRight: 0, marginIntent: 0, spaceBefore: 0, spaceAfter: 0,\n}\n\nexport interface RenderBorderEdge {\n /** SOLID/DASH/DOT… — NONE은 edge 자체를 생략 */\n type: string\n /** pt 단위 굵기 */\n widthPt: number\n color: string\n}\n\nexport interface RenderBorderFill {\n left?: RenderBorderEdge\n right?: RenderBorderEdge\n top?: RenderBorderEdge\n bottom?: RenderBorderEdge\n /** 배경색 #RRGGBB (없으면 undefined) */\n fill?: string\n}\n\nexport interface RenderStyles {\n charPr: Map<string, RenderCharStyle>\n paraAlign: Map<string, ParaAlign>\n /** reflow용 문단 기하 (줄간격·여백) — id별 */\n paraGeom: Map<string, RenderParaGeom>\n borderFill: Map<string, RenderBorderFill>\n /** \"쪽 번호\"(Page Number) 스타일의 글자 모양 id — 한컴은 쪽 번호를 이 글꼴로 찍는다 */\n pageNumCharPr?: string\n /** 한글 2024 이전 판으로 저장된 문서 — 판을 모르면(생성 문서 등) 2024 규칙 */\n before2024?: boolean\n /** 한글 2024 초기 빌드(3622 미만) 저장본 — 번호·글머리표 문단도 공백 압축(condense)으로 채운다 */\n early2024?: boolean\n}\n\n/** version.xml 의 저장 빌드(appVersion 넷째 수). 없거나 못 읽으면 undefined */\nexport function hwpxAppBuild(versionXml: string | null | undefined): number | undefined {\n const m = versionXml?.match(/appVersion=\"\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*(\\d+)/)\n return m ? Number(m[1]) : undefined\n}\n\n/** version.xml 의 저장 판 번호(appVersion 첫 수 — 12 = 한글 2022, 13 = 2024). 없거나 못 읽으면 undefined */\nexport function hwpxAppMajor(versionXml: string | null | undefined): number | undefined {\n const m = versionXml?.match(/appVersion=\"\\s*(\\d+)/)\n return m ? Number(m[1]) : undefined\n}\n\n/** 문서에서 온 색 값 — #RRGGBB 만 통과(나머지는 fallback). SVG 속성에 그대로 들어가므로 url(…) 같은 임의 값을 막는다 */\nexport function docColor(v: string | null | undefined, fallback?: string): string | undefined {\n return v != null && /^#[0-9A-Fa-f]{6}$/.test(v) ? v : fallback\n}\n\nexport const DEFAULT_CHAR: RenderCharStyle = { height: 1000, bold: false, italic: false, underline: false, ratio: 100, spacing: 0 }\n\n// ─── 글꼴 매핑 (HWP fontfaces face → CSS font-family 스택) ──────────────\n//\n// HWPX charPr 는 <hh:fontRef hangul=\"N\">으로 fontfaces 테이블의 실제 글꼴명을 가리킨다.\n// 렌더는 이 글꼴명을 SVG <text font-family>로 내보내 뷰어(WebView2/브라우저)가 원본과\n// 같은 글꼴로 그리게 한다. 미설치 글꼴은 계열(명조=serif / 고딕=sans) 폴백으로 수렴시켜\n// 최소한 획 계열(바탕↔돋움)은 원본과 일치시킨다 — 공문서 제목 고딕이 바탕체로 나오던 회귀 해소.\n\n/** 자주 쓰는 글꼴의 별칭 스택 (한글명↔영문 시스템명 병기 — 뷰어 OS별 등록명 차이 흡수) */\nconst FONT_ALIASES: Record<string, string> = {\n \"함초롬바탕\": \"'HCR Batang','함초롬바탕','한컴바탕'\",\n \"한컴바탕\": \"'HCR Batang','함초롬바탕','한컴바탕'\",\n \"함초롬돋움\": \"'HCR Dotum','함초롬돋움','한컴돋움'\",\n \"한컴돋움\": \"'HCR Dotum','함초롬돋움','한컴돋움'\",\n \"맑은 고딕\": \"'Malgun Gothic','맑은 고딕'\",\n \"맑은고딕\": \"'Malgun Gothic','맑은 고딕'\",\n \"굴림\": \"'Gulim','굴림'\",\n \"굴림체\": \"'GulimChe','굴림체','Gulim'\",\n \"돋움\": \"'Dotum','돋움'\",\n \"돋움체\": \"'DotumChe','돋움체','Dotum'\",\n \"바탕\": \"'Batang','바탕'\",\n \"바탕체\": \"'BatangChe','바탕체','Batang'\",\n \"궁서\": \"'Gungsuh','궁서'\",\n \"궁서체\": \"'GungsuhChe','궁서체','Gungsuh'\",\n \"나눔고딕\": \"'NanumGothic','나눔고딕'\",\n \"나눔명조\": \"'NanumMyeongjo','나눔명조'\",\n \"맑은 고딕 Semilight\": \"'Malgun Gothic Semilight','맑은 고딕'\",\n}\n\n/** 명조/바탕 계열(serif) 여부 — 아니면 고딕/돋움(sans)으로 본다 */\nfunction isSerifFace(face: string): boolean {\n return /바탕|명조|Batang|Myeong|Mincho|궁서|Gungsuh|Serif|신명|순명|Song|송/i.test(face)\n}\n\n/** CSS font-family 토큰 인용 — 영숫자·하이픈만이면 무인용, 그 외(공백·한글)는 작은따옴표 */\nfunction cssQuote(name: string): string {\n return /^[A-Za-z0-9_-]+$/.test(name) ? name : `'${name.replace(/['\"\\\\]/g, \"\")}'`\n}\n\n/**\n * HWP 글꼴명 → CSS font-family 스택. 정확한 글꼴 먼저, 이어서 계열 폴백(명조=serif /\n * 고딕=sans, 각 한국어 렌더 가능 글꼴 포함)을 붙여 미설치 시에도 획 계열을 보존한다.\n */\nexport function hwpFaceToCssStack(face: string | null | undefined): string {\n const trimmed = (face ?? \"\").trim()\n if (!trimmed) return \"\"\n const generic = isSerifFace(trimmed)\n ? \"'HCR Batang','Batang','Noto Serif KR',serif\"\n : \"'Malgun Gothic','HCR Dotum','Noto Sans KR',sans-serif\"\n const head = FONT_ALIASES[trimmed] ?? cssQuote(trimmed)\n return `${head},${generic}`\n}\n\n/** fontfaces 테이블에서 HANGUL 그룹의 font id → 글꼴명 맵 (없으면 첫 그룹) */\nfunction collectHangulFonts(root: Element, lang = \"HANGUL\", hftIds?: Set<string>, substKoPub = false): Map<string, string> {\n const map = new Map<string, string>()\n const findFaces = (el: Element, depth: number): Element | null => {\n if (depth > 24) return null\n for (const ch of Array.from(el.childNodes)) {\n if (ch.nodeType !== 1) continue\n const e = ch as Element\n if ((e.tagName || \"\").replace(/^[^:]+:/, \"\") === \"fontfaces\") return e\n const f = findFaces(e, depth + 1)\n if (f) return f\n }\n return null\n }\n const faces = findFaces(root, 0)\n if (!faces) return map\n let group: Element | null = null\n let firstGroup: Element | null = null\n for (const ch of Array.from(faces.childNodes)) {\n if (ch.nodeType !== 1) continue\n const e = ch as Element\n if ((e.tagName || \"\").replace(/^[^:]+:/, \"\") !== \"fontface\") continue\n if (!firstGroup) firstGroup = e\n if ((e.getAttribute(\"lang\") ?? \"\").toUpperCase() === lang) { group = e; break }\n }\n group = group ?? firstGroup\n if (!group) return map\n for (const ch of Array.from(group.childNodes)) {\n if (ch.nodeType !== 1) continue\n const e = ch as Element\n if ((e.tagName || \"\").replace(/^[^:]+:/, \"\") !== \"font\") continue\n const id = e.getAttribute(\"id\")\n let face = e.getAttribute(\"face\")\n let type = e.getAttribute(\"type\")\n // 한양 글꼴은 HFT 로만 있다 — 글꼴 항목이 TTF 면 한컴은 그 이름의 TTF 를 못 찾아 대체 글꼴(substFont)로 조판한다\n // (한컴 저장 lineseg: 한양중고딕 TTF → HY중고딕·한컴바탕 등. 두 줄 이상 문단 줄 시작 +53)\n // KoPub 글꼴이 없는 PC 에서 작성·인쇄한 문서는 대체 글꼴로 조판됐다 — 렌더가 저장 줄 나눔으로 판정해 substKoPub 를 켠다(kopubSubstituted)\n if (face && type === \"TTF\" && (/^한양/.test(face) || (substKoPub && /^KoPub/.test(face)))) {\n const sub = Array.from(e.childNodes).find(c => c.nodeType === 1 && ((c as Element).tagName || \"\").replace(/^[^:]+:/, \"\") === \"substFont\") as Element | undefined\n const sf = sub?.getAttribute(\"face\")\n if (sf) { face = sf; type = sub!.getAttribute(\"type\") }\n }\n if (id != null && face) map.set(id, face)\n if (id != null && hftIds && type === \"HFT\") hftIds.add(id)\n }\n return map\n}\n\n/** \"0.12 mm\" / \"0.5 mm\" → pt */\nfunction borderWidthPt(v: string | null | undefined): number {\n const n = parseFloat(v ?? \"\")\n if (!Number.isFinite(n)) return 0.34\n return n * 2.834645 // mm → pt\n}\n\nfunction parseEdge(el: Element | null): RenderBorderEdge | undefined {\n if (!el) return undefined\n const type = el.getAttribute(\"type\") ?? \"NONE\"\n if (type === \"NONE\") return undefined\n return { type, widthPt: borderWidthPt(el.getAttribute(\"width\")), color: docColor(el.getAttribute(\"color\"), \"#000000\")! }\n}\n\n/** 서브트리에서 localName이 일치하는 첫 요소를 재귀 탐색 (switch/case 래핑 대응) */\nfunction findDeep(el: Element, name: string, depth = 0): Element | null {\n if (depth > 32) return null\n const children = el.childNodes\n if (!children) return null\n for (let i = 0; i < children.length; i++) {\n const ch = children[i]\n if (ch.nodeType !== 1) continue\n const e = ch as Element\n if ((e.tagName || \"\").replace(/^[^:]+:/, \"\") === name) return e\n const found = findDeep(e, name, depth + 1)\n if (found) return found\n }\n return null\n}\n\n/** paraPr 요소 → 문단 기하 (margin·lineSpacing은 hp:switch/case 안이라 재귀 탐색) */\nfunction parseParaGeom(el: Element, oldUnit = false): RenderParaGeom {\n const g: RenderParaGeom = { ...DEFAULT_PARA_GEOM }\n const condense = Number(el.getAttribute(\"condense\")) || 0\n if (condense > 0) g.condense = condense\n if (el.getAttribute(\"fontLineHeight\") === \"1\") g.fontLineHeight = true\n const ls = findDeep(el, \"lineSpacing\")\n if (ls) {\n g.lineSpacingType = ls.getAttribute(\"type\") ?? \"PERCENT\"\n // 0% 도 저장값 그대로(줄 간격 몫 = −글자 높이) — `|| 160` 이면 0 이 160% 로 바뀐다\n const lv = Number(ls.getAttribute(\"value\"))\n g.lineSpacingValue = Number.isFinite(lv) && ls.getAttribute(\"value\") !== null ? lv : 160\n }\n const margin = findDeep(el, \"margin\")\n if (margin) {\n // head version 1.4 미만 문서에서 hp:switch 밖에 바로 놓인 margin 은 옛 단위(HWPUNIT 의 2배) — 한컴은 절반으로 조판한다\n // (한컴 저장 lineseg: 1.2·1.3 문서의 그런 문단 첫 줄 horzpos = left/2 가 262개 중 243개. hp:case 안·1.4 이상(생성기 포함)은 값 그대로)\n const plain = oldUnit && ((margin.parentNode as Element | null)?.tagName ?? \"\").replace(/^[^:]+:/, \"\") === \"paraPr\"\n const v = (n: string): number => {\n const c = findDeep(margin, n)\n // unit=\"CHAR\" 값은 5배가 HWPUNIT 이다(한컴 저장 lineseg: 왼쪽 400 → horzpos 2000, 100 → 500, 위 간격 53 → 첫 줄 265 — 글자 크기 1000~1200 모두)\n const x = c ? (Number(c.getAttribute(\"value\")) || 0) * (c.getAttribute(\"unit\") === \"CHAR\" ? 5 : 1) : 0\n return plain ? Math.trunc(x / 2) : x\n }\n g.marginLeft = v(\"left\")\n g.marginRight = v(\"right\")\n g.marginIntent = v(\"intent\")\n g.spaceBefore = v(\"prev\")\n g.spaceAfter = v(\"next\")\n }\n const va = findDeep(el, \"align\")?.getAttribute(\"vertical\")\n if (va && va !== \"BASELINE\") g.vertAlign = va\n const bs = findDeep(el, \"breakSetting\")\n if (bs) {\n // 속성 의미 역전 주의(위 RenderParaGeom.wrapMode 주석): BREAK_WORD=어절, KEEP_WORD=글자\n if (bs.getAttribute(\"breakLatinWord\") === \"BREAK_WORD\") g.latinBreak = true\n if (bs.getAttribute(\"lineWrap\") === \"SQUEEZE\") g.squeeze = true\n if (bs.getAttribute(\"widowOrphan\") === \"1\") g.widowOrphan = true\n if (bs.getAttribute(\"keepLines\") === \"1\") g.keepLines = true\n if (bs.getAttribute(\"keepWithNext\") === \"1\") g.keepWithNext = true\n if (bs.getAttribute(\"pageBreakBefore\") === \"1\") g.pageBreakBefore = true\n const nl = bs.getAttribute(\"breakNonLatinWord\")\n if (nl === \"BREAK_WORD\") g.wrapMode = \"keep\"\n else if (nl === \"KEEP_WORD\") g.wrapMode = \"charAll\"\n }\n return g\n}\n\n/** header.xml(구버전 head.xml) → 렌더용 스타일 맵. appMajor = 저장 판(hwpxAppMajor) */\nexport function parseRenderStyles(headXml: string, opts?: { appMajor?: number; appBuild?: number; substKoPub?: boolean }): RenderStyles {\n const before2024 = opts?.appMajor !== undefined && opts.appMajor > 0 && opts.appMajor < 13\n // 새 빌드는 휴먼명조 “ ” 를 반각으로 잰다 — 한컴 저장본: 2024 3901·2022 4605 문단은 반각이라야 줄 나눔이 맞고, 2024 3050·2022 4426 이하는 전각\n const major = opts?.appMajor ?? 0, build = opts?.appBuild ?? 0\n const halfQuote = (major === 12 && build >= 4605) || (major === 13 && build >= 3901) || major > 13\n // 2024 초기 빌드는 번호·글머리표 문단도 공백을 압축해 채운다 — 한컴 저장본: 2024 3050 글머리표 부제(압축 50%)가 공백을 줄여 한 줄,\n // 3622·3901 저장본은 압축 없이 채워야 줄 나눔이 맞다(그 사이 빌드는 표본 없음)\n const early2024 = major === 13 && build > 0 && build < 3622\n const styles: RenderStyles = { charPr: new Map(), paraAlign: new Map(), paraGeom: new Map(), borderFill: new Map(), ...(before2024 ? { before2024 } : {}), ...(early2024 ? { early2024 } : {}) }\n const doc = createXmlParser().parseFromString(headXml, \"text/xml\")\n const root = doc.documentElement as unknown as Element | null\n if (!root) return styles\n\n const hv = Number.parseFloat(root.getAttribute(\"version\") ?? \"\")\n const oldUnit = Number.isFinite(hv) && hv < 1.4\n // fontfaces(HANGUL) 선파싱 — charPr fontRef 해석에 쓴다\n const hftHangul = new Set<string>(), hftLatin = new Set<string>()\n const sk = !!opts?.substKoPub\n const hangulFonts = collectHangulFonts(root, \"HANGUL\", hftHangul, sk)\n const latinFonts = collectHangulFonts(root, \"LATIN\", hftLatin, sk)\n const hftHanja = new Set<string>(), hftJapanese = new Set<string>(), hftSymbol = new Set<string>()\n const langFonts = { hanja: [collectHangulFonts(root, \"HANJA\", hftHanja, sk), hftHanja], japanese: [collectHangulFonts(root, \"JAPANESE\", hftJapanese, sk), hftJapanese], symbol: [collectHangulFonts(root, \"SYMBOL\", hftSymbol, sk), hftSymbol] } as const\n // 탭 정의 — 문단의 tabPrIDRef 는 워크가 끝난 뒤 해석(순서 무관)\n const autoTabPr = new Set<string>()\n const paraTabRef = new Map<string, string>()\n const paraBullet = new Map<string, string>()\n const bullets = new Map<string, NonNullable<RenderParaGeom[\"bullet\"]>>()\n const paraNumber = new Map<string, { id: string; level: number }>()\n /** 번호 정의 id → 수준 → { 머리, 앞머리 속성 } */\n const numberings = new Map<string, Map<number, { head: NumberHead; charPrId?: string; offset: number; offsetPct: boolean; autoIndent: boolean }>>()\n\n const walk = (el: Element): void => {\n const tag = (el.tagName || \"\").replace(/^[^:]+:/, \"\")\n if (tag === \"charPr\") {\n const id = el.getAttribute(\"id\")\n if (id != null) {\n const ratioEl = findChildByLocalName(el, \"ratio\")\n const spacingEl = findChildByLocalName(el, \"spacing\")\n const underlineEl = findChildByLocalName(el, \"underline\")\n const textColor = el.getAttribute(\"textColor\")\n const shadeColor = el.getAttribute(\"shadeColor\")\n const fontRef = findChildByLocalName(el, \"fontRef\")\n const fontId = fontRef?.getAttribute(\"hangul\") ?? fontRef?.getAttribute(\"latin\")\n const face = fontId != null ? hangulFonts.get(fontId) : undefined\n const relEl = findChildByLocalName(el, \"relSz\")\n const num = (e: Element | null, a: string, d: number): number => { const v = e?.getAttribute(a); return v == null || v === \"\" ? d : Number(v) }\n const latinFace = latinFonts.get(fontRef?.getAttribute(\"latin\") ?? \"\")\n const latinHft = hftLatin.has(fontRef?.getAttribute(\"latin\") ?? \"\")\n const latin = { face: latinFace ?? face, ratio: num(ratioEl, \"latin\", 100), spacing: num(spacingEl, \"latin\", 0), rel: num(relEl, \"latin\", 100), ...(latinHft ? { hft: true } : {}) }\n const rel = num(relEl, \"hangul\", 100)\n const hangulRatio = Number(ratioEl?.getAttribute(\"hangul\")) || 100, hangulSpacing = Number(spacingEl?.getAttribute(\"hangul\")) || 0, hangulHft = hftHangul.has(fontRef?.getAttribute(\"hangul\") ?? \"\")\n // 한자·일어·기호 칸 — 한글 칸과 다를 때만(글꼴 칸이 목록에 없으면 한글 칸 글꼴)\n const langs: Partial<Record<\"hanja\" | \"japanese\" | \"symbol\", RenderLangStyle>> = {}\n for (const lang of [\"hanja\", \"japanese\", \"symbol\"] as const) {\n const ref = fontRef?.getAttribute(lang) ?? \"\", [fonts, hfts] = langFonts[lang]\n const ls: RenderLangStyle = { face: fonts.get(ref) ?? face, ratio: num(ratioEl, lang, 100), spacing: num(spacingEl, lang, 0), rel: num(relEl, lang, 100), ...((fonts.has(ref) ? hfts.has(ref) : hangulHft) ? { hft: true } : {}) }\n if (ls.face !== face || ls.ratio !== hangulRatio || ls.spacing !== hangulSpacing || ls.rel !== rel || !!ls.hft !== hangulHft) langs[lang] = ls\n }\n styles.charPr.set(id, {\n height: Number(el.getAttribute(\"height\")) || 1000,\n bold: findChildByLocalName(el, \"bold\") != null,\n italic: findChildByLocalName(el, \"italic\") != null,\n underline: underlineEl != null && (underlineEl.getAttribute(\"type\") ?? \"NONE\") !== \"NONE\",\n color: textColor !== \"#000000\" ? docColor(textColor) : undefined,\n ratio: Number(ratioEl?.getAttribute(\"hangul\")) || 100,\n spacing: Number(spacingEl?.getAttribute(\"hangul\")) || 0,\n fontFamily: face ? hwpFaceToCssStack(face) : undefined,\n face,\n ...(rel !== 100 ? { rel } : {}),\n // 흰 음영은 흰 바탕에서 보이지 않고 색 칸 위에서만 흰 상자가 된다 — 확인 전이라 그리지 않는다\n ...(shadeColor && /^#[0-9A-Fa-f]{6}$/.test(shadeColor) && shadeColor.toUpperCase() !== \"#FFFFFF\" ? { shade: shadeColor } : {}),\n ...(el.getAttribute(\"borderFillIDRef\") ? { borderFill: el.getAttribute(\"borderFillIDRef\")! } : {}),\n ...(hftHangul.has(fontRef?.getAttribute(\"hangul\") ?? \"\") ? { hft: true } : {}),\n ...(latin.face !== face || latin.ratio !== (Number(ratioEl?.getAttribute(\"hangul\")) || 100) || latin.spacing !== (Number(spacingEl?.getAttribute(\"hangul\")) || 0) || latin.rel !== rel ? { latin } : {}),\n ...langs,\n ...(el.getAttribute(\"useFontSpace\") === \"1\" ? { fontSpace: true } : {}),\n ...(findChildByLocalName(el, \"supscript\") ? { script: \"sup\" as const } : findChildByLocalName(el, \"subscript\") ? { script: \"sub\" as const } : {}),\n })\n }\n } else if (tag === \"tabPr\") {\n const id = el.getAttribute(\"id\")\n if (id != null && el.getAttribute(\"autoTabLeft\") === \"1\") autoTabPr.add(id)\n } else if (tag === \"paraPr\") {\n const id = el.getAttribute(\"id\")\n if (id != null) {\n const align = findChildByLocalName(el, \"align\")\n styles.paraAlign.set(id, (align?.getAttribute(\"horizontal\") as ParaAlign) || \"JUSTIFY\")\n styles.paraGeom.set(id, parseParaGeom(el, oldUnit))\n const tab = el.getAttribute(\"tabPrIDRef\")\n if (tab != null) paraTabRef.set(id, tab)\n const heading = findDeep(el, \"heading\")\n if (heading?.getAttribute(\"type\") === \"BULLET\") paraBullet.set(id, heading.getAttribute(\"idRef\") ?? \"\")\n else if (heading?.getAttribute(\"type\") === \"NUMBER\") paraNumber.set(id, { id: heading.getAttribute(\"idRef\") ?? \"\", level: (Number(heading.getAttribute(\"level\")) || 0) + 1 })\n }\n } else if (tag === \"bullet\") {\n const id = el.getAttribute(\"id\"), head = findChildByLocalName(el, \"paraHead\")\n if (id != null && el.getAttribute(\"useImage\") !== \"1\") {\n const cpr = head?.getAttribute(\"charPrIDRef\")\n bullets.set(id, {\n ch: el.getAttribute(\"char\") ?? \"\",\n ...(cpr != null && cpr !== \"4294967295\" ? { charPrId: cpr } : {}),\n offset: Number(head?.getAttribute(\"textOffset\")) || 0,\n offsetPct: (head?.getAttribute(\"textOffsetType\") ?? \"PERCENT\") === \"PERCENT\",\n autoIndent: head?.getAttribute(\"autoIndent\") !== \"0\",\n ...(head?.hasAttribute(\"useInstWidth\") ? { useInstWidth: head.getAttribute(\"useInstWidth\") !== \"0\" } : {}),\n })\n }\n } else if (tag === \"numbering\") {\n const id = el.getAttribute(\"id\")\n if (id != null) {\n const levels = new Map<number, { head: NumberHead; charPrId?: string; offset: number; offsetPct: boolean; autoIndent: boolean }>()\n for (let i = 0; i < el.childNodes.length; i++) {\n const h = el.childNodes[i] as Element\n if (h.nodeType !== 1 || (h.tagName || \"\").replace(/^[^:]+:/, \"\") !== \"paraHead\") continue\n const cpr = h.getAttribute(\"charPrIDRef\")\n levels.set(Number(h.getAttribute(\"level\")) || 1, {\n head: { text: h.textContent ?? \"\", format: h.getAttribute(\"numFormat\") ?? \"DIGIT\", start: Number(h.getAttribute(\"start\") ?? 1) },\n ...(cpr != null && cpr !== \"4294967295\" ? { charPrId: cpr } : {}),\n offset: Number(h.getAttribute(\"textOffset\")) || 0,\n offsetPct: (h.getAttribute(\"textOffsetType\") ?? \"PERCENT\") === \"PERCENT\",\n autoIndent: h.getAttribute(\"autoIndent\") !== \"0\",\n })\n }\n numberings.set(id, levels)\n }\n } else if (tag === \"borderFill\") {\n const id = el.getAttribute(\"id\")\n if (id != null) {\n const bf: RenderBorderFill = {\n left: parseEdge(findChildByLocalName(el, \"leftBorder\")),\n right: parseEdge(findChildByLocalName(el, \"rightBorder\")),\n top: parseEdge(findChildByLocalName(el, \"topBorder\")),\n bottom: parseEdge(findChildByLocalName(el, \"bottomBorder\")),\n }\n const fillBrush = findChildByLocalName(el, \"fillBrush\")\n const winBrush = fillBrush ? findChildByLocalName(fillBrush, \"winBrush\") : null\n const face = winBrush?.getAttribute(\"faceColor\")\n const fill = docColor(face)\n if (fill) bf.fill = fill\n styles.borderFill.set(id, bf)\n }\n }\n if (tag === \"style\" && (el.getAttribute(\"engName\") === \"Page Number\" || el.getAttribute(\"name\") === \"쪽 번호\")) {\n const cp = el.getAttribute(\"charPrIDRef\")\n if (cp != null && styles.pageNumCharPr == null) styles.pageNumCharPr = cp\n }\n const children = el.childNodes\n for (let i = 0; i < children.length; i++) {\n const ch = children[i]\n if (ch.nodeType === 1) walk(ch as Element)\n }\n }\n walk(root)\n for (const [id, tab] of paraTabRef) {\n const g = styles.paraGeom.get(id)\n if (g && autoTabPr.has(tab)) g.autoTabLeft = true\n }\n for (const [id, n] of paraNumber) {\n const g = styles.paraGeom.get(id), levels = numberings.get(n.id), lv = levels?.get(n.level)\n if (g && levels && lv) g.number = { id: n.id, level: n.level, heads: new Map([...levels].map(([k, v]) => [k, v.head])), ...(lv.charPrId != null ? { charPrId: lv.charPrId } : {}), offset: lv.offset, offsetPct: lv.offsetPct, autoIndent: lv.autoIndent }\n }\n for (const [id, b] of paraBullet) {\n const g = styles.paraGeom.get(id), bu = bullets.get(b)\n if (g && bu?.ch) g.bullet = bu\n }\n if (before2024) for (const st of styles.charPr.values()) st.before2024 = true\n if (halfQuote) for (const st of styles.charPr.values()) st.halfQuote = true\n return styles\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunk6ZRZLV3Jcjs = require('./chunk-6ZRZLV3J.cjs');
3
+ var _chunk6A4KYMQRcjs = require('./chunk-6A4KYMQR.cjs');
4
4
 
5
5
  // src/pdf/formula/models.ts
6
6
  var _crypto = require('crypto');
@@ -23,7 +23,7 @@ function isOfflineMode() {
23
23
  }
24
24
  function assertNetworkAllowed(what, hint) {
25
25
  if (!isOfflineMode()) return;
26
- throw new (0, _chunk6ZRZLV3Jcjs.KordocError)(
26
+ throw new (0, _chunk6A4KYMQRcjs.KordocError)(
27
27
  `\uD3D0\uC1C4\uB9DD \uBAA8\uB4DC(KORDOC_OFFLINE)\uC5D0\uC11C \uCC28\uB2E8\uB428: ${what}` + (hint ? ` \u2014 ${hint}` : "")
28
28
  );
29
29
  }
@@ -310,4 +310,4 @@ async function sha256OfFile(p) {
310
310
 
311
311
 
312
312
  exports.MFD_MODEL = MFD_MODEL; exports.MFR_ENCODER_MODEL = MFR_ENCODER_MODEL; exports.MFR_DECODER_MODEL = MFR_DECODER_MODEL; exports.MFR_TOKENIZER = MFR_TOKENIZER; exports.ALL_FORMULA_MODELS = ALL_FORMULA_MODELS; exports.getModelsDir = getModelsDir; exports.getFormulaModelsDir = getFormulaModelsDir; exports.getFormulaModelStatus = getFormulaModelStatus; exports.ensureModelsIn = ensureModelsIn; exports.ensureFormulaModels = ensureFormulaModels; exports.ensureSingleModel = ensureSingleModel;
313
- //# sourceMappingURL=chunk-VFEI2GIE.cjs.map
313
+ //# sourceMappingURL=chunk-KFQNG2EA.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/mong-e/workspace/kordoc/dist/chunk-VFEI2GIE.cjs","../src/pdf/formula/models.ts","../src/shared/offline.ts","../src/pdf/formula/model-partials.ts"],"names":[],"mappings":"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","file":"/Users/mong-e/workspace/kordoc/dist/chunk-VFEI2GIE.cjs","sourcesContent":[null,"/**\n * 수식 OCR 모델 자동 다운로드 + SHA-256 검증\n *\n * 캐시 위치: `~/.cache/kordoc/models/pix2text/`\n * 총 ~155MB (MFD 44MB + MFR encoder 87MB + MFR decoder 30MB + tokenizer 40KB)\n *\n * HF URL + SHA 는 Docufinder archive/rust-formula-ocr-wip 에서 검증 완료됨.\n * 모델 버전 갱신 시 URL + SHA 를 함께 갱신해야 한다.\n */\n\nimport { createHash } from \"crypto\"\nimport { createReadStream } from \"fs\"\nimport { mkdir, stat, unlink, rename } from \"fs/promises\"\nimport { createWriteStream } from \"fs\"\nimport { homedir } from \"os\"\nimport { join, dirname } from \"path\"\nimport { pipeline } from \"stream/promises\"\nimport { Readable } from \"stream\"\nimport { assertNetworkAllowed } from \"../../shared/offline.js\"\nimport { cleanupModelDownloadPartials, modelDownloadPartialPath } from \"./model-partials.js\"\n\nexport interface ModelSpec {\n name: string\n filename: string\n url: string\n sha256: string\n sizeMb: number\n /** 폐쇄망 차단 안내 — 없으면 models --export/--import 안내 */\n offlineHint?: string\n}\n\n/** 모델 스펙 (SHA-256 은 실제 다운로드로 검증됨 — 변경 금지) */\nexport const MFD_MODEL: ModelSpec = {\n name: \"Pix2Text MFD\",\n filename: \"mfd.onnx\",\n url: \"https://huggingface.co/breezedeus/pix2text-mfd/resolve/main/mfd-v20240618.onnx\",\n sha256: \"51a8854743b17ae654729af8db82a630c1ccfa06debf4856c8b28055f87d02c1\",\n sizeMb: 42,\n}\n\nexport const MFR_ENCODER_MODEL: ModelSpec = {\n name: \"Pix2Text MFR encoder\",\n filename: \"encoder_model.onnx\",\n url: \"https://huggingface.co/breezedeus/pix2text-mfr/resolve/main/encoder_model.onnx\",\n sha256: \"bd8d5c322792e9ec45793af5569e9748f82a3d728a9e00213dbfc56c1486f37d\",\n sizeMb: 87,\n}\n\nexport const MFR_DECODER_MODEL: ModelSpec = {\n name: \"Pix2Text MFR decoder\",\n filename: \"decoder_model.onnx\",\n url: \"https://huggingface.co/breezedeus/pix2text-mfr/resolve/main/decoder_model.onnx\",\n sha256: \"fd0f92d7a012f3dae41e1ac79421aea0ea888b5a66cb3f9a004e424f82f3daed\",\n sizeMb: 30,\n}\n\nexport const MFR_TOKENIZER: ModelSpec = {\n name: \"Pix2Text MFR tokenizer\",\n filename: \"tokenizer.json\",\n url: \"https://huggingface.co/breezedeus/pix2text-mfr/resolve/main/tokenizer.json\",\n sha256: \"3e2ab757277d22639bec28c9d7972e352d3d1dba223051fa674002dc5ab64df3\",\n sizeMb: 1,\n}\n\nexport const ALL_FORMULA_MODELS: ReadonlyArray<ModelSpec> = [\n MFD_MODEL,\n MFR_ENCODER_MODEL,\n MFR_DECODER_MODEL,\n MFR_TOKENIZER,\n]\n\n/**\n * 모델 캐시 하위 디렉토리 경로. 환경 변수 `KORDOC_MODEL_CACHE` 가 설정되면\n * 해당 경로를 우선 사용, 없으면 `~/.cache/kordoc/models/<subdir>/`.\n * (수식 OCR 외 텍스트 OCR 등 다른 모델군도 공용 — subdir 로 격리)\n */\nexport function getModelsDir(subdir: string): string {\n const override = process.env.KORDOC_MODEL_CACHE\n if (override && override.trim()) {\n return join(override, subdir)\n }\n return join(homedir(), \".cache\", \"kordoc\", \"models\", subdir)\n}\n\nexport function getFormulaModelsDir(): string {\n return getModelsDir(\"pix2text\")\n}\n\nexport interface ModelStatus {\n spec: ModelSpec\n localPath: string\n exists: boolean\n /** SHA 검증까지 끝났는가 (exists && valid) */\n verified: boolean\n /** 검증 실패 시 이유 */\n invalidReason?: string\n}\n\n/** 지정 모델군의 존재/유효성 상태 반환 (다운로드 없이 확인만) */\nexport async function getModelStatusIn(dir: string, specs: ReadonlyArray<ModelSpec>): Promise<ModelStatus[]> {\n const result: ModelStatus[] = []\n for (const spec of specs) {\n const localPath = join(dir, spec.filename)\n let exists = false\n try {\n const s = await stat(localPath)\n exists = s.isFile() && s.size > 0\n } catch {\n exists = false\n }\n if (!exists) {\n result.push({ spec, localPath, exists: false, verified: false })\n continue\n }\n try {\n const actual = await sha256OfFile(localPath)\n if (actual === spec.sha256) {\n result.push({ spec, localPath, exists: true, verified: true })\n } else {\n result.push({\n spec,\n localPath,\n exists: true,\n verified: false,\n invalidReason: `SHA256 mismatch: expected ${spec.sha256}, got ${actual}`,\n })\n }\n } catch (e) {\n result.push({\n spec,\n localPath,\n exists: true,\n verified: false,\n invalidReason: `SHA compute failed: ${(e as Error).message}`,\n })\n }\n }\n return result\n}\n\n/** 모든 수식 모델의 존재/유효성 상태 반환 (다운로드 없이 확인만) */\nexport async function getFormulaModelStatus(): Promise<ModelStatus[]> {\n return getModelStatusIn(getFormulaModelsDir(), ALL_FORMULA_MODELS)\n}\n\nexport interface DownloadProgress {\n spec: ModelSpec\n /** 현재까지 다운로드한 바이트 */\n downloaded: number\n /** 전체 바이트 (알 수 있으면) */\n total: number | null\n /** \"download\" | \"verify\" | \"done\" | \"skip\" */\n phase: \"download\" | \"verify\" | \"done\" | \"skip\" | \"error\"\n message?: string\n}\n\nexport type ProgressHandler = (p: DownloadProgress) => void\n\n/**\n * 지정 모델군을 다운로드/검증한다.\n * - 이미 있고 SHA 일치 → skip\n * - 없음 → 다운로드 후 SHA 검증. 실패 시 파일 삭제.\n */\nexport async function ensureModelsIn(dir: string, specs: ReadonlyArray<ModelSpec>, onProgress?: ProgressHandler): Promise<void> {\n await mkdir(dir, { recursive: true })\n\n for (const spec of specs) {\n const localPath = join(dir, spec.filename)\n await cleanupModelDownloadPartials(localPath)\n\n if (await isExistingValid(localPath, spec.sha256)) {\n onProgress?.({\n spec,\n downloaded: 0,\n total: null,\n phase: \"skip\",\n message: \"이미 존재 + SHA 일치\",\n })\n continue\n }\n\n // Keep the destination until atomic replacement; another process may have\n // finished a valid download since our initial check.\n await downloadToFile(spec, localPath, onProgress)\n }\n}\n\n/** 필요한 모든 수식 모델을 다운로드/검증한다. */\nexport async function ensureFormulaModels(onProgress?: ProgressHandler): Promise<void> {\n return ensureModelsIn(getFormulaModelsDir(), ALL_FORMULA_MODELS, onProgress)\n}\n\n/** 단일 모델만 확인/다운로드 (진단 UI 에서 개별 상태 새로고침용) */\nexport async function ensureSingleModel(spec: ModelSpec, onProgress?: ProgressHandler): Promise<void> {\n const dir = getFormulaModelsDir()\n await mkdir(dir, { recursive: true })\n const localPath = join(dir, spec.filename)\n await cleanupModelDownloadPartials(localPath)\n if (await isExistingValid(localPath, spec.sha256)) {\n onProgress?.({ spec, downloaded: 0, total: null, phase: \"skip\" })\n return\n }\n await downloadToFile(spec, localPath, onProgress)\n}\n\nasync function isExistingValid(localPath: string, sha256Expected: string): Promise<boolean> {\n try {\n const s = await stat(localPath)\n if (!s.isFile() || s.size === 0) return false\n } catch {\n return false\n }\n try {\n const actual = await sha256OfFile(localPath)\n return actual === sha256Expected\n } catch {\n return false\n }\n}\n\nasync function downloadToFile(\n spec: ModelSpec,\n localPath: string,\n onProgress?: ProgressHandler,\n): Promise<void> {\n // 폐쇄망 모드에서는 요청 자체를 보내지 않는다 (사이드로드 안내로 대체)\n assertNetworkAllowed(\n `${spec.name} 모델 다운로드`,\n spec.offlineHint ?? \"온라인 PC에서 `kordoc models --export <디렉토리>` 로 내보낸 뒤 이 PC에서 `kordoc models --import <디렉토리>` 하거나, KORDOC_MODEL_CACHE 로 모델 캐시 경로를 지정하세요\",\n )\n\n // 먼저 .part 로 받고 검증 후 rename — 중단된 다운로드가 \"정상 파일\"로 오인되는 걸 방지\n // Batch workers may download the same missing model concurrently. Never share\n // a partial file: only a fully verified download is atomically published.\n const partPath = modelDownloadPartialPath(localPath)\n await mkdir(dirname(localPath), { recursive: true })\n\n try {\n const resp = await fetch(spec.url, {\n headers: {\n // HF CDN 은 UA 없으면 가끔 403 을 뱉는다\n \"User-Agent\": \"kordoc-formula-ocr/1.0 (+https://github.com/chrisryugj/kordoc)\",\n },\n })\n if (!resp.ok || !resp.body) {\n throw new Error(\n `${spec.name} 다운로드 실패: HTTP ${resp.status} ${resp.statusText} (${spec.url})`,\n )\n }\n\n const lenHeader = resp.headers.get(\"content-length\")\n const total = lenHeader ? Number.parseInt(lenHeader, 10) : null\n let downloaded = 0\n\n // 크기 상한 — SHA-256 은 다 받은 뒤 검증하므로, 받는 동안 응답이 끝없이 이어지면 디스크가 찬다\n const maxBytes = Math.max(spec.sizeMb * 1.5, spec.sizeMb + 8) * 1024 * 1024\n const ws = createWriteStream(partPath)\n try {\n const reader = Readable.fromWeb(resp.body as unknown as import(\"stream/web\").ReadableStream)\n reader.on(\"data\", (chunk: Buffer | Uint8Array) => {\n downloaded += chunk.length\n if (downloaded > maxBytes) reader.destroy(new Error(`응답이 예상 크기(${spec.sizeMb}MB)를 크게 넘습니다`))\n onProgress?.({\n spec,\n downloaded,\n total,\n phase: \"download\",\n })\n })\n await pipeline(reader, ws)\n } catch (e) {\n throw new Error(`${spec.name} 스트리밍 실패: ${(e as Error).message}`)\n }\n\n onProgress?.({\n spec,\n downloaded,\n total,\n phase: \"verify\",\n })\n\n // SHA 검증\n let actual: string\n try {\n actual = await sha256OfFile(partPath)\n } catch (e) {\n throw new Error(`${spec.name} SHA 계산 실패: ${(e as Error).message}`)\n }\n\n if (actual !== spec.sha256) {\n throw new Error(\n `${spec.name} SHA256 mismatch: expected ${spec.sha256}, got ${actual} — 모델 URL 이 오염되었거나 전송 중 손상되었습니다.`,\n )\n }\n\n await rename(partPath, localPath)\n onProgress?.({\n spec,\n downloaded,\n total,\n phase: \"done\",\n })\n } finally {\n // Successful rename already removed this path. Process termination bypasses\n // finally; the next ensure call reclaims it only after proving the owner dead.\n try { await unlink(partPath) } catch { /* Preserve the original outcome. */ }\n }\n}\n\nasync function sha256OfFile(p: string): Promise<string> {\n const h = createHash(\"sha256\")\n const stream = createReadStream(p)\n await pipeline(stream, async function* (src) {\n for await (const chunk of src) {\n h.update(chunk)\n // yield 안 함 (소비만)\n }\n })\n return h.digest(\"hex\")\n}\n","/**\n * 폐쇄망(내부망) 배포용 아웃바운드 통신 킬스위치 + 파일 접근 루트 제한.\n *\n * kordoc 자체는 문서 파서라 상시 외부 통신이 없지만, 두 경로만 나간다:\n * 1. OCR/수식 모델 최초 1회 다운로드 (huggingface.co)\n * 2. `kordoc watch --webhook` 사용자가 명시한 URL\n *\n * `KORDOC_OFFLINE=1` 이면 위 경로를 **시도 전에** 차단한다. 보안성 검토에서\n * \"외부 통신 없음\"을 런타임으로 증명하기 위한 단일 관문 — 새 아웃바운드 경로를\n * 추가하면 반드시 `assertNetworkAllowed()` 를 먼저 통과시킬 것.\n *\n * `KORDOC_ROOT` 는 파일 접근을 특정 디렉토리 하위로 제한한다(미설정 시 제한 없음 —\n * 기존 동작 보존). 내부망 배포에서는 두 변수를 함께 고정하는 것을 권장.\n */\n\nimport { realpathSync } from \"fs\"\nimport { isAbsolute, relative, resolve, sep } from \"path\"\nimport { KordocError } from \"../utils.js\"\n\nconst TRUTHY = new Set([\"1\", \"true\", \"yes\", \"on\"])\n\nfunction envFlag(name: string): boolean {\n const v = process.env[name]\n return !!v && TRUTHY.has(v.trim().toLowerCase())\n}\n\n/** 폐쇄망 모드 여부 (`KORDOC_OFFLINE=1|true|yes|on`) */\nexport function isOfflineMode(): boolean {\n return envFlag(\"KORDOC_OFFLINE\")\n}\n\n/**\n * 아웃바운드 통신 관문. 폐쇄망 모드면 요청을 보내기 전에 예외로 끊는다.\n * @param what 차단 대상 설명 (사용자에게 노출됨 — 호스트/URL 은 넣지 말 것)\n * @param hint 대안 안내 (오프라인 사이드로드 방법 등)\n */\nexport function assertNetworkAllowed(what: string, hint?: string): void {\n if (!isOfflineMode()) return\n throw new KordocError(\n `폐쇄망 모드(KORDOC_OFFLINE)에서 차단됨: ${what}` + (hint ? ` — ${hint}` : \"\"),\n )\n}\n\n/** 파일 접근 루트 (`KORDOC_ROOT`). 미설정이면 null = 제한 없음 */\nexport function getAccessRoot(): string | null {\n const raw = process.env.KORDOC_ROOT\n if (!raw || !raw.trim()) return null\n const resolved = resolve(raw.trim())\n try {\n return realpathSync(resolved)\n } catch {\n // 루트가 실재하지 않으면 모든 접근이 막히는 게 안전한 기본값\n return resolved\n }\n}\n\n/** 경로가 루트 하위인지 (루트 자신 포함). 경계 문자열 매칭이 아닌 세그먼트 단위 비교 */\nexport function isWithinRoot(realPath: string, root: string): boolean {\n if (realPath === root) return true\n const rel = relative(root, realPath)\n return rel !== \"\" && !rel.startsWith(\"..\") && !isAbsolute(rel) && !rel.startsWith(`..${sep}`)\n}\n\n/**\n * `KORDOC_ROOT` 가 설정된 경우 경로가 루트 하위인지 검증한다.\n * 인자는 이미 realpath 로 해석된 절대경로여야 한다 (심볼릭 링크 탈출 방지).\n */\nexport function assertWithinRoot(realPath: string): void {\n const root = getAccessRoot()\n if (!root) return\n if (!isWithinRoot(realPath, root)) {\n throw new KordocError(`허용된 작업 디렉토리(KORDOC_ROOT) 밖의 경로입니다: ${realPath}`)\n }\n}\n","/** Process-owned temporary model downloads. Never infer abandonment from age alone. */\nimport { createHash, randomUUID } from \"node:crypto\"\nimport { readFileSync, readlinkSync } from \"node:fs\"\nimport { readdir, unlink } from \"node:fs/promises\"\nimport { hostname } from \"node:os\"\nimport { basename, dirname, join } from \"node:path\"\n\nlet scope: string | undefined\nfunction ownerScope(): string {\n if (scope) return scope\n let namespace = \"\"\n if (process.platform === \"linux\") {\n try {\n // A cache may be shared by containers whose PID spaces differ. A missing\n // PID in this namespace says nothing about a download in another one.\n // Boot identity also separates machines with matching hostnames/ns inodes.\n const boot = readFileSync(\"/proc/sys/kernel/random/boot_id\", \"utf8\").trim()\n if (!boot) throw new Error(\"Kernel boot identity unavailable\")\n namespace = `${boot}:${readlinkSync(\"/proc/self/ns/pid\")}`\n } catch {\n // Fail closed if namespace identity is unavailable: only this process can\n // recognize its own paths; it will not scavenge another process's files.\n namespace = randomUUID()\n }\n }\n scope = createHash(\"sha256\").update(`${process.platform}\\0${hostname()}\\0${namespace}`).digest(\"hex\").slice(0, 32)\n return scope\n}\n\nexport function modelDownloadPartialPath(localPath: string): string {\n return `${localPath}.kordoc-${ownerScope()}-${process.pid}-${randomUUID()}.part`\n}\n\n/** Best effort: cache maintenance must not prevent use of a valid model. */\nexport async function cleanupModelDownloadPartials(localPath: string): Promise<void> {\n const dir = dirname(localPath)\n const prefix = `${basename(localPath)}.kordoc-${ownerScope()}-`\n let entries\n try { entries = await readdir(dir, { withFileTypes: true }) } catch { return }\n for (const entry of entries) {\n // Do not follow symlinks, touch other models, or guess ownership for old\n // UUID-only partials/foreign hosts/namespaces. Their owner could still be live.\n if (!entry.isFile() || !entry.name.startsWith(prefix)) continue\n const match = /^([1-9]\\d*)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\.part$/.exec(entry.name.slice(prefix.length))\n if (!match) continue\n const pid = Number(match[1])\n if (!Number.isSafeInteger(pid) || pid > 0x7fffffff) continue\n try {\n process.kill(pid, 0)\n continue // Includes a reused PID: retaining an orphan beats deleting live data.\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ESRCH\") continue // EPERM/unknown = potentially live.\n }\n try { await unlink(join(dir, entry.name)) } catch { /* Another cleaner may have won. */ }\n }\n}\n"]}
1
+ {"version":3,"sources":["/Users/mong-e/workspace/kordoc/dist/chunk-KFQNG2EA.cjs","../src/pdf/formula/models.ts","../src/shared/offline.ts","../src/pdf/formula/model-partials.ts"],"names":[],"mappings":"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","file":"/Users/mong-e/workspace/kordoc/dist/chunk-KFQNG2EA.cjs","sourcesContent":[null,"/**\n * 수식 OCR 모델 자동 다운로드 + SHA-256 검증\n *\n * 캐시 위치: `~/.cache/kordoc/models/pix2text/`\n * 총 ~155MB (MFD 44MB + MFR encoder 87MB + MFR decoder 30MB + tokenizer 40KB)\n *\n * HF URL + SHA 는 Docufinder archive/rust-formula-ocr-wip 에서 검증 완료됨.\n * 모델 버전 갱신 시 URL + SHA 를 함께 갱신해야 한다.\n */\n\nimport { createHash } from \"crypto\"\nimport { createReadStream } from \"fs\"\nimport { mkdir, stat, unlink, rename } from \"fs/promises\"\nimport { createWriteStream } from \"fs\"\nimport { homedir } from \"os\"\nimport { join, dirname } from \"path\"\nimport { pipeline } from \"stream/promises\"\nimport { Readable } from \"stream\"\nimport { assertNetworkAllowed } from \"../../shared/offline.js\"\nimport { cleanupModelDownloadPartials, modelDownloadPartialPath } from \"./model-partials.js\"\n\nexport interface ModelSpec {\n name: string\n filename: string\n url: string\n sha256: string\n sizeMb: number\n /** 폐쇄망 차단 안내 — 없으면 models --export/--import 안내 */\n offlineHint?: string\n}\n\n/** 모델 스펙 (SHA-256 은 실제 다운로드로 검증됨 — 변경 금지) */\nexport const MFD_MODEL: ModelSpec = {\n name: \"Pix2Text MFD\",\n filename: \"mfd.onnx\",\n url: \"https://huggingface.co/breezedeus/pix2text-mfd/resolve/main/mfd-v20240618.onnx\",\n sha256: \"51a8854743b17ae654729af8db82a630c1ccfa06debf4856c8b28055f87d02c1\",\n sizeMb: 42,\n}\n\nexport const MFR_ENCODER_MODEL: ModelSpec = {\n name: \"Pix2Text MFR encoder\",\n filename: \"encoder_model.onnx\",\n url: \"https://huggingface.co/breezedeus/pix2text-mfr/resolve/main/encoder_model.onnx\",\n sha256: \"bd8d5c322792e9ec45793af5569e9748f82a3d728a9e00213dbfc56c1486f37d\",\n sizeMb: 87,\n}\n\nexport const MFR_DECODER_MODEL: ModelSpec = {\n name: \"Pix2Text MFR decoder\",\n filename: \"decoder_model.onnx\",\n url: \"https://huggingface.co/breezedeus/pix2text-mfr/resolve/main/decoder_model.onnx\",\n sha256: \"fd0f92d7a012f3dae41e1ac79421aea0ea888b5a66cb3f9a004e424f82f3daed\",\n sizeMb: 30,\n}\n\nexport const MFR_TOKENIZER: ModelSpec = {\n name: \"Pix2Text MFR tokenizer\",\n filename: \"tokenizer.json\",\n url: \"https://huggingface.co/breezedeus/pix2text-mfr/resolve/main/tokenizer.json\",\n sha256: \"3e2ab757277d22639bec28c9d7972e352d3d1dba223051fa674002dc5ab64df3\",\n sizeMb: 1,\n}\n\nexport const ALL_FORMULA_MODELS: ReadonlyArray<ModelSpec> = [\n MFD_MODEL,\n MFR_ENCODER_MODEL,\n MFR_DECODER_MODEL,\n MFR_TOKENIZER,\n]\n\n/**\n * 모델 캐시 하위 디렉토리 경로. 환경 변수 `KORDOC_MODEL_CACHE` 가 설정되면\n * 해당 경로를 우선 사용, 없으면 `~/.cache/kordoc/models/<subdir>/`.\n * (수식 OCR 외 텍스트 OCR 등 다른 모델군도 공용 — subdir 로 격리)\n */\nexport function getModelsDir(subdir: string): string {\n const override = process.env.KORDOC_MODEL_CACHE\n if (override && override.trim()) {\n return join(override, subdir)\n }\n return join(homedir(), \".cache\", \"kordoc\", \"models\", subdir)\n}\n\nexport function getFormulaModelsDir(): string {\n return getModelsDir(\"pix2text\")\n}\n\nexport interface ModelStatus {\n spec: ModelSpec\n localPath: string\n exists: boolean\n /** SHA 검증까지 끝났는가 (exists && valid) */\n verified: boolean\n /** 검증 실패 시 이유 */\n invalidReason?: string\n}\n\n/** 지정 모델군의 존재/유효성 상태 반환 (다운로드 없이 확인만) */\nexport async function getModelStatusIn(dir: string, specs: ReadonlyArray<ModelSpec>): Promise<ModelStatus[]> {\n const result: ModelStatus[] = []\n for (const spec of specs) {\n const localPath = join(dir, spec.filename)\n let exists = false\n try {\n const s = await stat(localPath)\n exists = s.isFile() && s.size > 0\n } catch {\n exists = false\n }\n if (!exists) {\n result.push({ spec, localPath, exists: false, verified: false })\n continue\n }\n try {\n const actual = await sha256OfFile(localPath)\n if (actual === spec.sha256) {\n result.push({ spec, localPath, exists: true, verified: true })\n } else {\n result.push({\n spec,\n localPath,\n exists: true,\n verified: false,\n invalidReason: `SHA256 mismatch: expected ${spec.sha256}, got ${actual}`,\n })\n }\n } catch (e) {\n result.push({\n spec,\n localPath,\n exists: true,\n verified: false,\n invalidReason: `SHA compute failed: ${(e as Error).message}`,\n })\n }\n }\n return result\n}\n\n/** 모든 수식 모델의 존재/유효성 상태 반환 (다운로드 없이 확인만) */\nexport async function getFormulaModelStatus(): Promise<ModelStatus[]> {\n return getModelStatusIn(getFormulaModelsDir(), ALL_FORMULA_MODELS)\n}\n\nexport interface DownloadProgress {\n spec: ModelSpec\n /** 현재까지 다운로드한 바이트 */\n downloaded: number\n /** 전체 바이트 (알 수 있으면) */\n total: number | null\n /** \"download\" | \"verify\" | \"done\" | \"skip\" */\n phase: \"download\" | \"verify\" | \"done\" | \"skip\" | \"error\"\n message?: string\n}\n\nexport type ProgressHandler = (p: DownloadProgress) => void\n\n/**\n * 지정 모델군을 다운로드/검증한다.\n * - 이미 있고 SHA 일치 → skip\n * - 없음 → 다운로드 후 SHA 검증. 실패 시 파일 삭제.\n */\nexport async function ensureModelsIn(dir: string, specs: ReadonlyArray<ModelSpec>, onProgress?: ProgressHandler): Promise<void> {\n await mkdir(dir, { recursive: true })\n\n for (const spec of specs) {\n const localPath = join(dir, spec.filename)\n await cleanupModelDownloadPartials(localPath)\n\n if (await isExistingValid(localPath, spec.sha256)) {\n onProgress?.({\n spec,\n downloaded: 0,\n total: null,\n phase: \"skip\",\n message: \"이미 존재 + SHA 일치\",\n })\n continue\n }\n\n // Keep the destination until atomic replacement; another process may have\n // finished a valid download since our initial check.\n await downloadToFile(spec, localPath, onProgress)\n }\n}\n\n/** 필요한 모든 수식 모델을 다운로드/검증한다. */\nexport async function ensureFormulaModels(onProgress?: ProgressHandler): Promise<void> {\n return ensureModelsIn(getFormulaModelsDir(), ALL_FORMULA_MODELS, onProgress)\n}\n\n/** 단일 모델만 확인/다운로드 (진단 UI 에서 개별 상태 새로고침용) */\nexport async function ensureSingleModel(spec: ModelSpec, onProgress?: ProgressHandler): Promise<void> {\n const dir = getFormulaModelsDir()\n await mkdir(dir, { recursive: true })\n const localPath = join(dir, spec.filename)\n await cleanupModelDownloadPartials(localPath)\n if (await isExistingValid(localPath, spec.sha256)) {\n onProgress?.({ spec, downloaded: 0, total: null, phase: \"skip\" })\n return\n }\n await downloadToFile(spec, localPath, onProgress)\n}\n\nasync function isExistingValid(localPath: string, sha256Expected: string): Promise<boolean> {\n try {\n const s = await stat(localPath)\n if (!s.isFile() || s.size === 0) return false\n } catch {\n return false\n }\n try {\n const actual = await sha256OfFile(localPath)\n return actual === sha256Expected\n } catch {\n return false\n }\n}\n\nasync function downloadToFile(\n spec: ModelSpec,\n localPath: string,\n onProgress?: ProgressHandler,\n): Promise<void> {\n // 폐쇄망 모드에서는 요청 자체를 보내지 않는다 (사이드로드 안내로 대체)\n assertNetworkAllowed(\n `${spec.name} 모델 다운로드`,\n spec.offlineHint ?? \"온라인 PC에서 `kordoc models --export <디렉토리>` 로 내보낸 뒤 이 PC에서 `kordoc models --import <디렉토리>` 하거나, KORDOC_MODEL_CACHE 로 모델 캐시 경로를 지정하세요\",\n )\n\n // 먼저 .part 로 받고 검증 후 rename — 중단된 다운로드가 \"정상 파일\"로 오인되는 걸 방지\n // Batch workers may download the same missing model concurrently. Never share\n // a partial file: only a fully verified download is atomically published.\n const partPath = modelDownloadPartialPath(localPath)\n await mkdir(dirname(localPath), { recursive: true })\n\n try {\n const resp = await fetch(spec.url, {\n headers: {\n // HF CDN 은 UA 없으면 가끔 403 을 뱉는다\n \"User-Agent\": \"kordoc-formula-ocr/1.0 (+https://github.com/chrisryugj/kordoc)\",\n },\n })\n if (!resp.ok || !resp.body) {\n throw new Error(\n `${spec.name} 다운로드 실패: HTTP ${resp.status} ${resp.statusText} (${spec.url})`,\n )\n }\n\n const lenHeader = resp.headers.get(\"content-length\")\n const total = lenHeader ? Number.parseInt(lenHeader, 10) : null\n let downloaded = 0\n\n // 크기 상한 — SHA-256 은 다 받은 뒤 검증하므로, 받는 동안 응답이 끝없이 이어지면 디스크가 찬다\n const maxBytes = Math.max(spec.sizeMb * 1.5, spec.sizeMb + 8) * 1024 * 1024\n const ws = createWriteStream(partPath)\n try {\n const reader = Readable.fromWeb(resp.body as unknown as import(\"stream/web\").ReadableStream)\n reader.on(\"data\", (chunk: Buffer | Uint8Array) => {\n downloaded += chunk.length\n if (downloaded > maxBytes) reader.destroy(new Error(`응답이 예상 크기(${spec.sizeMb}MB)를 크게 넘습니다`))\n onProgress?.({\n spec,\n downloaded,\n total,\n phase: \"download\",\n })\n })\n await pipeline(reader, ws)\n } catch (e) {\n throw new Error(`${spec.name} 스트리밍 실패: ${(e as Error).message}`)\n }\n\n onProgress?.({\n spec,\n downloaded,\n total,\n phase: \"verify\",\n })\n\n // SHA 검증\n let actual: string\n try {\n actual = await sha256OfFile(partPath)\n } catch (e) {\n throw new Error(`${spec.name} SHA 계산 실패: ${(e as Error).message}`)\n }\n\n if (actual !== spec.sha256) {\n throw new Error(\n `${spec.name} SHA256 mismatch: expected ${spec.sha256}, got ${actual} — 모델 URL 이 오염되었거나 전송 중 손상되었습니다.`,\n )\n }\n\n await rename(partPath, localPath)\n onProgress?.({\n spec,\n downloaded,\n total,\n phase: \"done\",\n })\n } finally {\n // Successful rename already removed this path. Process termination bypasses\n // finally; the next ensure call reclaims it only after proving the owner dead.\n try { await unlink(partPath) } catch { /* Preserve the original outcome. */ }\n }\n}\n\nasync function sha256OfFile(p: string): Promise<string> {\n const h = createHash(\"sha256\")\n const stream = createReadStream(p)\n await pipeline(stream, async function* (src) {\n for await (const chunk of src) {\n h.update(chunk)\n // yield 안 함 (소비만)\n }\n })\n return h.digest(\"hex\")\n}\n","/**\n * 폐쇄망(내부망) 배포용 아웃바운드 통신 킬스위치 + 파일 접근 루트 제한.\n *\n * kordoc 자체는 문서 파서라 상시 외부 통신이 없지만, 두 경로만 나간다:\n * 1. OCR/수식 모델 최초 1회 다운로드 (huggingface.co)\n * 2. `kordoc watch --webhook` 사용자가 명시한 URL\n *\n * `KORDOC_OFFLINE=1` 이면 위 경로를 **시도 전에** 차단한다. 보안성 검토에서\n * \"외부 통신 없음\"을 런타임으로 증명하기 위한 단일 관문 — 새 아웃바운드 경로를\n * 추가하면 반드시 `assertNetworkAllowed()` 를 먼저 통과시킬 것.\n *\n * `KORDOC_ROOT` 는 파일 접근을 특정 디렉토리 하위로 제한한다(미설정 시 제한 없음 —\n * 기존 동작 보존). 내부망 배포에서는 두 변수를 함께 고정하는 것을 권장.\n */\n\nimport { realpathSync } from \"fs\"\nimport { isAbsolute, relative, resolve, sep } from \"path\"\nimport { KordocError } from \"../utils.js\"\n\nconst TRUTHY = new Set([\"1\", \"true\", \"yes\", \"on\"])\n\nfunction envFlag(name: string): boolean {\n const v = process.env[name]\n return !!v && TRUTHY.has(v.trim().toLowerCase())\n}\n\n/** 폐쇄망 모드 여부 (`KORDOC_OFFLINE=1|true|yes|on`) */\nexport function isOfflineMode(): boolean {\n return envFlag(\"KORDOC_OFFLINE\")\n}\n\n/**\n * 아웃바운드 통신 관문. 폐쇄망 모드면 요청을 보내기 전에 예외로 끊는다.\n * @param what 차단 대상 설명 (사용자에게 노출됨 — 호스트/URL 은 넣지 말 것)\n * @param hint 대안 안내 (오프라인 사이드로드 방법 등)\n */\nexport function assertNetworkAllowed(what: string, hint?: string): void {\n if (!isOfflineMode()) return\n throw new KordocError(\n `폐쇄망 모드(KORDOC_OFFLINE)에서 차단됨: ${what}` + (hint ? ` — ${hint}` : \"\"),\n )\n}\n\n/** 파일 접근 루트 (`KORDOC_ROOT`). 미설정이면 null = 제한 없음 */\nexport function getAccessRoot(): string | null {\n const raw = process.env.KORDOC_ROOT\n if (!raw || !raw.trim()) return null\n const resolved = resolve(raw.trim())\n try {\n return realpathSync(resolved)\n } catch {\n // 루트가 실재하지 않으면 모든 접근이 막히는 게 안전한 기본값\n return resolved\n }\n}\n\n/** 경로가 루트 하위인지 (루트 자신 포함). 경계 문자열 매칭이 아닌 세그먼트 단위 비교 */\nexport function isWithinRoot(realPath: string, root: string): boolean {\n if (realPath === root) return true\n const rel = relative(root, realPath)\n return rel !== \"\" && !rel.startsWith(\"..\") && !isAbsolute(rel) && !rel.startsWith(`..${sep}`)\n}\n\n/**\n * `KORDOC_ROOT` 가 설정된 경우 경로가 루트 하위인지 검증한다.\n * 인자는 이미 realpath 로 해석된 절대경로여야 한다 (심볼릭 링크 탈출 방지).\n */\nexport function assertWithinRoot(realPath: string): void {\n const root = getAccessRoot()\n if (!root) return\n if (!isWithinRoot(realPath, root)) {\n throw new KordocError(`허용된 작업 디렉토리(KORDOC_ROOT) 밖의 경로입니다: ${realPath}`)\n }\n}\n","/** Process-owned temporary model downloads. Never infer abandonment from age alone. */\nimport { createHash, randomUUID } from \"node:crypto\"\nimport { readFileSync, readlinkSync } from \"node:fs\"\nimport { readdir, unlink } from \"node:fs/promises\"\nimport { hostname } from \"node:os\"\nimport { basename, dirname, join } from \"node:path\"\n\nlet scope: string | undefined\nfunction ownerScope(): string {\n if (scope) return scope\n let namespace = \"\"\n if (process.platform === \"linux\") {\n try {\n // A cache may be shared by containers whose PID spaces differ. A missing\n // PID in this namespace says nothing about a download in another one.\n // Boot identity also separates machines with matching hostnames/ns inodes.\n const boot = readFileSync(\"/proc/sys/kernel/random/boot_id\", \"utf8\").trim()\n if (!boot) throw new Error(\"Kernel boot identity unavailable\")\n namespace = `${boot}:${readlinkSync(\"/proc/self/ns/pid\")}`\n } catch {\n // Fail closed if namespace identity is unavailable: only this process can\n // recognize its own paths; it will not scavenge another process's files.\n namespace = randomUUID()\n }\n }\n scope = createHash(\"sha256\").update(`${process.platform}\\0${hostname()}\\0${namespace}`).digest(\"hex\").slice(0, 32)\n return scope\n}\n\nexport function modelDownloadPartialPath(localPath: string): string {\n return `${localPath}.kordoc-${ownerScope()}-${process.pid}-${randomUUID()}.part`\n}\n\n/** Best effort: cache maintenance must not prevent use of a valid model. */\nexport async function cleanupModelDownloadPartials(localPath: string): Promise<void> {\n const dir = dirname(localPath)\n const prefix = `${basename(localPath)}.kordoc-${ownerScope()}-`\n let entries\n try { entries = await readdir(dir, { withFileTypes: true }) } catch { return }\n for (const entry of entries) {\n // Do not follow symlinks, touch other models, or guess ownership for old\n // UUID-only partials/foreign hosts/namespaces. Their owner could still be live.\n if (!entry.isFile() || !entry.name.startsWith(prefix)) continue\n const match = /^([1-9]\\d*)-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\.part$/.exec(entry.name.slice(prefix.length))\n if (!match) continue\n const pid = Number(match[1])\n if (!Number.isSafeInteger(pid) || pid > 0x7fffffff) continue\n try {\n process.kill(pid, 0)\n continue // Includes a reused PID: retaining an orphan beats deleting live data.\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ESRCH\") continue // EPERM/unknown = potentially live.\n }\n try { await unlink(join(dir, entry.name)) } catch { /* Another cleaner may have won. */ }\n }\n}\n"]}
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  parse
4
- } from "./chunk-OAOM7IEI.js";
4
+ } from "./chunk-5AHG3XY3.js";
5
5
  import {
6
6
  classifyError,
7
7
  sanitizeError,
8
8
  toArrayBuffer
9
- } from "./chunk-EWCJCWTT.js";
9
+ } from "./chunk-6CPBNK5O.js";
10
10
 
11
11
  // src/cli/worker-parse.ts
12
12
  import { readFileSync, statSync } from "fs";
@@ -26,4 +26,4 @@ async function parseWorkerFile(absPath, options) {
26
26
  export {
27
27
  parseWorkerFile
28
28
  };
29
- //# sourceMappingURL=chunk-PHZDAHZC.js.map
29
+ //# sourceMappingURL=chunk-LBOXKGAH.js.map
@@ -4,15 +4,15 @@
4
4
 
5
5
 
6
6
 
7
- var _chunkN65EY5BIcjs = require('./chunk-N65EY5BI.cjs');
7
+ var _chunkTPKP3WQUcjs = require('./chunk-TPKP3WQU.cjs');
8
8
 
9
9
 
10
10
 
11
- var _chunkVFEI2GIEcjs = require('./chunk-VFEI2GIE.cjs');
11
+ var _chunkKFQNG2EAcjs = require('./chunk-KFQNG2EA.cjs');
12
12
 
13
13
 
14
14
 
15
- var _chunk6ZRZLV3Jcjs = require('./chunk-6ZRZLV3J.cjs');
15
+ var _chunk6A4KYMQRcjs = require('./chunk-6A4KYMQR.cjs');
16
16
 
17
17
  // src/ocr/models.ts
18
18
  var _path = require('path');
@@ -56,7 +56,7 @@ var OCR_REC_DICT = {
56
56
  sizeMb: 1
57
57
  };
58
58
  function getOcrModelsDir() {
59
- return _chunkVFEI2GIEcjs.getModelsDir.call(void 0, "ppocr");
59
+ return _chunkKFQNG2EAcjs.getModelsDir.call(void 0, "ppocr");
60
60
  }
61
61
  var DEFAULT_OCR_MODELS = Object.freeze({
62
62
  det: { spec: OCR_DET_MODEL },
@@ -98,7 +98,7 @@ async function ensureOcrModelsFor(sel, onProgress) {
98
98
  if (!stamp || verifiedInProcess.get(path) !== stamp) todo.push(s.spec);
99
99
  }
100
100
  if (!todo.length) return;
101
- await _chunkVFEI2GIEcjs.ensureModelsIn.call(void 0, dir, todo, onProgress);
101
+ await _chunkKFQNG2EAcjs.ensureModelsIn.call(void 0, dir, todo, onProgress);
102
102
  for (const spec of todo) {
103
103
  const path = _path.join.call(void 0, dir, spec.filename), stamp = await fileStamp(path);
104
104
  if (stamp) verifiedInProcess.set(path, stamp);
@@ -2055,6 +2055,8 @@ var EDGE_COVER = 0.6;
2055
2055
  var EDGE_SLACK = 3;
2056
2056
  var DASH_GAP = 3;
2057
2057
  var TIP_MAX = 1.5;
2058
+ var SOLID_END_GAP = 2;
2059
+ var SOLID_JOIN = 0.3;
2058
2060
  function rules(lines, dir) {
2059
2061
  const sorted = [...lines].sort((a, b) => dir === "h" ? a.y1 - b.y1 : a.x1 - b.x1);
2060
2062
  return { dir, pos: sorted.map((l) => dir === "h" ? l.y1 : l.x1), lines: sorted };
@@ -2062,8 +2064,8 @@ function rules(lines, dir) {
2062
2064
  function seen(r, at, a1, a2, near2 = EDGE_NEAR2) {
2063
2065
  const len = a2 - a1;
2064
2066
  if (len <= 0) return false;
2065
- const covered = coverage(r, at, a1, a2, near2);
2066
- return covered >= len * EDGE_COVER && len - covered <= EDGE_SLACK;
2067
+ const { covered, solid, endGap } = coverage(r, at, a1, a2, near2);
2068
+ return covered >= len * EDGE_COVER && len - covered <= EDGE_SLACK && !(solid && endGap > SOLID_END_GAP);
2067
2069
  }
2068
2070
  function coverage(r, at, a1, a2, near2) {
2069
2071
  let i = 0, j = r.pos.length;
@@ -2080,10 +2082,11 @@ function coverage(r, at, a1, a2, near2) {
2080
2082
  if (hi <= lo || l1 < a1 !== l2 > a2 && hi - lo < TIP_MAX) continue;
2081
2083
  spans.push([lo, hi]);
2082
2084
  }
2083
- if (!spans.length) return 0;
2085
+ if (!spans.length) return { covered: 0, solid: false, endGap: a2 - a1 };
2084
2086
  spans.sort((p, q) => p[0] - q[0]);
2085
- let covered = 0, s = spans[0][0], e = spans[0][1];
2087
+ let covered = 0, s = spans[0][0], e = spans[0][1], solid = true;
2086
2088
  for (const [lo, hi] of spans.slice(1)) {
2089
+ if (lo > e + SOLID_JOIN) solid = false;
2087
2090
  if (lo <= e + DASH_GAP) e = Math.max(e, hi);
2088
2091
  else {
2089
2092
  covered += e - s;
@@ -2091,7 +2094,7 @@ function coverage(r, at, a1, a2, near2) {
2091
2094
  e = hi;
2092
2095
  }
2093
2096
  }
2094
- return covered + e - s;
2097
+ return { covered: covered + e - s, solid, endGap: Math.max(spans[0][0] - a1, a2 - e) };
2095
2098
  }
2096
2099
  function edgesOf(box, h, v, hNear = EDGE_NEAR2) {
2097
2100
  return {
@@ -2147,16 +2150,16 @@ function recordOpenEndCellEdges(grid, cells, horizontals) {
2147
2150
  }
2148
2151
  function takeClipCellEdges(from, to) {
2149
2152
  const e = CLIP_CELL_EDGES.get(from);
2150
- if (e) _chunkN65EY5BIcjs.CELL_EDGES.set(to, e);
2153
+ if (e) _chunkTPKP3WQUcjs.CELL_EDGES.set(to, e);
2151
2154
  }
2152
2155
  function hasOpenCellSeam(a, b) {
2153
- const u = _chunkN65EY5BIcjs.CELL_EDGES.get(a), d = _chunkN65EY5BIcjs.CELL_EDGES.get(b);
2156
+ const u = _chunkTPKP3WQUcjs.CELL_EDGES.get(a), d = _chunkTPKP3WQUcjs.CELL_EDGES.get(b);
2154
2157
  return !!u && !!d && u.t && !u.b && !d.t && d.b && u.l && u.r && d.l && d.r;
2155
2158
  }
2156
2159
  function joinCellEdges(a, b) {
2157
- const u = _chunkN65EY5BIcjs.CELL_EDGES.get(a), d = _chunkN65EY5BIcjs.CELL_EDGES.get(b);
2160
+ const u = _chunkTPKP3WQUcjs.CELL_EDGES.get(a), d = _chunkTPKP3WQUcjs.CELL_EDGES.get(b);
2158
2161
  if (!u || !d) return;
2159
- _chunkN65EY5BIcjs.CELL_EDGES.set(a, { t: u.t, b: d.b, l: u.l || d.l, r: u.r || d.r });
2162
+ _chunkTPKP3WQUcjs.CELL_EDGES.set(a, { t: u.t, b: d.b, l: u.l || d.l, r: u.r || d.r });
2160
2163
  }
2161
2164
 
2162
2165
  // src/pdf/cell-extract.ts
@@ -2332,7 +2335,7 @@ function tagScripts(text, lines) {
2332
2335
  const [s, e, kind] = marks[m];
2333
2336
  out = out.slice(0, s) + `<${kind}>` + out.slice(s, e) + `</${kind}>` + out.slice(e);
2334
2337
  }
2335
- return _chunkN65EY5BIcjs.tidyScriptTags.call(void 0, out);
2338
+ return _chunkTPKP3WQUcjs.tidyScriptTags.call(void 0, out);
2336
2339
  }
2337
2340
 
2338
2341
  // src/pdf/line-wrap.ts
@@ -5391,9 +5394,9 @@ function detectHeadings(blocks, medianFontSize) {
5391
5394
  if (/^\d+$/.test(text) && !isChapterNumber(block, blocks[bi + 1], medianFontSize)) continue;
5392
5395
  const ratio = block.style.fontSize / medianFontSize;
5393
5396
  let level = 0;
5394
- if (ratio >= _chunkN65EY5BIcjs.HEADING_RATIO_H1) level = 1;
5395
- else if (ratio >= _chunkN65EY5BIcjs.HEADING_RATIO_H2) level = 2;
5396
- else if (ratio >= _chunkN65EY5BIcjs.HEADING_RATIO_H3) level = 3;
5397
+ if (ratio >= _chunkTPKP3WQUcjs.HEADING_RATIO_H1) level = 1;
5398
+ else if (ratio >= _chunkTPKP3WQUcjs.HEADING_RATIO_H2) level = 2;
5399
+ else if (ratio >= _chunkTPKP3WQUcjs.HEADING_RATIO_H3) level = 3;
5397
5400
  if (level > 0) {
5398
5401
  block.type = "heading";
5399
5402
  block.level = level;
@@ -5406,7 +5409,7 @@ function isChapterNumber(block, next, medianFontSize) {
5406
5409
  const nextSize = _nullishCoalesce(_optionalChain([next, 'optionalAccess', _159 => _159.style, 'optionalAccess', _160 => _160.fontSize]), () => ( 0));
5407
5410
  const text = _nullishCoalesce(_optionalChain([next, 'optionalAccess', _161 => _161.text, 'optionalAccess', _162 => _162.trim, 'call', _163 => _163()]), () => ( ""));
5408
5411
  const a = block.bbox, b = _optionalChain([next, 'optionalAccess', _164 => _164.bbox]);
5409
- return /^\d{1,2}$/.test(_nullishCoalesce(_optionalChain([block, 'access', _165 => _165.text, 'optionalAccess', _166 => _166.trim, 'call', _167 => _167()]), () => ( ""))) && size >= medianFontSize * 2.5 && !!next && (next.type === "paragraph" || next.type === "heading") && next.pageNumber === block.pageNumber && nextSize >= medianFontSize * _chunkN65EY5BIcjs.HEADING_RATIO_H1 && text.length > 0 && text.length <= 80 && !/^[\d\s.]+$/.test(text) && !!a && !!b && a.y > b.y && a.y - (b.y + b.height) <= size * 2;
5412
+ return /^\d{1,2}$/.test(_nullishCoalesce(_optionalChain([block, 'access', _165 => _165.text, 'optionalAccess', _166 => _166.trim, 'call', _167 => _167()]), () => ( ""))) && size >= medianFontSize * 2.5 && !!next && (next.type === "paragraph" || next.type === "heading") && next.pageNumber === block.pageNumber && nextSize >= medianFontSize * _chunkTPKP3WQUcjs.HEADING_RATIO_H1 && text.length > 0 && text.length <= 80 && !/^[\d\s.]+$/.test(text) && !!a && !!b && a.y > b.y && a.y - (b.y + b.height) <= size * 2;
5410
5413
  }
5411
5414
  function mergeStackedHeadingLines(blocks, medianFontSize) {
5412
5415
  for (let i = 0; i < blocks.length - 1; ) {
@@ -6944,16 +6947,16 @@ function isProseSpread(items) {
6944
6947
  for (let i = 1; i < sorted.length; i++) {
6945
6948
  gaps.push(sorted[i].x - (sorted[i - 1].x + sorted[i - 1].w));
6946
6949
  }
6947
- const maxGap = _chunk6ZRZLV3Jcjs.safeMax.call(void 0, gaps);
6950
+ const maxGap = _chunk6A4KYMQRcjs.safeMax.call(void 0, gaps);
6948
6951
  const avgLen = items.reduce((s, i) => s + i.text.length, 0) / items.length;
6949
6952
  if (items.length >= 4 && maxGap < 40 && avgLen < 5) return true;
6950
- const minGap = _chunk6ZRZLV3Jcjs.safeMin.call(void 0, gaps);
6953
+ const minGap = _chunk6A4KYMQRcjs.safeMin.call(void 0, gaps);
6951
6954
  return minGap > 0 && maxGap - minGap <= Math.max(2, maxGap * 0.15) && (items.filter((i) => /^\P{L}*\p{Ll}/u.test(i.text)).length * 2 >= items.length || items.some((i) => /\p{L}[,.;:]["”’)]?$/u.test(i.text)));
6952
6955
  }
6953
6956
  function detectColumns(yLines) {
6954
6957
  const allItems = yLines.flat();
6955
6958
  if (allItems.length === 0) return null;
6956
- const pageWidth = _chunk6ZRZLV3Jcjs.safeMax.call(void 0, allItems.map((i) => i.x + i.w)) - _chunk6ZRZLV3Jcjs.safeMin.call(void 0, allItems.map((i) => i.x));
6959
+ const pageWidth = _chunk6A4KYMQRcjs.safeMax.call(void 0, allItems.map((i) => i.x + i.w)) - _chunk6A4KYMQRcjs.safeMin.call(void 0, allItems.map((i) => i.x));
6957
6960
  if (pageWidth < 100) return null;
6958
6961
  let bigoLineIdx = -1;
6959
6962
  for (let i = 0; i < yLines.length; i++) {
@@ -7445,7 +7448,7 @@ function rebuildUnitLine(items, grid) {
7445
7448
  function prependUnitRow(cells, cols, line, usedItems) {
7446
7449
  const text = cleanCellText(line.map((item) => item.text).join(""));
7447
7450
  cells.unshift(Array.from({ length: cols }, (_, c) => ({ text: c === 0 ? text : "", colSpan: c === 0 ? cols : 1, rowSpan: 1 })));
7448
- for (const cell of cells[0]) _chunkN65EY5BIcjs.CELL_EDGES.set(cell, NO_EDGES);
7451
+ for (const cell of cells[0]) _chunkTPKP3WQUcjs.CELL_EDGES.set(cell, NO_EDGES);
7449
7452
  for (const item of line) usedItems.add(item);
7450
7453
  }
7451
7454
  function attachUnitRow(items, grid, cells, cols, usedItems) {
@@ -7977,7 +7980,7 @@ function recordFrameTitle(table, source, pageNum) {
7977
7980
  const [label, spacer, title] = cells;
7978
7981
  if (!/^(?:붙임|별첨|첨부|부록)\s*\d{1,3}$/.test(label.text.trim()) || spacer.text.trim() || !title.text.trim()) return;
7979
7982
  if (cells.some((c) => {
7980
- const e = _chunkN65EY5BIcjs.CELL_EDGES.get(c);
7983
+ const e = _chunkTPKP3WQUcjs.CELL_EDGES.get(c);
7981
7984
  return !e || e.t || e.b || e.l || e.r;
7982
7985
  })) return;
7983
7986
  const xs = TABLE_COLXS.get(table), style = dominantStyle(source), fs = _nullishCoalesce(_optionalChain([style, 'optionalAccess', _358 => _358.fontSize]), () => ( 0));
@@ -8558,7 +8561,7 @@ function extractBlocksWithGrids(items, pageNum, pageWidth, pageHeight, grids, ho
8558
8561
  groupSizes.push(1);
8559
8562
  }
8560
8563
  const allY = remaining.map((i) => i.y);
8561
- const pageH = _chunk6ZRZLV3Jcjs.safeMax.call(void 0, allY) - _chunk6ZRZLV3Jcjs.safeMin.call(void 0, allY);
8564
+ const pageH = _chunk6A4KYMQRcjs.safeMax.call(void 0, allY) - _chunk6A4KYMQRcjs.safeMin.call(void 0, allY);
8562
8565
  const gapThreshold = Math.max(15, pageH * 0.03);
8563
8566
  const sides = frameColumnTextBands(remaining, spanning, gx);
8564
8567
  const textBlocks = [];
@@ -8574,7 +8577,7 @@ function extractBlocksWithGrids(items, pageNum, pageWidth, pageHeight, grids, ho
8574
8577
  finalTextBlocks.push(...detectListBlocks(textBlocks));
8575
8578
  } else {
8576
8579
  const allY = remaining.map((i) => i.y);
8577
- const pageH = _chunk6ZRZLV3Jcjs.safeMax.call(void 0, allY) - _chunk6ZRZLV3Jcjs.safeMin.call(void 0, allY);
8580
+ const pageH = _chunk6A4KYMQRcjs.safeMax.call(void 0, allY) - _chunk6A4KYMQRcjs.safeMin.call(void 0, allY);
8578
8581
  const gapThreshold = Math.max(15, pageH * 0.03);
8579
8582
  const groups = xyCutOrder(remaining, gapThreshold);
8580
8583
  const textBlocks = [];
@@ -8902,7 +8905,7 @@ function extractPageBlocksFallback(items, pageNum, fullPage = false, detectTable
8902
8905
  blocks.push(...columnTextToBlocks(tableText, pageNum, bbox, dominantStyle(items)));
8903
8906
  } else {
8904
8907
  const allY = items.map((i) => i.y);
8905
- const pageHeight = _chunk6ZRZLV3Jcjs.safeMax.call(void 0, allY) - _chunk6ZRZLV3Jcjs.safeMin.call(void 0, allY);
8908
+ const pageHeight = _chunk6A4KYMQRcjs.safeMax.call(void 0, allY) - _chunk6A4KYMQRcjs.safeMin.call(void 0, allY);
8906
8909
  const gapThreshold = Math.max(15, pageHeight * 0.03);
8907
8910
  const orderedGroups = proseCutX !== null ? splitTwoColumnProse(items, proseCutX) : xyCutOrder(items, gapThreshold, 0, void 0, minGap);
8908
8911
  for (const group of orderedGroups) {
@@ -9118,4 +9121,4 @@ function rowParagraphs(block) {
9118
9121
 
9119
9122
 
9120
9123
  exports.DEFAULT_OCR_MODELS = DEFAULT_OCR_MODELS; exports.resolveOcrModels = resolveOcrModels; exports.ocrSelectionKey = ocrSelectionKey; exports.ocrSourcePath = ocrSourcePath; exports.ensureOcrModelsFor = ensureOcrModelsFor; exports.ocrModelsCached = ocrModelsCached; exports.parseCharacterDict = parseCharacterDict; exports.extractLines = extractLines; exports.extractImageRegions = extractImageRegions; exports.NO_EDGES = NO_EDGES; exports.hasOpenCellSeam = hasOpenCellSeam; exports.joinCellEdges = joinCellEdges; exports.filterHiddenText = filterHiddenText; exports.collapseEvenSpacing = collapseEvenSpacing; exports.computeBBox = computeBBox; exports.dominantStyle = dominantStyle; exports.normalizeItems = normalizeItems; exports.groupByY = groupByY; exports.mergeLineSimple = mergeLineSimple; exports.WrapLexicon = WrapLexicon; exports.startsNewItem = startsNewItem; exports.wrapJoiner = wrapJoiner; exports.joinPageBreakWraps = joinPageBreakWraps; exports.splitPageBreakWraps = splitPageBreakWraps; exports.latinSoftWrap = latinSoftWrap; exports.TOC_BLOCKS = TOC_BLOCKS; exports.computeMedianFontSizeFromFreq = computeMedianFontSizeFromFreq; exports.detectHeadings = detectHeadings; exports.mergeStackedHeadingLines = mergeStackedHeadingLines; exports.detectTypographyHeadings = detectTypographyHeadings; exports.detectDocumentStyleHeadings = detectDocumentStyleHeadings; exports.detectSiblingStyleHeadings = detectSiblingStyleHeadings; exports.detectRepeatedPageLabels = detectRepeatedPageLabels; exports.detectPageLeadHeadings = detectPageLeadHeadings; exports.refineDocumentStyleHeadings = refineDocumentStyleHeadings; exports.detectMarkerHeadings = detectMarkerHeadings; exports.detectTableCaptions = detectTableCaptions; exports.detectKoreanListBlocks = detectKoreanListBlocks; exports.removeHeaderFooterBlocks = removeHeaderFooterBlocks; exports.runningLinePatterns = runningLinePatterns; exports.stripAbsorbedRunningLines = stripAbsorbedRunningLines; exports.CLIP_TABLES = CLIP_TABLES; exports.FRAME_TITLE_BLOCKS = FRAME_TITLE_BLOCKS; exports.TABLE_COLXS = TABLE_COLXS; exports.TABLE_ROWYS = TABLE_ROWYS; exports.TABLE_TAIL = TABLE_TAIL; exports.PART_COLXS = PART_COLXS; exports.EMPTY_PARTS = EMPTY_PARTS; exports.IMAGE_CELLS = IMAGE_CELLS; exports.FILLER_CELLS = FILLER_CELLS; exports.CONT_PARTS = CONT_PARTS; exports.CELL_LINES = CELL_LINES; exports.ROW_RULES = ROW_RULES; exports.trimTrailingEmptyTableCols = trimTrailingEmptyTableCols; exports.restoreImageBullets = restoreImageBullets; exports.detectRightArrowRegions = detectRightArrowRegions; exports.removeSideTabs = removeSideTabs; exports.extractPageBlocksWithLines = extractPageBlocksWithLines; exports.UNRULED_REGION_TABLES = UNRULED_REGION_TABLES; exports.mergeOcrImageRegions = mergeOcrImageRegions;
9121
- //# sourceMappingURL=chunk-JILIA5EV.cjs.map
9124
+ //# sourceMappingURL=chunk-LJ7U7MFN.cjs.map