kordoc 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +176 -176
  3. package/README.md +962 -958
  4. package/THIRD_PARTY/apache-2.0.LICENSE +201 -201
  5. package/THIRD_PARTY/claw-hwp.LICENSE +21 -21
  6. package/THIRD_PARTY/hml-equation-parser.LICENSE +201 -201
  7. package/THIRD_PARTY/hml-equation-parser.txt +35 -35
  8. package/THIRD_PARTY/opendataloader-pdf.txt +32 -32
  9. package/THIRD_PARTY/pix2text.txt +17 -17
  10. package/THIRD_PARTY/rhwp-forms.txt +35 -35
  11. package/dist/{-DEVLQKIT.js → -MK2223RL.js} +12 -12
  12. package/dist/{chunk-3FFPHTXD.js → chunk-3H5X63BQ.js} +1 -1
  13. package/dist/chunk-3H5X63BQ.js.map +1 -0
  14. package/dist/{chunk-IBVMJIHR.js → chunk-3VTMSSDW.js} +2 -2
  15. package/dist/chunk-3VTMSSDW.js.map +1 -0
  16. package/dist/{chunk-HPRGVU6A.cjs → chunk-5QTHHQQE.cjs} +6 -6
  17. package/dist/chunk-5QTHHQQE.cjs.map +1 -0
  18. package/dist/{chunk-6GBLFQMA.js → chunk-5RPYBC2Q.js} +1 -1
  19. package/dist/chunk-5RPYBC2Q.js.map +1 -0
  20. package/dist/{chunk-7BTO44Q7.js → chunk-6PIR74EA.js} +13 -13
  21. package/dist/chunk-6PIR74EA.js.map +1 -0
  22. package/dist/{chunk-W34QSJFF.js → chunk-7FCNOKOR.js} +1 -1
  23. package/dist/chunk-7FCNOKOR.js.map +1 -0
  24. package/dist/{chunk-MSMSHU2F.js → chunk-AEYNY6FG.js} +3 -3
  25. package/dist/chunk-AEYNY6FG.js.map +1 -0
  26. package/dist/{chunk-KC63W72C.js → chunk-AFPD74JJ.js} +2 -2
  27. package/dist/chunk-AFPD74JJ.js.map +1 -0
  28. package/dist/{chunk-IHK5LCYX.js → chunk-BCG2PKQU.js} +2 -2
  29. package/dist/chunk-BCG2PKQU.js.map +1 -0
  30. package/dist/{chunk-I42DND3B.cjs → chunk-C27PPXRZ.cjs} +342 -29
  31. package/dist/chunk-C27PPXRZ.cjs.map +1 -0
  32. package/dist/{chunk-X34YWRL5.cjs → chunk-DCZVOIEO.cjs} +1 -1
  33. package/dist/chunk-DCZVOIEO.cjs.map +1 -0
  34. package/dist/{chunk-O5EK52TG.js → chunk-DZIXKL7E.js} +1 -1
  35. package/dist/chunk-DZIXKL7E.js.map +1 -0
  36. package/dist/{chunk-AIQ3ISQU.js → chunk-GE43BE46.js} +1 -1
  37. package/dist/chunk-GE43BE46.js.map +1 -0
  38. package/dist/{chunk-2CENWSAW.js → chunk-GIWKBPII.js} +335 -22
  39. package/dist/chunk-GIWKBPII.js.map +1 -0
  40. package/dist/chunk-GS7T56RP.cjs.map +1 -1
  41. package/dist/{chunk-6XPXIG57.js → chunk-GZDOWYJM.js} +2 -2
  42. package/dist/chunk-GZDOWYJM.js.map +1 -0
  43. package/dist/{chunk-OPL22SYF.js → chunk-IDHBO6AG.js} +3 -3
  44. package/dist/chunk-IDHBO6AG.js.map +1 -0
  45. package/dist/{chunk-3TBUDJDE.js → chunk-MOL7MDBG.js} +1 -1
  46. package/dist/chunk-MOL7MDBG.js.map +1 -0
  47. package/dist/{chunk-PSAHY5XW.js → chunk-MVTX6ODZ.js} +1 -1
  48. package/dist/chunk-MVTX6ODZ.js.map +1 -0
  49. package/dist/{chunk-2CABQYTT.js → chunk-P2NWNJMR.js} +3 -3
  50. package/dist/chunk-P2NWNJMR.js.map +1 -0
  51. package/dist/{chunk-PQDNCWAZ.js → chunk-PEVPKD6Z.js} +336 -23
  52. package/dist/chunk-PEVPKD6Z.js.map +1 -0
  53. package/dist/{chunk-UQ4NSK66.js → chunk-PV2EXXGI.js} +3 -3
  54. package/dist/chunk-PV2EXXGI.js.map +1 -0
  55. package/dist/{chunk-WX7WQTOT.cjs → chunk-TUU535RB.cjs} +2 -2
  56. package/dist/chunk-TUU535RB.cjs.map +1 -0
  57. package/dist/{chunk-YNJDNP44.js → chunk-UAFSPWU7.js} +1 -1
  58. package/dist/chunk-UAFSPWU7.js.map +1 -0
  59. package/dist/{chunk-CARNWWGI.cjs → chunk-W5KRKBYR.cjs} +4 -4
  60. package/dist/chunk-W5KRKBYR.cjs.map +1 -0
  61. package/dist/{chunk-VNEV6VKD.js → chunk-XZDJXYUC.js} +2 -2
  62. package/dist/chunk-XZDJXYUC.js.map +1 -0
  63. package/dist/{chunk-UGKXJA3W.js → chunk-YUVKU5CW.js} +2 -2
  64. package/dist/chunk-YUVKU5CW.js.map +1 -0
  65. package/dist/{chunk-KQOTL4WI.js → chunk-YXXHH6WC.js} +2 -2
  66. package/dist/chunk-YXXHH6WC.js.map +1 -0
  67. package/dist/{chunk-MGGU3ZRK.cjs → chunk-ZV5OKQ3Z.cjs} +1 -1
  68. package/dist/chunk-ZV5OKQ3Z.cjs.map +1 -0
  69. package/dist/{chunk-ZYQ5X6PG.js → chunk-ZYVULRYF.js} +2 -2
  70. package/dist/chunk-ZYVULRYF.js.map +1 -0
  71. package/dist/chunks-4HEG6ZPG.js +10 -0
  72. package/dist/cli.js +24 -24
  73. package/dist/cli.js.map +1 -1
  74. package/dist/{formula-WKGP3D3Y.cjs → formula-CG4S7TWO.cjs} +8 -8
  75. package/dist/formula-CG4S7TWO.cjs.map +1 -0
  76. package/dist/{formula-ETB6MESN.js → formula-JLYS73QL.js} +2 -2
  77. package/dist/formula-JLYS73QL.js.map +1 -0
  78. package/dist/{formula-KW4NI5IO.js → formula-LIVQSCMM.js} +2 -2
  79. package/dist/formula-LIVQSCMM.js.map +1 -0
  80. package/dist/{image-ocr-C6U7YHRC.js → image-ocr-3A3HZX6O.js} +5 -5
  81. package/dist/image-ocr-3A3HZX6O.js.map +1 -0
  82. package/dist/{image-ocr-YIBJCOUQ.js → image-ocr-4MHEX4MN.js} +7 -7
  83. package/dist/image-ocr-4MHEX4MN.js.map +1 -0
  84. package/dist/{image-ocr-YID6TDN5.cjs → image-ocr-OB5PCQUU.cjs} +11 -11
  85. package/dist/image-ocr-OB5PCQUU.cjs.map +1 -0
  86. package/dist/index.cjs +185 -185
  87. package/dist/index.cjs.map +1 -1
  88. package/dist/index.js +7 -7
  89. package/dist/index.js.map +1 -1
  90. package/dist/mcp.js +20 -20
  91. package/dist/mcp.js.map +1 -1
  92. package/dist/{models-6VMAVZR6.js → models-6SIKLKRH.js} +3 -3
  93. package/dist/page-range-737B4EZW.js +8 -0
  94. package/dist/page-range-LNMXJU6W.js +7 -0
  95. package/dist/page-range-P7SDW6LR.cjs +8 -0
  96. package/dist/page-range-P7SDW6LR.cjs.map +1 -0
  97. package/dist/{parser-GFXZV2VJ.js → parser-EIVCD4NS.js} +10 -10
  98. package/dist/parser-EIVCD4NS.js.map +1 -0
  99. package/dist/{parser-SHM3JEH6.js → parser-EQN4YKU4.js} +8 -8
  100. package/dist/parser-EQN4YKU4.js.map +1 -0
  101. package/dist/{parser-EYCVDUJA.cjs → parser-VACM3DFR.cjs} +28 -28
  102. package/dist/parser-VACM3DFR.cjs.map +1 -0
  103. package/dist/pdf-ocr-N62EWEX4.js +12 -0
  104. package/dist/pdf-ocr-NPYDUIC5.js +15 -0
  105. package/dist/pdf-ocr-OBPQ4F4D.cjs +13 -0
  106. package/dist/pdf-ocr-OBPQ4F4D.cjs.map +1 -0
  107. package/dist/{profile-io-FQZNNA3O.js → profile-io-L5EH4JAD.js} +3 -3
  108. package/dist/{rasterize-QGKFNTXO.js → rasterize-7AR662AL.js} +2 -2
  109. package/dist/rasterize-7AR662AL.js.map +1 -0
  110. package/dist/{redact-W5K3ILQH.js → redact-7ILEAUTS.js} +2 -2
  111. package/dist/render-CAFQT7S4.js +10 -0
  112. package/dist/seal-ESAC3IZR.js +10 -0
  113. package/dist/{setup-I4I75QJU.js → setup-GUQT5ELJ.js} +1 -1
  114. package/dist/setup-GUQT5ELJ.js.map +1 -0
  115. package/dist/{watch-OIVXH2RN.js → watch-MONT6CGI.js} +12 -12
  116. package/dist/watch-MONT6CGI.js.map +1 -0
  117. package/package.json +103 -103
  118. package/templates//352/260/204/354/235/264/352/270/260/354/225/210/353/254/270_/354/230/210/354/213/234/352/260/222.json +9 -9
  119. package/templates//354/235/274/353/260/230/352/270/260/354/225/210/353/254/270_/354/230/210/354/213/234/352/260/222.json +24 -24
  120. package/dist/chunk-2CABQYTT.js.map +0 -1
  121. package/dist/chunk-2CENWSAW.js.map +0 -1
  122. package/dist/chunk-3FFPHTXD.js.map +0 -1
  123. package/dist/chunk-3TBUDJDE.js.map +0 -1
  124. package/dist/chunk-6GBLFQMA.js.map +0 -1
  125. package/dist/chunk-6XPXIG57.js.map +0 -1
  126. package/dist/chunk-7BTO44Q7.js.map +0 -1
  127. package/dist/chunk-AIQ3ISQU.js.map +0 -1
  128. package/dist/chunk-CARNWWGI.cjs.map +0 -1
  129. package/dist/chunk-HPRGVU6A.cjs.map +0 -1
  130. package/dist/chunk-I42DND3B.cjs.map +0 -1
  131. package/dist/chunk-IBVMJIHR.js.map +0 -1
  132. package/dist/chunk-IHK5LCYX.js.map +0 -1
  133. package/dist/chunk-KC63W72C.js.map +0 -1
  134. package/dist/chunk-KQOTL4WI.js.map +0 -1
  135. package/dist/chunk-MGGU3ZRK.cjs.map +0 -1
  136. package/dist/chunk-MSMSHU2F.js.map +0 -1
  137. package/dist/chunk-O5EK52TG.js.map +0 -1
  138. package/dist/chunk-OPL22SYF.js.map +0 -1
  139. package/dist/chunk-PQDNCWAZ.js.map +0 -1
  140. package/dist/chunk-PSAHY5XW.js.map +0 -1
  141. package/dist/chunk-UGKXJA3W.js.map +0 -1
  142. package/dist/chunk-UQ4NSK66.js.map +0 -1
  143. package/dist/chunk-VNEV6VKD.js.map +0 -1
  144. package/dist/chunk-W34QSJFF.js.map +0 -1
  145. package/dist/chunk-WX7WQTOT.cjs.map +0 -1
  146. package/dist/chunk-X34YWRL5.cjs.map +0 -1
  147. package/dist/chunk-YNJDNP44.js.map +0 -1
  148. package/dist/chunk-ZYQ5X6PG.js.map +0 -1
  149. package/dist/chunks-Y2VE2WCJ.js +0 -10
  150. package/dist/formula-ETB6MESN.js.map +0 -1
  151. package/dist/formula-KW4NI5IO.js.map +0 -1
  152. package/dist/formula-WKGP3D3Y.cjs.map +0 -1
  153. package/dist/image-ocr-C6U7YHRC.js.map +0 -1
  154. package/dist/image-ocr-YIBJCOUQ.js.map +0 -1
  155. package/dist/image-ocr-YID6TDN5.cjs.map +0 -1
  156. package/dist/page-range-CIRRJPXJ.js +0 -7
  157. package/dist/page-range-OF5I4PQY.js +0 -8
  158. package/dist/page-range-TPIRSA3J.cjs +0 -8
  159. package/dist/page-range-TPIRSA3J.cjs.map +0 -1
  160. package/dist/parser-EYCVDUJA.cjs.map +0 -1
  161. package/dist/parser-GFXZV2VJ.js.map +0 -1
  162. package/dist/parser-SHM3JEH6.js.map +0 -1
  163. package/dist/pdf-ocr-TMAWPOHC.cjs +0 -13
  164. package/dist/pdf-ocr-TMAWPOHC.cjs.map +0 -1
  165. package/dist/pdf-ocr-UKFYODLP.js +0 -15
  166. package/dist/pdf-ocr-ZG4UVUIT.js +0 -12
  167. package/dist/rasterize-QGKFNTXO.js.map +0 -1
  168. package/dist/render-YFIWQYVB.js +0 -10
  169. package/dist/seal-GZ7NDWLR.js +0 -10
  170. package/dist/setup-I4I75QJU.js.map +0 -1
  171. package/dist/watch-OIVXH2RN.js.map +0 -1
  172. /package/dist/{-DEVLQKIT.js.map → -MK2223RL.js.map} +0 -0
  173. /package/dist/{chunks-Y2VE2WCJ.js.map → chunks-4HEG6ZPG.js.map} +0 -0
  174. /package/dist/{models-6VMAVZR6.js.map → models-6SIKLKRH.js.map} +0 -0
  175. /package/dist/{page-range-CIRRJPXJ.js.map → page-range-737B4EZW.js.map} +0 -0
  176. /package/dist/{page-range-OF5I4PQY.js.map → page-range-LNMXJU6W.js.map} +0 -0
  177. /package/dist/{pdf-ocr-UKFYODLP.js.map → pdf-ocr-N62EWEX4.js.map} +0 -0
  178. /package/dist/{pdf-ocr-ZG4UVUIT.js.map → pdf-ocr-NPYDUIC5.js.map} +0 -0
  179. /package/dist/{profile-io-FQZNNA3O.js.map → profile-io-L5EH4JAD.js.map} +0 -0
  180. /package/dist/{redact-W5K3ILQH.js.map → redact-7ILEAUTS.js.map} +0 -0
  181. /package/dist/{render-YFIWQYVB.js.map → render-CAFQT7S4.js.map} +0 -0
  182. /package/dist/{seal-GZ7NDWLR.js.map → seal-ESAC3IZR.js.map} +0 -0
@@ -30,4 +30,4 @@ function parsePageRange(spec, maxPages) {
30
30
 
31
31
 
32
32
  exports.parsePageRange = parsePageRange;
33
- //# sourceMappingURL=chunk-X34YWRL5.cjs.map
33
+ //# sourceMappingURL=chunk-DCZVOIEO.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/mong-e/workspace/kordoc/dist/chunk-DCZVOIEO.cjs","../src/page-range.ts"],"names":[],"mappings":"AAAA;ACSO,SAAS,cAAA,CAAe,IAAA,EAAyB,QAAA,EAA+B;AACrF,EAAA,MAAM,OAAA,kBAAS,IAAI,GAAA,CAAY,CAAA;AAC/B,EAAA,GAAA,CAAI,SAAA,GAAY,CAAA,EAAG,OAAO,MAAA;AAE1B,EAAA,GAAA,CAAI,KAAA,CAAM,OAAA,CAAQ,IAAI,CAAA,EAAG;AACvB,IAAA,IAAA,CAAA,MAAW,EAAA,GAAK,IAAA,EAAM;AACpB,MAAA,MAAM,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AACzB,MAAA,GAAA,CAAI,KAAA,GAAQ,EAAA,GAAK,KAAA,GAAQ,QAAA,EAAU,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA;AAAA,IACpD;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,GAAA,CAAI,OAAO,KAAA,IAAS,SAAA,GAAY,IAAA,CAAK,IAAA,CAAK,EAAA,IAAM,EAAA,EAAI,OAAO,MAAA;AAE3D,EAAA,MAAM,MAAA,EAAQ,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC5B,EAAA,IAAA,CAAA,MAAW,KAAA,GAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,QAAA,EAAU,IAAA,CAAK,IAAA,CAAK,CAAA;AAC1B,IAAA,GAAA,CAAI,CAAC,OAAA,EAAS,QAAA;AAEd,IAAA,MAAM,WAAA,EAAa,OAAA,CAAQ,KAAA,CAAM,qBAAqB,CAAA;AACtD,IAAA,GAAA,CAAI,UAAA,EAAY;AACd,MAAA,MAAM,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,QAAA,CAAS,UAAA,CAAW,CAAC,CAAA,EAAG,EAAE,CAAC,CAAA;AACrD,MAAA,MAAM,IAAA,EAAM,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,QAAA,CAAS,UAAA,CAAW,CAAC,CAAA,EAAG,EAAE,CAAC,CAAA;AAC1D,MAAA,IAAA,CAAA,IAAS,EAAA,EAAI,KAAA,EAAO,EAAA,GAAK,GAAA,EAAK,CAAA,EAAA,EAAK,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA;AAAA,IACjD,EAAA,KAAO;AACL,MAAA,MAAM,KAAA,EAAO,QAAA,CAAS,OAAA,EAAS,EAAE,CAAA;AACjC,MAAA,GAAA,CAAI,CAAC,KAAA,CAAM,IAAI,EAAA,GAAK,KAAA,GAAQ,EAAA,GAAK,KAAA,GAAQ,QAAA,EAAU,MAAA,CAAO,GAAA,CAAI,IAAI,CAAA;AAAA,IACpE;AAAA,EACF;AAEA,EAAA,OAAO,MAAA;AACT;ADZA;AACA;AACE;AACF,wCAAC","file":"/Users/mong-e/workspace/kordoc/dist/chunk-DCZVOIEO.cjs","sourcesContent":[null,"/** 페이지/섹션 범위 파싱 유틸리티 */\n\n/**\n * 페이지 범위 지정을 1-based Set<number>로 변환.\n *\n * @param spec - [1,2,3] 또는 \"1-3\" 또는 \"1,3,5-7\"\n * @param maxPages - 최대 페이지 수 (클램핑 상한)\n * @returns 1-based 페이지 번호 Set\n */\nexport function parsePageRange(spec: number[] | string, maxPages: number): Set<number> {\n const result = new Set<number>()\n if (maxPages <= 0) return result\n\n if (Array.isArray(spec)) {\n for (const n of spec) {\n const page = Math.round(n)\n if (page >= 1 && page <= maxPages) result.add(page)\n }\n return result\n }\n\n if (typeof spec !== \"string\" || spec.trim() === \"\") return result\n\n const parts = spec.split(\",\")\n for (const part of parts) {\n const trimmed = part.trim()\n if (!trimmed) continue\n\n const rangeMatch = trimmed.match(/^(\\d+)\\s*-\\s*(\\d+)$/)\n if (rangeMatch) {\n const start = Math.max(1, parseInt(rangeMatch[1], 10))\n const end = Math.min(maxPages, parseInt(rangeMatch[2], 10))\n for (let i = start; i <= end; i++) result.add(i)\n } else {\n const page = parseInt(trimmed, 10)\n if (!isNaN(page) && page >= 1 && page <= maxPages) result.add(page)\n }\n }\n\n return result\n}\n"]}
@@ -142,4 +142,4 @@ export {
142
142
  redactText,
143
143
  redactMarkdown
144
144
  };
145
- //# sourceMappingURL=chunk-O5EK52TG.js.map
145
+ //# sourceMappingURL=chunk-DZIXKL7E.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/redact.ts"],"sourcesContent":["/**\n * 한국 공문서 PII(개인정보) 탐지·마스킹 순수 로직.\n *\n * 텍스트 in → 마스킹된 텍스트 + 히트 리포트 out. 문서 파싱/patch는 기존 인프라가\n * 담당하고 여기서는 텍스트만 다룬다 (CLI/MCP 배선은 별도).\n *\n * 원칙:\n * - 서식 보존 마스킹 — 자릿수·구분자를 유지해 마스킹 전후 길이가 동일\n * - 히트 리포트에 원본 PII를 절대 담지 않는다 (`masked` 필드만 존재)\n * - 룰 우선순위 겹침 처리 — 우선순위순으로 매치를 수집하고, 이미 점유된\n * 구간과 겹치는 하위 룰 매치는 스킵 (RULE_PRIORITY 참조)\n * - 정규식은 모듈 로드 시 1회 컴파일 (matchAll은 내부 클론이라 lastIndex 안전)\n */\n\nexport type RedactRule = \"rrn\" | \"phone\" | \"email\" | \"card\" | \"account\" | \"passport\" | \"driver\"\n\nexport interface RedactHit {\n rule: RedactRule\n /** 마스킹 후 문자열 — 원본 PII는 리포트에 담지 않는다 */\n masked: string\n /** 원문 내 시작 오프셋 (UTF-16 단위) */\n index: number\n /** 매치 길이 (서식 보존이라 마스킹 전후 동일) */\n length: number\n}\n\nexport interface RedactTextResult {\n text: string\n hits: RedactHit[]\n}\n\nexport interface RedactOptions {\n /** 적용할 룰 (기본: DEFAULT_REDACT_RULES — passport·driver는 기본 OFF) */\n rules?: RedactRule[]\n /** 마스크 문자 — 1글자(UTF-16 1단위), 영숫자 금지. 기본 \"●\" (마크다운 특수문자 아님) */\n maskChar?: string\n}\n\n/** 기본 적용 룰 — passport(여권)·driver(운전면허)는 오탐 여지가 있어 opt-in */\nexport const DEFAULT_REDACT_RULES: readonly RedactRule[] = [\n \"rrn\",\n \"phone\",\n \"email\",\n \"card\",\n \"account\",\n]\n\n/**\n * 룰 우선순위 (앞이 높음). 스펙 요구는 rrn > card > phone > account —\n * 나머지는 포섭 관계로 배치: email을 phone 앞에(로컬파트 숫자에 전화 패턴 오탐 방지),\n * driver를 account 앞에(면허번호 12자리 4그룹이 계좌 패턴에 포섭됨).\n */\nconst RULE_PRIORITY: readonly RedactRule[] = [\n \"rrn\",\n \"email\",\n \"card\",\n \"phone\",\n \"driver\",\n \"account\",\n \"passport\",\n]\n\ninterface RuleDef {\n pattern: RegExp\n /** 오탐 필터 — false면 매치 스킵 */\n validate?: (m: RegExpMatchArray) => boolean\n /** 매치 → 같은 길이의 마스킹 문자열 */\n mask: (m: RegExpMatchArray, mc: string) => string\n}\n\n/** Luhn 체크섬 (카드번호 오탐 축소) */\nfunction luhnValid(digits: string): boolean {\n let sum = 0\n for (let i = 0; i < digits.length; i++) {\n let d = digits.charCodeAt(digits.length - 1 - i) - 48\n if (i % 2 === 1) {\n d *= 2\n if (d > 9) d -= 9\n }\n sum += d\n }\n return sum % 10 === 0\n}\n\n/** 주민번호 앞 6자리(YYMMDD)의 월 01-12, 일 01-31 검증 */\nfunction birthdateValid(front6: string): boolean {\n const mm = Number(front6.slice(2, 4))\n const dd = Number(front6.slice(4, 6))\n return mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31\n}\n\n// 전체 정규식은 모듈 로드 시 1회 컴파일\nconst RULES: Record<RedactRule, RuleDef> = {\n // 주민/외국인등록번호 — 뒷자리 첫 숫자 1-8 + 생년월일 유효성으로 오탐 축소.\n // 유니코드 대시 변형(‐ ‑ – —)은 rrn만 허용. 앞 6자리 유지, 뒤 7자리 전부 마스크.\n rrn: {\n pattern: /(?<!\\d)(\\d{6})([-‐‑–—])([1-8]\\d{6})(?!\\d)/g,\n validate: (m) => birthdateValid(m[1]),\n mask: (m, mc) => m[1] + m[2] + mc.repeat(7),\n },\n // 이메일 — 로컬파트 첫 글자만 남기고 마스크, 도메인 유지\n email: {\n pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g,\n mask: (m, mc) => {\n const at = m[0].indexOf(\"@\")\n return m[0][0] + mc.repeat(at - 1) + m[0].slice(at)\n },\n },\n // 카드번호 — 구분자 필수(무구분 16자리는 오탐 높아 제외), 동일 구분자 강제(\\2),\n // Luhn 체크. 가운데 8자리 마스크.\n card: {\n pattern: /(?<!\\d)(\\d{4})([- ])(\\d{4})\\2(\\d{4})\\2(\\d{4})(?!\\d)/g,\n validate: (m) => luhnValid(m[1] + m[3] + m[4] + m[5]),\n mask: (m, mc) => m[1] + m[2] + mc.repeat(4) + m[2] + mc.repeat(4) + m[2] + m[5],\n },\n // 전화번호 — 휴대폰(01[016789])·서울(02)·지역(0[3-6]\\d)·인터넷(070)은 구분자\n // -·.·공백 또는 무구분(동일 구분자 강제), 대표번호(15xx/16xx/18xx)는 구분자 필수.\n // 가운데 자리만 마스크 (대표번호는 뒤 4자리). 선행 [\\d-] 금지 — 계좌 부분매치 방지.\n phone: {\n pattern:\n /(?<![\\d-])(?:(01[016789]|070|02|0[3-6]\\d)([-. ]?)(\\d{3,4})\\2(\\d{4})|(1[568]\\d{2})([-. ])(\\d{4}))(?!\\d)/g,\n mask: (m, mc) =>\n m[1] !== undefined\n ? m[1] + m[2] + mc.repeat(m[3].length) + m[2] + m[4]\n : m[5] + m[6] + mc.repeat(4),\n },\n // 운전면허 (기본 OFF) — 신형 12자리만 (지역명 2글자 선행 구버전은 스킵). 뒷 8자리 마스크.\n driver: {\n pattern: /(?<![\\d-])(\\d{2})-(\\d{2})-\\d{6}-\\d{2}(?!-?\\d)/g,\n mask: (m, mc) => m[1] + \"-\" + m[2] + \"-\" + mc.repeat(6) + \"-\" + mc.repeat(2),\n },\n // 계좌번호 — 3~4그룹 + 총 자릿수 10~16. rrn·card·phone과 겹치면 그쪽 우선.\n // 마지막 그룹 빼고 전부 마스크. 사업자등록번호(3-2-5, 10자리)도 걸린다 — 계약상 의도\n // (테스트로 명시). 날짜(2026-07-16)는 8자리라 총자릿수 검증에서 탈락.\n account: {\n pattern: /(?<!\\d)(?<!\\d-)\\d{2,6}(?:-\\d{2,6}){1,2}-\\d{2,8}(?!-?\\d)/g,\n validate: (m) => {\n const digits = m[0].replace(/-/g, \"\").length\n return digits >= 10 && digits <= 16\n },\n mask: (m, mc) => {\n const parts = m[0].split(\"-\")\n return parts.map((p, i) => (i === parts.length - 1 ? p : mc.repeat(p.length))).join(\"-\")\n },\n },\n // 여권번호 (기본 OFF) — 단어 경계, 첫 글자만 남기고 전부 마스크\n passport: {\n pattern: /\\b([MSRODG])\\d{8}(?![0-9A-Za-z])/g,\n mask: (m, mc) => m[1] + mc.repeat(8),\n },\n}\n\n/**\n * 텍스트에서 PII를 탐지해 서식 보존 마스킹.\n *\n * @param text - 대상 텍스트 (마크다운 포함)\n * @param options - 룰 선택·마스크 문자 (기본: rrn·phone·email·card·account, \"●\")\n * @returns 마스킹된 텍스트 + 히트 리포트 (index 오름차순, 원본 PII 미포함)\n */\nexport function redactText(text: string, options?: RedactOptions): RedactTextResult {\n const maskChar = options?.maskChar ?? \"●\"\n if (maskChar.length !== 1 || /[0-9A-Za-z]/.test(maskChar)) {\n throw new Error(`maskChar는 영숫자가 아닌 1글자여야 함: ${JSON.stringify(maskChar)}`)\n }\n\n const enabled = options?.rules ?? DEFAULT_REDACT_RULES\n const hits: RedactHit[] = []\n if (text === \"\" || enabled.length === 0) return { text, hits }\n\n // 우선순위순으로 매치 수집 — 점유 구간과 겹치는 하위 룰 매치는 스킵\n const occupied: Array<{ start: number; end: number }> = []\n for (const rule of RULE_PRIORITY) {\n if (!enabled.includes(rule)) continue\n const def = RULES[rule]\n for (const m of text.matchAll(def.pattern)) {\n if (def.validate && !def.validate(m)) continue\n const start = m.index as number\n const end = start + m[0].length\n if (occupied.some((o) => start < o.end && end > o.start)) continue\n occupied.push({ start, end })\n hits.push({ rule, masked: def.mask(m, maskChar), index: start, length: m[0].length })\n }\n }\n\n hits.sort((a, b) => a.index - b.index)\n\n let out = \"\"\n let cursor = 0\n for (const h of hits) {\n out += text.slice(cursor, h.index) + h.masked\n cursor = h.index + h.length\n }\n out += text.slice(cursor)\n return { text: out, hits }\n}\n\n/**\n * 마크다운 문서 전용 래퍼 — base64 이미지(data URI) 라인은 마스킹에서 제외한다.\n * base64 페이로드의 숫자열이 phone 등에 오탐되면 이미지가 깨지기 때문.\n * hits의 index는 문서 전체 기준 절대 오프셋으로 환산된다.\n */\nexport function redactMarkdown(markdown: string, options?: RedactOptions): RedactTextResult {\n const lines = markdown.split(\"\\n\")\n const hits: RedactHit[] = []\n let offset = 0\n const outLines = lines.map((line) => {\n if (line.includes(\"data:image/\")) {\n offset += line.length + 1\n return line\n }\n const r = redactText(line, options)\n for (const h of r.hits) hits.push({ ...h, index: h.index + offset })\n offset += line.length + 1\n return r.text\n })\n return { text: outLines.join(\"\\n\"), hits }\n}\n"],"mappings":"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","names":[]}
@@ -30,4 +30,4 @@ function parsePageRange(spec, maxPages) {
30
30
  export {
31
31
  parsePageRange
32
32
  };
33
- //# sourceMappingURL=chunk-AIQ3ISQU.js.map
33
+ //# sourceMappingURL=chunk-GE43BE46.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/page-range.ts"],"sourcesContent":["/** 페이지/섹션 범위 파싱 유틸리티 */\n\n/**\n * 페이지 범위 지정을 1-based Set<number>로 변환.\n *\n * @param spec - [1,2,3] 또는 \"1-3\" 또는 \"1,3,5-7\"\n * @param maxPages - 최대 페이지 수 (클램핑 상한)\n * @returns 1-based 페이지 번호 Set\n */\nexport function parsePageRange(spec: number[] | string, maxPages: number): Set<number> {\n const result = new Set<number>()\n if (maxPages <= 0) return result\n\n if (Array.isArray(spec)) {\n for (const n of spec) {\n const page = Math.round(n)\n if (page >= 1 && page <= maxPages) result.add(page)\n }\n return result\n }\n\n if (typeof spec !== \"string\" || spec.trim() === \"\") return result\n\n const parts = spec.split(\",\")\n for (const part of parts) {\n const trimmed = part.trim()\n if (!trimmed) continue\n\n const rangeMatch = trimmed.match(/^(\\d+)\\s*-\\s*(\\d+)$/)\n if (rangeMatch) {\n const start = Math.max(1, parseInt(rangeMatch[1], 10))\n const end = Math.min(maxPages, parseInt(rangeMatch[2], 10))\n for (let i = start; i <= end; i++) result.add(i)\n } else {\n const page = parseInt(trimmed, 10)\n if (!isNaN(page) && page >= 1 && page <= maxPages) result.add(page)\n }\n }\n\n return result\n}\n"],"mappings":";AASO,SAAS,eAAe,MAAyB,UAA+B;AACrF,QAAM,SAAS,oBAAI,IAAY;AAC/B,MAAI,YAAY,EAAG,QAAO;AAE1B,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,eAAW,KAAK,MAAM;AACpB,YAAM,OAAO,KAAK,MAAM,CAAC;AACzB,UAAI,QAAQ,KAAK,QAAQ,SAAU,QAAO,IAAI,IAAI;AAAA,IACpD;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,SAAS,YAAY,KAAK,KAAK,MAAM,GAAI,QAAO;AAE3D,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AAEd,UAAM,aAAa,QAAQ,MAAM,qBAAqB;AACtD,QAAI,YAAY;AACd,YAAM,QAAQ,KAAK,IAAI,GAAG,SAAS,WAAW,CAAC,GAAG,EAAE,CAAC;AACrD,YAAM,MAAM,KAAK,IAAI,UAAU,SAAS,WAAW,CAAC,GAAG,EAAE,CAAC;AAC1D,eAAS,IAAI,OAAO,KAAK,KAAK,IAAK,QAAO,IAAI,CAAC;AAAA,IACjD,OAAO;AACL,YAAM,OAAO,SAAS,SAAS,EAAE;AACjC,UAAI,CAAC,MAAM,IAAI,KAAK,QAAQ,KAAK,QAAQ,SAAU,QAAO,IAAI,IAAI;AAAA,IACpE;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  HEADING_RATIO_H3,
5
5
  safeMax,
6
6
  safeMin
7
- } from "./chunk-UGKXJA3W.js";
7
+ } from "./chunk-YUVKU5CW.js";
8
8
 
9
9
  // src/pdf/image-regions.ts
10
10
  import { OPS } from "pdfjs-dist/legacy/build/pdf.mjs";
@@ -2681,6 +2681,149 @@ function findVerticalCut(items) {
2681
2681
  return { position, gap: largestGap };
2682
2682
  }
2683
2683
 
2684
+ // src/pdf/two-column.ts
2685
+ var GUTTER_SCAN_LO = 0.3;
2686
+ var GUTTER_SCAN_HI = 0.7;
2687
+ var GUTTER_MAX_SAMPLES = 400;
2688
+ var WIDE_RECT_RATIO = 0.55;
2689
+ var GUTTER_MAX_COVER_RATIO = 0.06;
2690
+ var SIDE_MIN_COUNT = 5;
2691
+ var SIDE_MIN_HEIGHT_RATIO = 0.3;
2692
+ var SIDE_MIN_VSPAN_RATIO = 0.45;
2693
+ var SIDE_MIN_WIDTH_SYMMETRY = 0.5;
2694
+ var MIN_CONTENT_SPAN = 300;
2695
+ var TEXT_RECT_MAX_H = 20;
2696
+ var ROW_Y_TOL = 2;
2697
+ var SIDE_MIN_TEXT_ROWS = 12;
2698
+ var CROSS_ROW_TINY_GAP = 12;
2699
+ var MAX_TINY_GAP_ROWS = 1;
2700
+ var MAX_BOTH_SIDE_ROW_RATIO = 0.65;
2701
+ var BLOCK_DOMINANT_TEXT_RECTS = 10;
2702
+ function detectColumnGutter(rects) {
2703
+ if (rects.length < 8) return null;
2704
+ let minX = Infinity;
2705
+ let maxX = -Infinity;
2706
+ for (const r of rects) {
2707
+ if (r.x < minX) minX = r.x;
2708
+ if (r.x + r.w > maxX) maxX = r.x + r.w;
2709
+ }
2710
+ const span = maxX - minX;
2711
+ if (!Number.isFinite(span) || span < MIN_CONTENT_SPAN) return null;
2712
+ const narrow = rects.filter((r) => r.w < span * WIDE_RECT_RATIO && r.w > 0 && r.h > 0);
2713
+ if (narrow.length < SIDE_MIN_COUNT * 2) return null;
2714
+ let totalH = 0;
2715
+ for (const r of narrow) totalH += r.h;
2716
+ if (totalH <= 0) return null;
2717
+ const lo = minX + span * GUTTER_SCAN_LO;
2718
+ const hi = minX + span * GUTTER_SCAN_HI;
2719
+ const step = Math.max(2, (hi - lo) / GUTTER_MAX_SAMPLES);
2720
+ let gutterX = 0;
2721
+ let bestCover = Infinity;
2722
+ for (let x = lo; x <= hi; x += step) {
2723
+ let cover = 0;
2724
+ for (const r of narrow) {
2725
+ if (r.x < x && r.x + r.w > x) cover += r.h;
2726
+ }
2727
+ if (cover < bestCover) {
2728
+ bestCover = cover;
2729
+ gutterX = x;
2730
+ }
2731
+ }
2732
+ if (bestCover > totalH * GUTTER_MAX_COVER_RATIO) return null;
2733
+ const left = [];
2734
+ const right = [];
2735
+ for (const r of narrow) {
2736
+ if (r.x + r.w <= gutterX) left.push(r);
2737
+ else if (r.x >= gutterX) right.push(r);
2738
+ }
2739
+ if (left.length < SIDE_MIN_COUNT || right.length < SIDE_MIN_COUNT) return null;
2740
+ const sideStats = (side) => {
2741
+ let sMinX = Infinity, sMaxR = -Infinity, sMinY = Infinity, sMaxT = -Infinity, hSum = 0;
2742
+ for (const r of side) {
2743
+ if (r.x < sMinX) sMinX = r.x;
2744
+ if (r.x + r.w > sMaxR) sMaxR = r.x + r.w;
2745
+ if (r.y < sMinY) sMinY = r.y;
2746
+ if (r.y + r.h > sMaxT) sMaxT = r.y + r.h;
2747
+ hSum += r.h;
2748
+ }
2749
+ return { width: sMaxR - sMinX, vspan: sMaxT - sMinY, minY: sMinY, maxT: sMaxT, hSum };
2750
+ };
2751
+ const L = sideStats(left);
2752
+ const R = sideStats(right);
2753
+ if (Math.min(L.hSum, R.hSum) / Math.max(L.hSum, R.hSum) < SIDE_MIN_HEIGHT_RATIO) return null;
2754
+ const unionVspan = Math.max(L.maxT, R.maxT) - Math.min(L.minY, R.minY);
2755
+ if (unionVspan <= 0) return null;
2756
+ if (L.vspan / unionVspan < SIDE_MIN_VSPAN_RATIO) return null;
2757
+ if (R.vspan / unionVspan < SIDE_MIN_VSPAN_RATIO) return null;
2758
+ if (Math.min(L.width, R.width) / Math.max(L.width, R.width) < SIDE_MIN_WIDTH_SYMMETRY) return null;
2759
+ const textRects = (side) => side.filter((r) => r.h <= TEXT_RECT_MAX_H);
2760
+ const lText = textRects(left);
2761
+ const rText = textRects(right);
2762
+ if (lText.length + rText.length >= BLOCK_DOMINANT_TEXT_RECTS) {
2763
+ const rows = /* @__PURE__ */ new Map();
2764
+ const rowKey = (y) => Math.round(y / ROW_Y_TOL);
2765
+ for (const r of lText) {
2766
+ const k = rowKey(r.y);
2767
+ const e = rows.get(k) ?? { lEnd: -Infinity, rStart: Infinity };
2768
+ if (r.x + r.w > e.lEnd) e.lEnd = r.x + r.w;
2769
+ rows.set(k, e);
2770
+ }
2771
+ const lRowCount = rows.size;
2772
+ for (const r of rText) {
2773
+ const k = rowKey(r.y);
2774
+ const e = rows.get(k) ?? { lEnd: -Infinity, rStart: Infinity };
2775
+ if (r.x < e.rStart) e.rStart = r.x;
2776
+ rows.set(k, e);
2777
+ }
2778
+ let rRowCount = 0;
2779
+ let bothRows = 0;
2780
+ let tinyGapRows = 0;
2781
+ for (const e of rows.values()) {
2782
+ if (e.rStart < Infinity) rRowCount++;
2783
+ if (e.lEnd > -Infinity && e.rStart < Infinity) {
2784
+ bothRows++;
2785
+ if (e.rStart - e.lEnd < CROSS_ROW_TINY_GAP) tinyGapRows++;
2786
+ }
2787
+ }
2788
+ if (lRowCount < SIDE_MIN_TEXT_ROWS || rRowCount < SIDE_MIN_TEXT_ROWS) return null;
2789
+ if (tinyGapRows > MAX_TINY_GAP_ROWS) return null;
2790
+ if (bothRows / Math.min(lRowCount, rRowCount) > MAX_BOTH_SIDE_ROW_RATIO) return null;
2791
+ }
2792
+ return gutterX;
2793
+ }
2794
+ function orderByGutter(units, rectOf, gutterX) {
2795
+ const left = [];
2796
+ const right = [];
2797
+ const cross = [];
2798
+ for (const u of units) {
2799
+ const r = rectOf(u);
2800
+ const top = r.y + r.h;
2801
+ if (r.x < gutterX && r.x + r.w > gutterX) cross.push({ u, top });
2802
+ else if (r.x + r.w <= gutterX) left.push({ u, top });
2803
+ else right.push({ u, top });
2804
+ }
2805
+ const byTopDesc = (a, b) => b.top - a.top;
2806
+ cross.sort(byTopDesc);
2807
+ left.sort(byTopDesc);
2808
+ right.sort(byTopDesc);
2809
+ const bandOf = (top) => {
2810
+ let k = 0;
2811
+ while (k < cross.length && cross[k].top > top) k++;
2812
+ return k;
2813
+ };
2814
+ const ordered = [];
2815
+ for (let k = 0; k <= cross.length; k++) {
2816
+ for (const t of left) {
2817
+ if (bandOf(t.top) === k) ordered.push(t.u);
2818
+ }
2819
+ for (const t of right) {
2820
+ if (bandOf(t.top) === k) ordered.push(t.u);
2821
+ }
2822
+ if (k < cross.length) ordered.push(cross[k].u);
2823
+ }
2824
+ return ordered;
2825
+ }
2826
+
2684
2827
  // src/pdf/columns.ts
2685
2828
  function isProseSpread(items) {
2686
2829
  if (items.length < 4) return false;
@@ -2897,6 +3040,123 @@ function buildGridTable(lines, columns) {
2897
3040
  return md.join("\n");
2898
3041
  }
2899
3042
 
3043
+ // src/pdf/underline.ts
3044
+ var UNDER_MAX_THICKNESS = 2;
3045
+ var UNDER_BELOW_EM = 0.72;
3046
+ var UNDER_BELOW_MIN_PT = 3;
3047
+ var UNDER_ABOVE_PT = 1;
3048
+ var UNDER_MIN_OVERLAP_RATIO = 0.6;
3049
+ var UNDER_OWNER_PAD_EM = 0.75;
3050
+ var UNDER_OWNER_PAD_MIN_PT = 4;
3051
+ var UNDER_MIN_COVERAGE = 0.6;
3052
+ var UNDER_GRID_EPS = 2;
3053
+ var UNDER_COLUMN_GAP_EM = 2;
3054
+ var UNDER_REPEATED_SPAN_LEVELS = 3;
3055
+ var UNDER_SPAN_OVERLAP_RATIO = 0.8;
3056
+ var UNDER_BOX_PAIR_MIN_EM = 0.5;
3057
+ var UNDER_BOX_PAIR_MAX_EM = 2.2;
3058
+ var UNDER_BOX_PAIR_OVERLAP = 0.8;
3059
+ function markUnderlineItems(items, horizontals, verticals) {
3060
+ if (items.length === 0 || horizontals.length === 0) return;
3061
+ for (const line of horizontals) {
3062
+ if (line.lineWidth > UNDER_MAX_THICKNESS) continue;
3063
+ if (touchesVertical(line, verticals)) continue;
3064
+ if (isRepeatedSpanRule(line, horizontals)) continue;
3065
+ const matches = [];
3066
+ for (const item of items) {
3067
+ const h = item.h > 0 ? item.h : item.fontSize;
3068
+ if (h <= 0 || item.w <= 0 || !item.text.trim()) continue;
3069
+ const below = Math.max(h * UNDER_BELOW_EM, UNDER_BELOW_MIN_PT);
3070
+ if (line.y1 < item.y - below || line.y1 > item.y + UNDER_ABOVE_PT) continue;
3071
+ const overlap = Math.min(line.x2, item.x + item.w) - Math.max(line.x1, item.x);
3072
+ if (overlap / item.w < UNDER_MIN_OVERLAP_RATIO) continue;
3073
+ matches.push(item);
3074
+ }
3075
+ if (matches.length === 0) continue;
3076
+ let x1 = Infinity, x2 = -Infinity, maxH = 0, covered = 0;
3077
+ for (const m of matches) {
3078
+ x1 = Math.min(x1, m.x);
3079
+ x2 = Math.max(x2, m.x + m.w);
3080
+ maxH = Math.max(maxH, m.h > 0 ? m.h : m.fontSize);
3081
+ covered += m.w;
3082
+ }
3083
+ const pad = Math.max(maxH * UNDER_OWNER_PAD_EM, UNDER_OWNER_PAD_MIN_PT);
3084
+ if (line.x1 < x1 - pad || line.x2 > x2 + pad) continue;
3085
+ if (covered < (line.x2 - line.x1) * UNDER_MIN_COVERAGE) continue;
3086
+ if (hasBoxTopPair(line, maxH, horizontals)) continue;
3087
+ matches.sort((a, b) => a.x - b.x);
3088
+ let hole = false;
3089
+ for (let i = 1; i < matches.length; i++) {
3090
+ if (matches[i].x - (matches[i - 1].x + matches[i - 1].w) > maxH * UNDER_COLUMN_GAP_EM) {
3091
+ hole = true;
3092
+ break;
3093
+ }
3094
+ }
3095
+ if (hole) continue;
3096
+ for (const m of matches) m.underline = true;
3097
+ }
3098
+ }
3099
+ function touchesVertical(line, verticals) {
3100
+ for (const v of verticals) {
3101
+ if (v.x1 < line.x1 - UNDER_GRID_EPS || v.x1 > line.x2 + UNDER_GRID_EPS) continue;
3102
+ const lo = Math.min(v.y1, v.y2), hi = Math.max(v.y1, v.y2);
3103
+ if (lo <= line.y1 + UNDER_GRID_EPS && hi >= line.y1 - UNDER_GRID_EPS) return true;
3104
+ }
3105
+ return false;
3106
+ }
3107
+ function hasBoxTopPair(line, maxH, horizontals) {
3108
+ const w = line.x2 - line.x1;
3109
+ if (w <= 0) return false;
3110
+ for (const o of horizontals) {
3111
+ if (o === line) continue;
3112
+ const dy = o.y1 - line.y1;
3113
+ if (dy < maxH * UNDER_BOX_PAIR_MIN_EM || dy > maxH * UNDER_BOX_PAIR_MAX_EM) continue;
3114
+ const ow = o.x2 - o.x1;
3115
+ if (ow <= 0) continue;
3116
+ const overlap = Math.min(line.x2, o.x2) - Math.max(line.x1, o.x1);
3117
+ if (overlap / Math.max(w, ow) >= UNDER_BOX_PAIR_OVERLAP) return true;
3118
+ }
3119
+ return false;
3120
+ }
3121
+ function isRepeatedSpanRule(line, horizontals) {
3122
+ const w = line.x2 - line.x1;
3123
+ if (w <= 0) return false;
3124
+ const ys = [];
3125
+ for (const o of horizontals) {
3126
+ const ow = o.x2 - o.x1;
3127
+ if (ow <= 0) continue;
3128
+ const overlap = Math.min(line.x2, o.x2) - Math.max(line.x1, o.x1);
3129
+ if (overlap / Math.max(w, ow) < UNDER_SPAN_OVERLAP_RATIO) continue;
3130
+ if (!ys.some((y) => Math.abs(y - o.y1) < 2)) ys.push(o.y1);
3131
+ }
3132
+ return ys.length >= UNDER_REPEATED_SPAN_LEVELS;
3133
+ }
3134
+ function wrapUnderlineRuns(items) {
3135
+ const marked = items.filter((i) => i.underline);
3136
+ if (marked.length === 0) return;
3137
+ const lines = /* @__PURE__ */ new Map();
3138
+ for (const item of marked) {
3139
+ const key = Math.round(item.y / 3);
3140
+ const arr = lines.get(key) || [];
3141
+ arr.push(item);
3142
+ lines.set(key, arr);
3143
+ }
3144
+ for (const arr of lines.values()) {
3145
+ arr.sort((a, b) => a.x - b.x);
3146
+ let runStart = 0;
3147
+ for (let i = 1; i <= arr.length; i++) {
3148
+ const prev = arr[i - 1];
3149
+ const em = Math.max(prev.h > 0 ? prev.h : prev.fontSize, 1);
3150
+ const gap = i < arr.length ? arr[i].x - (prev.x + prev.w) : Infinity;
3151
+ if (gap > em) {
3152
+ arr[runStart].text = "<u>" + arr[runStart].text;
3153
+ prev.text = prev.text + "</u>";
3154
+ runStart = i;
3155
+ }
3156
+ }
3157
+ }
3158
+ }
3159
+
2900
3160
  // src/pdf/page-blocks.ts
2901
3161
  function extractPageBlocksWithLines(items, pageNum, opList, pageWidth, pageHeight, extraLines, detectTables = true) {
2902
3162
  if (items.length === 0) return [];
@@ -2912,6 +3172,8 @@ function extractPageBlocksWithLines(items, pageNum, opList, pageWidth, pageHeigh
2912
3172
  verticals = bridgeSplitColumnVerticals(horizontals, verticals);
2913
3173
  markStrikethroughItems(items, horizontals);
2914
3174
  wrapStrikethroughRuns(items);
3175
+ markUnderlineItems(items, horizontals, verticals);
3176
+ wrapUnderlineRuns(items);
2915
3177
  const grids = detectTables ? buildTableGrids(horizontals, verticals) : [];
2916
3178
  if (grids.length > 0) {
2917
3179
  return extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals);
@@ -3114,6 +3376,9 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
3114
3376
  blocks.push({ type: "table", table: irTable, pageNumber: pageNum, bbox: tableBbox });
3115
3377
  }
3116
3378
  let remaining = items.filter((i) => !usedItems.has(i));
3379
+ const groupSizes = [];
3380
+ let finalTextBlocks = [];
3381
+ let gutterX = null;
3117
3382
  if (remaining.length > 0) {
3118
3383
  remaining.sort((a, b) => b.y - a.y || a.x - b.x);
3119
3384
  const clusterItems = remaining.map((i) => ({
@@ -3140,9 +3405,37 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
3140
3405
  }
3141
3406
  remaining = remaining.filter((_, idx) => !usedClusterIndices.has(idx));
3142
3407
  }
3143
- const groupSizes = [];
3144
- let finalTextBlocks = [];
3145
- if (remaining.length > 0) {
3408
+ }
3409
+ {
3410
+ const rects = remaining.map((i) => ({ x: i.x, y: i.y, w: i.w, h: i.h > 0 ? i.h : i.fontSize }));
3411
+ for (const b of blocks) {
3412
+ if (b.bbox) rects.push({ x: b.bbox.x, y: b.bbox.y, w: b.bbox.width, h: b.bbox.height });
3413
+ }
3414
+ gutterX = detectColumnGutter(rects);
3415
+ }
3416
+ if (remaining.length > 0) {
3417
+ if (gutterX !== null) {
3418
+ const gx = gutterX;
3419
+ const allY = remaining.map((i) => i.y);
3420
+ const pageH = safeMax(allY) - safeMin(allY);
3421
+ const gapThreshold = Math.max(15, pageH * 0.03);
3422
+ const sides = [
3423
+ remaining.filter((i) => i.x + i.w <= gx),
3424
+ remaining.filter((i) => i.x < gx && i.x + i.w > gx),
3425
+ remaining.filter((i) => i.x >= gx)
3426
+ ];
3427
+ const textBlocks = [];
3428
+ for (const side of sides) {
3429
+ if (side.length === 0) continue;
3430
+ for (const group of xyCutOrder(side, gapThreshold)) {
3431
+ if (group.length === 0) continue;
3432
+ const groupBlocks = extractPageBlocksFallback(group, pageNum);
3433
+ for (const b of groupBlocks) textBlocks.push(b);
3434
+ groupSizes.push(groupBlocks.length);
3435
+ }
3436
+ }
3437
+ finalTextBlocks = detectListBlocks(textBlocks);
3438
+ } else {
3146
3439
  const allY = remaining.map((i) => i.y);
3147
3440
  const pageH = safeMax(allY) - safeMin(allY);
3148
3441
  const groups = xyCutOrder(remaining, Math.max(15, pageH * 0.03));
@@ -3155,26 +3448,43 @@ function extractBlocksWithGrids(items, pageNum, grids, horizontals, verticals) {
3155
3448
  }
3156
3449
  finalTextBlocks = detectListBlocks(textBlocks);
3157
3450
  }
3158
- const units = blocks.map((b) => [b]);
3159
- let off = 0;
3160
- for (const size of groupSizes) {
3161
- const unit = finalTextBlocks.slice(off, off + size);
3162
- off += size;
3163
- if (unit.length > 0) units.push(unit);
3451
+ }
3452
+ const units = blocks.map((b) => [b]);
3453
+ let off = 0;
3454
+ for (const size of groupSizes) {
3455
+ const unit = finalTextBlocks.slice(off, off + size);
3456
+ off += size;
3457
+ if (unit.length > 0) units.push(unit);
3458
+ }
3459
+ const unitTopY = (u) => {
3460
+ let top = 0;
3461
+ for (const b of u) {
3462
+ if (b.bbox && b.bbox.y + b.bbox.height > top) top = b.bbox.y + b.bbox.height;
3164
3463
  }
3165
- const unitTopY = (u) => {
3166
- let top = 0;
3464
+ return top;
3465
+ };
3466
+ if (gutterX !== null && units.length > 1) {
3467
+ const gx = gutterX;
3468
+ const unitRect = (u) => {
3469
+ let minX = Infinity, minY = Infinity, maxR = -Infinity, maxT = -Infinity;
3167
3470
  for (const b of u) {
3168
- if (b.bbox && b.bbox.y + b.bbox.height > top) top = b.bbox.y + b.bbox.height;
3169
- }
3170
- return top;
3471
+ if (!b.bbox) continue;
3472
+ if (b.bbox.x < minX) minX = b.bbox.x;
3473
+ if (b.bbox.y < minY) minY = b.bbox.y;
3474
+ if (b.bbox.x + b.bbox.width > maxR) maxR = b.bbox.x + b.bbox.width;
3475
+ if (b.bbox.y + b.bbox.height > maxT) maxT = b.bbox.y + b.bbox.height;
3476
+ }
3477
+ if (!Number.isFinite(minX)) return { x: gx - 1, y: 0, w: 2, h: 0 };
3478
+ return { x: minX, y: minY, w: maxR - minX, h: maxT - minY };
3171
3479
  };
3172
- units.sort((a, b) => unitTopY(b) - unitTopY(a));
3173
- const ordered = [];
3174
- for (const u of units) for (const b of u) ordered.push(b);
3175
- return mergeAdjacentTableBlocks(ordered);
3480
+ const ordered2 = [];
3481
+ for (const u of orderByGutter(units, unitRect, gx)) for (const b of u) ordered2.push(b);
3482
+ return mergeAdjacentTableBlocks(ordered2);
3176
3483
  }
3177
- return mergeAdjacentTableBlocks(blocks);
3484
+ units.sort((a, b) => unitTopY(b) - unitTopY(a));
3485
+ const ordered = [];
3486
+ for (const u of units) for (const b of u) ordered.push(b);
3487
+ return mergeAdjacentTableBlocks(ordered);
3178
3488
  }
3179
3489
  var NEIGHBOR_TABLE_EPSILON = 0.2;
3180
3490
  function mergeCrossPageTables(blocks) {
@@ -3323,7 +3633,10 @@ function extractPageBlocksFallback(items, pageNum, fullPage = false, detectTable
3323
3633
  return by - ay;
3324
3634
  });
3325
3635
  } else {
3326
- const proseCutX = fullPage ? findTwoColumnProseCutX(items) : null;
3636
+ let proseCutX = fullPage ? findTwoColumnProseCutX(items) : null;
3637
+ if (proseCutX === null && fullPage) {
3638
+ proseCutX = detectColumnGutter(items.map((i) => ({ x: i.x, y: i.y, w: i.w, h: i.h > 0 ? i.h : i.fontSize })));
3639
+ }
3327
3640
  const allYLines = mergeSuperscriptLines(groupByY(items));
3328
3641
  const columns = proseCutX !== null || !detectTables ? null : detectColumns(allYLines);
3329
3642
  if (columns && columns.length >= 3) {
@@ -3371,4 +3684,4 @@ export {
3371
3684
  extractPageBlocksWithLines,
3372
3685
  mergeCrossPageTables
3373
3686
  };
3374
- //# sourceMappingURL=chunk-2CENWSAW.js.map
3687
+ //# sourceMappingURL=chunk-GIWKBPII.js.map