{"version":3,"sources":["/Users/mong-e/workspace/kordoc/dist/chunk-6GM2QLM3.cjs","../src/shared/pua.ts","../src/table/builder.ts","../src/image/transcode.ts"],"names":[],"mappings":"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","file":"/Users/mong-e/workspace/kordoc/dist/chunk-6GM2QLM3.cjs","sourcesContent":[null,"/**\n * 한컴 PUA(Private Use Area) 문자 → 유니코드 표준 문자 매핑.\n *\n * 한글(HWP)은 글머리표·문자표 기호를 Symbol/Wingdings 계열 PUA 코드포인트\n * (U+F020~U+F0FF)와 한컴 자체 Supplementary PUA-A 영역(U+F0000대)에 저장한다.\n * 매핑 없이 마크다운에 내보내면 빈 네모로 깨진다.\n *\n * 매핑 출처: rhwp (MIT, https://github.com/edwardkim/rhwp)\n * paragraph_layout.rs map_pua_bullet_char — 한컴 PDF 정답지 시각 검증 테이블.\n */\n\n/** BMP Symbol 영역 (U+F020~U+F0FF) 매핑 — 키는 (code - 0xF000) */\nconst BMP_SYMBOL_MAP: Record<number, string> = {\n  // 도형/기호\n  0x6c: \"●\", // ●\n  0x6d: \"●\", // ● (그림자 원 근사)\n  0x6e: \"■\", // ■\n  0x6f: \"□\", // □\n  0x70: \"□\", // □ (굵은 흰 사각 근사)\n  0x71: \"□\", // □ (그림자 근사)\n  0x72: \"□\", // □ (그림자 근사)\n  0x73: \"⬧\", // ⬧\n  0x74: \"⧫\", // ⧫\n  0x75: \"◆\", // ◆\n  0x76: \"❖\", // ❖\n  0x77: \"⬥\", // ⬥\n  // 체크/별/점\n  0x9e: \"·\", // ·\n  0x9f: \"•\", // •\n  0xa0: \"·\", // · (한컴 PDF 정답지 정합 — ▪ 아님)\n  0xa1: \"⚪\", // ⚪\n  0xa2: \"○\", // ○\n  0xa3: \"○\", // ○\n  0xa4: \"◉\", // ◉\n  0xa5: \"◎\", // ◎\n  0xa7: \"▪\", // ▪\n  0xa8: \"◻\", // ◻\n  0xaa: \"✦\", // ✦\n  0xab: \"★\", // ★\n  0xac: \"✶\", // ✶\n  0xad: \"✴\", // ✴\n  0xae: \"✹\", // ✹\n  // 손 모양\n  0x45: \"☜\", // ☜\n  0x46: \"☞\", // ☞\n  0x47: \"☝\", // ☝\n  0x48: \"☟\", // ☟\n  // 체크마크\n  0xfb: \"✗\", // ✗\n  0xfc: \"✔\", // ✔\n  0xfd: \"☒\", // ☒\n  0xfe: \"☑\", // ☑\n  // 화살표\n  0xe8: \"➔\", // ➔ (heavy wide-headed — 한컴 PDF 정답지 정합)\n  0xef: \"⇦\", // ⇦\n  0xf0: \"⇨\", // ⇨\n  0xf1: \"⇧\", // ⇧\n  0xf2: \"⇩\", // ⇩\n  // 기타\n  0x22: \"✂\", // ✂\n  0x36: \"⌛\", // ⌛\n  0x4a: \"☺\", // ☺\n  0x4e: \"☠\", // ☠\n  0x52: \"☼\", // ☼\n  0x54: \"❄\", // ❄\n  0x58: \"✠\", // ✠\n  0x59: \"✡\", // ✡\n}\n\n/** Supplementary PUA-A (U+F0000대) — 한컴 자체 영역 매핑 */\nconst SUPPLEMENTARY_MAP: Record<number, string> = {\n  0xf003b: \"↓\", // ↓\n  0xf02ef: \"·\", // ·\n  0xf0854: \"《\", // 《\n  0xf0855: \"》\", // 》\n  0xf00da: \"▸\", // ▸\n  0xf080f: \"━\", // ━\n  0xf0827: \"■\", // ■\n  0xf03c5: \"□\", // □ 글머리 — HWP3→HWP5 한컴 변환본 보존 코드, 한컴오피스 표시값 (rhwp #1105)\n}\n\n/** 단일 코드포인트 매핑 — 매핑 없으면 원본 유지 */\nexport function mapPuaChar(code: number): string | undefined {\n  if (code >= 0xf020 && code <= 0xf0ff) {\n    return BMP_SYMBOL_MAP[code - 0xf000]\n  }\n  if (code >= 0xf0000 && code <= 0xf09ff) {\n    return SUPPLEMENTARY_MAP[code]\n  }\n  return undefined\n}\n\n/** BMP PUA 영역 여부 (U+E000~U+F8FF) */\nexport function isBmpPua(code: number): boolean {\n  return code >= 0xe000 && code <= 0xf8ff\n}\n\n/**\n * 문자열 내 한컴 PUA 문자를 표준 유니코드로 치환.\n * 매핑 없는 BMP PUA는 원본 유지(옛한글 한양PUA 가능성 — 제거하면 안 됨),\n * 매핑 없는 Supplementary PUA-A는 호출부의 기존 제거 로직에 위임한다.\n */\nexport function mapPuaText(text: string): string {\n  let out = \"\"\n  for (const ch of text) {\n    const code = ch.codePointAt(0)!\n    out += mapPuaChar(code) ?? ch\n  }\n  return out\n}\n","/** 2-pass colSpan/rowSpan 테이블 빌더 및 Markdown 변환 */\n\nimport type { CellContext, IRBlock, IRCell, IRSpan, IRTable } from \"../types.js\"\nimport { sanitizeHref } from \"../utils.js\"\nimport { mapPuaText } from \"../shared/pua.js\"\n\n/** 테이블 열 수 상한 — 한국 공공문서 기준 충분한 값 */\nexport const MAX_COLS = 200\n/** 테이블 행 수 상한 — 메모리 폭주 방지 */\nexport const MAX_ROWS = 10000\n\nexport interface BuildTableOptions {\n  /** 실제 셀 앵커가 있는 빈 후행 열(서식 문서의 입력란)을 보존한다 (#47).\n   *  앵커 없는 유령 열(span 인플레이션)은 이 옵션과 무관하게 트림.\n   *  기본 false: 종전대로 텍스트 기준 전부 트림 (마크다운 가독성·벤치 계약). */\n  keepAnchoredEmptyCols?: boolean\n}\n\nexport function buildTable(rows: CellContext[][], options?: BuildTableOptions): IRTable {\n  if (rows.length > MAX_ROWS) rows = rows.slice(0, MAX_ROWS)\n  const numRows = rows.length\n\n  // colAddr/rowAddr가 있으면 직접 배치 (HWPX cellAddr, HWP5 colAddr/rowAddr)\n  const hasAddr = rows.some(row => row.some(c => c.colAddr !== undefined && c.rowAddr !== undefined))\n  if (hasAddr) return buildTableDirect(rows, numRows, options)\n\n  // Pass 1: maxCols 계산 — 2D 배열 사용 (동적 확장)\n  let maxCols = 0\n  const tempOccupied: boolean[][] = Array.from({ length: numRows }, () => [])\n\n  for (let rowIdx = 0; rowIdx < numRows; rowIdx++) {\n    let colIdx = 0\n    for (const cell of rows[rowIdx]) {\n      while (colIdx < MAX_COLS && tempOccupied[rowIdx][colIdx]) colIdx++\n      if (colIdx >= MAX_COLS) break\n\n      for (let r = rowIdx; r < Math.min(rowIdx + cell.rowSpan, numRows); r++) {\n        for (let c = colIdx; c < Math.min(colIdx + cell.colSpan, MAX_COLS); c++) {\n          tempOccupied[r][c] = true\n        }\n      }\n      colIdx += cell.colSpan\n      if (colIdx > maxCols) maxCols = colIdx\n    }\n  }\n\n  if (maxCols === 0) return { rows: 0, cols: 0, cells: [], hasHeader: false }\n\n  // Pass 2: 실제 배치\n  const grid: IRCell[][] = Array.from({ length: numRows }, () =>\n    Array.from({ length: maxCols }, () => ({ text: \"\", colSpan: 1, rowSpan: 1 }))\n  )\n  const occupied: boolean[][] = Array.from({ length: numRows }, () => Array(maxCols).fill(false))\n  const anchorCols = new Set<number>()\n\n  for (let rowIdx = 0; rowIdx < numRows; rowIdx++) {\n    let colIdx = 0\n    let cellIdx = 0\n\n    while (colIdx < maxCols && cellIdx < rows[rowIdx].length) {\n      while (colIdx < maxCols && occupied[rowIdx][colIdx]) colIdx++\n      if (colIdx >= maxCols) break\n\n      const cell = rows[rowIdx][cellIdx]\n      anchorCols.add(colIdx)\n      grid[rowIdx][colIdx] = {\n        text: cell.text.trim(),\n        colSpan: cell.colSpan,\n        rowSpan: cell.rowSpan,\n      }\n\n      for (let r = rowIdx; r < Math.min(rowIdx + cell.rowSpan, numRows); r++) {\n        for (let c = colIdx; c < Math.min(colIdx + cell.colSpan, maxCols); c++) {\n          occupied[r][c] = true\n        }\n      }\n\n      colIdx += cell.colSpan\n      cellIdx++\n    }\n  }\n\n  return trimAndReturn(grid, numRows, maxCols, anchorCols, options)\n}\n\n/** colAddr/rowAddr 절대 좌표 기반 직접 배치 */\nfunction buildTableDirect(rows: CellContext[][], numRows: number, options?: BuildTableOptions): IRTable {\n  // 전체 셀에서 maxCols 계산 (MAX_COLS 상한 적용)\n  let maxCols = 0\n  for (const row of rows) {\n    for (const cell of row) {\n      const end = (cell.colAddr ?? 0) + cell.colSpan\n      if (end > maxCols) maxCols = end\n    }\n  }\n  if (maxCols > MAX_COLS) maxCols = MAX_COLS\n  if (maxCols === 0) return { rows: 0, cols: 0, cells: [], hasHeader: false }\n\n  const grid: IRCell[][] = Array.from({ length: numRows }, () =>\n    Array.from({ length: maxCols }, () => ({ text: \"\", colSpan: 1, rowSpan: 1 }))\n  )\n\n  const anchorCols = new Set<number>()\n  for (const row of rows) {\n    for (const cell of row) {\n      const r = cell.rowAddr ?? 0\n      const c = cell.colAddr ?? 0\n      if (r >= numRows || c >= maxCols || r < 0 || c < 0) continue\n\n      anchorCols.add(c)\n      grid[r][c] = { text: cell.text.trim(), colSpan: cell.colSpan, rowSpan: cell.rowSpan }\n\n      // 병합 영역 마킹\n      for (let dr = 0; dr < cell.rowSpan; dr++) {\n        for (let dc = 0; dc < cell.colSpan; dc++) {\n          if (dr === 0 && dc === 0) continue\n          if (r + dr < numRows && c + dc < maxCols) {\n            grid[r + dr][c + dc] = { text: \"\", colSpan: 1, rowSpan: 1 }\n          }\n        }\n      }\n    }\n  }\n\n  return trimAndReturn(grid, numRows, maxCols, anchorCols, options)\n}\n\n/** 빈 후행 열 제거 후 IRTable 반환 — 기본 텍스트 기준 전부, keepAnchoredEmptyCols면 앵커 없는 유령 열만 (#47) */\nfunction trimAndReturn(grid: IRCell[][], numRows: number, maxCols: number, anchorCols: Set<number>, options?: BuildTableOptions): IRTable {\n  let effectiveCols = maxCols\n  while (effectiveCols > 0) {\n    const colEmpty = grid.every(row => !row[effectiveCols - 1]?.text?.trim())\n    if (!colEmpty) break\n    // 실제 셀 앵커가 있는 빈 열은 서식 문서의 입력란 — 옵션 시 트림하지 않는다 (#47)\n    if (options?.keepAnchoredEmptyCols && anchorCols.has(effectiveCols - 1)) break\n    effectiveCols--\n  }\n  if (effectiveCols < maxCols && effectiveCols > 0) {\n    const trimmed = grid.map(row => row.slice(0, effectiveCols))\n    return { rows: numRows, cols: effectiveCols, cells: trimmed, hasHeader: numRows > 1 }\n  }\n  return { rows: numRows, cols: maxCols, cells: grid, hasHeader: numRows > 1 }\n}\n\nexport function convertTableToText(rows: CellContext[][]): string {\n  return rows\n    .map(row =>\n      row\n        .map(c => c.text.trim().replace(/\\n/g, \" \").replace(/\\|/g, \"\\\\|\"))\n        .filter(Boolean)\n        .join(\" / \")\n    )\n    .filter(Boolean)\n    .join(\"\\n\")\n}\n\n/** 마크다운 GFM 특수문자 이스케이프 — remark-gfm 오해석 방지 */\nfunction escapeGfm(text: string): string {\n  // ~ → \\~ (GFM strikethrough 방지), * → \\* (emphasis/HR·마스킹 별표 \"******\" 방지),\n  // _ → \\_ (emphasis 방지), ` → \\` (inline code 방지).\n  // 단 $...$ / $$...$$ 수식 스팬은 KaTeX 문법이라 이스케이프하면 파스 에러가 나므로 보호한다\n  // (스팬을 임시 필러로 가린 뒤 escape → 복원). NUL 필러는 마크다운 본문에 등장하지 않는다.\n  // ![image](image_001.png) 이미지 참조 스팬과 링크 URL부 `](스킴...)`(sanitizeHref 허용\n  // 스킴 한정 — 우연한 \"[라벨](식별자)\" 평문은 제외)도 동일 보호 — _ 이스케이프 시 문법 파괴.\n  const NUL = String.fromCharCode(0) // 마크다운 본문에 없는 안전한 필러 (소스에 raw NUL 미기입)\n  const spans: string[] = []\n  const masked = text.replace(/!\\[[^\\]]*\\]\\([^)\\n]*\\)|\\]\\((?:https?:|mailto:|tel:|#)[^)\\n]*\\)|\\$\\$[^$]*\\$\\$|\\$[^$\\n]*\\$/gi, (m) => {\n    spans.push(m)\n    return NUL + (spans.length - 1) + NUL\n  })\n  const escaped = masked.replace(/([~*_`])/g, \"\\\\$1\")\n  return escaped.replace(new RegExp(NUL + \"(\\\\d+)\" + NUL, \"g\"), (_, n) => spans[Number(n)])\n}\n\n/** HWP 자동생성 도형/개체 대체텍스트 정규식 — 한컴오피스가 삽입하는 모든 알려진 패턴.\n *  행 전체 일치(^…$m)로 한정 — 무앵커면 \"붙임 문서는 표 입니다.\" 같은 본문 중간을 오삭제한다 */\nconst HWP_SHAPE_ALT_TEXT_RE = /^(?:모서리가 둥근 |둥근 )?(?:사각형|직사각형|정사각형|원|타원|삼각형|이등변 삼각형|직각 삼각형|선|직선|곡선|화살표|굵은 화살표|이중 화살표|오각형|육각형|팔각형|별|[4-8]점별|십자|십자형|구름|구름형|마름모|도넛|평행사변형|사다리꼴|부채꼴|호|반원|물결|번개|하트|빗금|블록 화살표|수식|표|그림|개체|그리기\\s?개체|묶음\\s?개체|글상자|수식\\s?개체|OLE\\s?개체)\\s?입니다\\.?$/gm\n\n/** HWP PUA 특수문자 및 도형 대체텍스트 제거 — 모든 포맷 공통 */\nfunction sanitizeText(text: string): string {\n  // 한컴 PUA → 표준 유니코드 매핑 (rhwp 검증 테이블) — 제거 regex보다 먼저 적용\n  let result = mapPuaText(text)\n    // Supplementary Private Use Area (U+F0000-U+FFFFD) — HWP 전용 기호 (매핑 안 된 잔여분)\n    .replace(/[\\u{F0000}-\\u{FFFFD}]/gu, \"\")\n    // HWP 도형/개체 자동생성 대체텍스트 제거\n    .replace(HWP_SHAPE_ALT_TEXT_RE, \"\")\n    .replace(/  +/g, \" \")\n    .trim()\n  // 균등배분 스페이스 정리 (\"현 장 대 응 단 장\" → \"현장대응단장\")\n  // 짧은 텍스트(30자 이하)에서 70%+ 토큰이 한글 1글자면 균등배분으로 판단\n  if (result.length <= 30 && result.includes(\" \")) {\n    const tokens = result.split(\" \")\n    // 한글 1글자 토큰만 카운트 — ASCII 특수문자(< > & 등)는 균등배분이 아님\n    const koreanSingleCharCount = tokens.filter(t => t.length === 1 && /[\\uAC00-\\uD7AF\\u3131-\\u318E]/.test(t)).length\n    if (tokens.length >= 3 && koreanSingleCharCount / tokens.length >= 0.7) {\n      result = tokens.join(\"\")\n    }\n  }\n  return result\n}\n\n/**\n * 레이아웃 테이블 감지 및 해체 — IRBlock 레벨에서 수행\n * 적은 행(≤3) + 셀 내 줄바꿈 다량 → table 블록을 paragraph 블록들로 분해\n * heading 감지 전에 호출해야 해체된 텍스트에 heading 감지 적용 가능\n *\n * 호출 정책(의도): HWP5 파서만 호출한다. 구형 HWP5 문서는 제목/본문을 표로\n * 감싼 레이아웃 표가 흔하지만, HWPX는 그 관행이 드물고 무엇보다 patchHwpx/\n * fillHwpx 무손실 라운드트립이 \"파서 렌더 = 소스맵 표 서수\" 대응에 의존하므로\n * HWPX에서 표를 문단으로 해체하면 표 매핑이 깨진다. HWPX 적용은 코퍼스\n * 전/후 정량 비교 + 라운드트립 e2e 검증이 선행되어야 한다.\n */\nexport function flattenLayoutTables(blocks: IRBlock[]): IRBlock[] {\n  const result: IRBlock[] = []\n\n  for (const block of blocks) {\n    if (block.type !== \"table\" || !block.table) {\n      result.push(block)\n      continue\n    }\n\n    const { rows: numRows, cols: numCols, cells } = block.table\n\n    // 1x1 테이블은 기존 로직(tableToMarkdown)에서 처리\n    if (numRows === 1 && numCols === 1) {\n      result.push(block)\n      continue\n    }\n\n    // 레이아웃 테이블 휴리스틱\n    if (numRows <= 3) {\n      let totalNewlines = 0\n      let totalTextLen = 0\n      for (let r = 0; r < numRows; r++) {\n        for (let c = 0; c < numCols; c++) {\n          const t = cells[r]?.[c]?.text || \"\"\n          totalNewlines += (t.match(/\\n/g) || []).length\n          totalTextLen += t.length\n        }\n      }\n\n      // 레이아웃 테이블 판정: 많은 줄바꿈(>5), 또는 적은 행에 비해 총 텍스트 과다(>300)\n      // 단, 열이 4개 이상이면 헤더-값 구조의 데이터 표일 가능성이 높아 해체하지 않는다\n      // (실증: 2×10 모집프로그램 표가 문단으로 해체되어 헤더↔값 연결 파괴)\n      if (numCols < 4 && (totalNewlines > 5 || (numRows <= 2 && totalTextLen > 300))) {\n        // 레이아웃 테이블 → 각 셀을 paragraph 블록으로 분해\n        for (let r = 0; r < numRows; r++) {\n          for (let c = 0; c < numCols; c++) {\n            const cell = cells[r]?.[c]\n            if (!cell) continue\n            // 셀에 구조화 블록(중첩표·이미지·다중문단)이 있으면 재귀 해체로 구조 보존.\n            // 중첩 spec 표(numRows>3)는 해체되지 않고 실제 table 블록으로 살아남는다\n            // (text의 \" / \" 평탄화 폴백으로만 남아 유실되던 버그 수정). 셀 blocks는\n            // 이미 같은 pageNumber로 생성되므로 그대로 보존된다.\n            if (cell.blocks?.length) {\n              result.push(...flattenLayoutTables(cell.blocks))\n              continue\n            }\n            const cellText = cell.text?.trim()\n            if (!cellText) continue\n            // 셀 내 줄바꿈을 별도 paragraph로 분리\n            for (const line of cellText.split(\"\\n\")) {\n              const trimmed = line.trim()\n              if (!trimmed) continue\n              result.push({ type: \"paragraph\", text: trimmed, pageNumber: block.pageNumber })\n            }\n          }\n        }\n        continue\n      }\n    }\n\n    result.push(block)\n  }\n\n  return result\n}\n\n/** 러닝 헤더 후보 판정 최대 길이 — 긴 본문 문단을 헤더로 오인하지 않도록 */\nconst RUNNING_HEADER_MAX_LEN = 40\n/** 러닝 헤더로 판정할 최소 반복 횟수 — 페이지마다 재삽입되는 노이즈만 제거 */\nconst RUNNING_HEADER_MIN_FREQ = 3\n\n/** 러닝 헤더 후보 여부 — 짧은 번호매김 섹션 제목(\"2. 과제 구축 내용\")만 */\nfunction isRunningHeaderCandidate(block: IRBlock): boolean {\n  if (block.type !== \"paragraph\" && block.type !== \"heading\") return false\n  const text = block.text?.trim()\n  if (!text || text.length > RUNNING_HEADER_MAX_LEN) return false\n  return /^\\d+\\.\\s/.test(text)\n}\n\n/**\n * 페이지 레이아웃 표에서 유래한 반복 러닝 헤더 문단 중복 제거 — IRBlock 레벨.\n *\n * 배경: 구형 HWP5 문서(군 제안서 등)는 본문 전체를 \"페이지 = N×1 표\" 사슬로\n * 구성하고 각 표의 cell(0,0)에 동일한 섹션 헤더(\"2. 과제 구축 내용\")를 반복\n * 배치한다. flattenLayoutTables가 이 표들을 문단으로 해체하면 같은 헤더가\n * 페이지마다 1회씩 독립 문단으로 남아 본문 사이에 노이즈로 흩어진다. HWP\n * 머리말/꼬리말(CTRL_HEAD/CTRL_FOOT)은 applyHeaderFooterEffect에서 이미\n * dedupe되지만, 본문 레이아웃 표의 러닝 헤더는 그 대상이 아니다.\n *\n * 정책(보수적): \"짧은 번호매김 섹션 제목\"이 정확히 동일 텍스트로 3회 이상\n * 반복될 때만 최초 1회를 남기고 이후 중복을 제거한다. 비후보 블록과 3회 미만\n * 후보는 원형 그대로 통과시킨다(본문 삭제 위험 최소화). 위치 무관 — 평탄화된\n * 블록 스트림 전체를 대상으로 하며, 입력 블록을 변형하지 않고 새 배열을 반환한다.\n */\nexport function dedupeRunningHeaders(blocks: IRBlock[]): IRBlock[] {\n  // Pass 1: 후보 텍스트 빈도 집계\n  const freq = new Map<string, number>()\n  for (const block of blocks) {\n    if (!isRunningHeaderCandidate(block)) continue\n    const text = block.text!.trim()\n    freq.set(text, (freq.get(text) ?? 0) + 1)\n  }\n\n  // Pass 2: 빈도 ≥ 임계값인 후보는 최초 1회만 유지, 이후 중복 제거\n  const seen = new Set<string>()\n  const result: IRBlock[] = []\n  for (const block of blocks) {\n    if (isRunningHeaderCandidate(block)) {\n      const text = block.text!.trim()\n      if ((freq.get(text) ?? 0) >= RUNNING_HEADER_MIN_FREQ) {\n        if (seen.has(text)) continue // 이후 중복 — 제거\n        seen.add(text)\n      }\n    }\n    result.push(block)\n  }\n\n  return result\n}\n\n/**\n * 왕복 채널 run-span → 강조 마커 재방출. 마커는 텍스트에 밀착해야 유효하므로\n * span 가장자리 공백은 마커 밖으로 옮긴다. 코드 span 내부는 이스케이프 없이 원문.\n */\nfunction spansToMarkdown(spans: IRSpan[]): string {\n  let out = \"\"\n  for (const s of spans) {\n    if (!s.text) continue\n    let marker = s.code ? \"`\" : s.bold && s.italic ? \"***\" : s.bold ? \"**\" : s.italic ? \"*\" : \"\"\n    if (s.strike && !s.code) marker = `~~${marker}` // 닫힘은 아래 close 에서 역순 조합 (~~**…**~~)\n    if (!marker) {\n      out += escapeGfm(s.text)\n      continue\n    }\n    const m = /^(\\s*)([\\s\\S]*?)(\\s*)$/.exec(s.text)!\n    const core = m[2]\n    if (!core) {\n      out += s.text\n      continue\n    }\n    const close = [...marker].reverse().join(\"\")\n    out += m[1] + marker + (s.code ? core : escapeGfm(core)) + close + m[3]\n  }\n  return out\n}\n\nexport function blocksToMarkdown(blocks: IRBlock[]): string {\n  const lines: string[] = []\n\n  for (let i = 0; i < blocks.length; i++) {\n    const block = blocks[i]\n\n    // 헤딩 블록 — escapeGfm 필수: 마스킹 별표(\"홍**\")가 볼드로 소비·삭제되는 것 방지\n    if (block.type === \"heading\" && block.text) {\n      const prefix = \"#\".repeat(Math.min(block.level || 2, 6))\n      const headingText = sanitizeText(block.text)\n      if (headingText) lines.push(\"\", `${prefix} ${escapeGfm(headingText)}`, \"\")\n      continue\n    }\n\n    // 이미지 블록 — ![alt](filename) 참조\n    if (block.type === \"image\" && block.text) {\n      lines.push(\"\", `![image](${block.text})`, \"\")\n      continue\n    }\n\n    // 구분선 블록\n    if (block.type === \"separator\") {\n      lines.push(\"\", \"---\", \"\")\n      continue\n    }\n\n    // 리스트 블록\n    if (block.type === \"list\" && block.text) {\n      const listText = sanitizeText(block.text)\n      if (!listText) continue\n      // 텍스트가 이미 번호로 시작하면 그대로 출력 (원래 번호 보존)\n      const alreadyNumbered = block.listType === \"ordered\" && /^\\d+\\.\\s/.test(listText)\n      const prefix = alreadyNumbered ? \"\" : block.listType === \"ordered\" ? \"1. \" : \"- \"\n      lines.push(`${prefix}${escapeGfm(listText)}`)\n      if (block.children) {\n        for (const child of block.children) {\n          const childPrefix = child.listType === \"ordered\" ? \"1.\" : \"-\"\n          lines.push(`  ${childPrefix} ${escapeGfm(child.text || \"\")}`)\n        }\n      }\n      continue\n    }\n\n    if (block.type === \"paragraph\" && block.text) {\n      let text = sanitizeText(block.text)\n      if (!text) continue\n\n      // 별표 패턴 (기존 호환)\n      if (/^\\[별표\\s*\\d+/.test(text)) {\n        const nextBlock = blocks[i + 1]\n        if (nextBlock?.type === \"paragraph\" && nextBlock.text && /관련\\)?$/.test(nextBlock.text)) {\n          lines.push(\"\", `## ${escapeGfm(text)} ${escapeGfm(nextBlock.text)}`, \"\")\n          i++\n        } else {\n          lines.push(\"\", `## ${escapeGfm(text)}`, \"\")\n        }\n        continue\n      }\n\n      if (/^\\([^)]*조[^)]*관련\\)$/.test(text)) {\n        lines.push(`*${escapeGfm(text)}*`, \"\")\n        continue\n      }\n\n      // gongmun 리스트 depth 선행 공백 (v4.0.5) — sanitizeText가 선두 공백을 지우므로\n      // 텍스트가 아니라 방출 시점에 붙인다. 2칸/단계 = md-runs 리스트 그리드와 동일\n      const listIndent = block.listDepth ? \"  \".repeat(block.listDepth) : \"\"\n\n      // 인라인 강조 run-span (왕복 채널 — 자사 생성 파일 + 외래 실속성 복원) → 마커 재방출.\n      // 마커 자체는 이스케이프 대상이 아니므로 span 내부 텍스트만 escapeGfm 후 감싼다\n      if (block.spans?.length) {\n        let rendered = spansToMarkdown(block.spans)\n        if (block.href) {\n          const href = sanitizeHref(block.href)\n          if (href) rendered = `[${rendered}](${href})`\n        }\n        if (block.footnoteText) rendered += ` (주: ${block.footnoteText})`\n        lines.push(block.quote ? \"> \" + rendered : listIndent + rendered, \"\")\n        continue\n      }\n\n      // 하이퍼링크가 있으면 텍스트에 링크 삽입 (javascript: 등 위험 스킴 제거)\n      if (block.href) {\n        const href = sanitizeHref(block.href)\n        if (href) text = `[${text}](${href})`\n      }\n\n      // 각주가 있으면 괄호로 인라인 삽입\n      if (block.footnoteText) {\n        text += ` (주: ${block.footnoteText})`\n      }\n\n      lines.push(block.quote ? \"> \" + escapeGfm(text) : listIndent + escapeGfm(text), \"\")\n    } else if (block.type === \"table\" && block.table) {\n      // 테이블 앞에 빈 줄 보장 (마크다운 렌더링 필수)\n      if (lines.length > 0 && lines[lines.length - 1] !== \"\") {\n        lines.push(\"\")\n      }\n      // 표 캡션 — 표 위에 강조 문단으로 출력 (v3.0)\n      if (block.table.caption) {\n        const caption = sanitizeText(block.table.caption)\n        if (caption) lines.push(`**${escapeGfm(caption)}**`, \"\")\n      }\n      const tableMd = tableToMarkdown(block.table)\n      if (tableMd) {\n        lines.push(tableMd)\n        lines.push(\"\")\n      }\n    }\n  }\n\n  return lines.join(\"\\n\").trim()\n}\n\n/** 병합 셀 존재 여부 확인 */\nfunction hasMergedCells(table: IRTable): boolean {\n  for (const row of table.cells) {\n    for (const cell of row) {\n      if (cell.colSpan > 1 || cell.rowSpan > 1) return true\n    }\n  }\n  return false\n}\n\n/** 셀 내부에 중첩 표 블록 존재 여부 — v3.0 */\nfunction hasNestedTables(table: IRTable): boolean {\n  for (const row of table.cells) {\n    for (const cell of row) {\n      if (cell.blocks?.some(b => b.type === \"table\" && b.table)) return true\n    }\n  }\n  return false\n}\n\n/** 셀 내부 콘텐츠 → HTML — blocks(중첩표/다중문단) 있으면 구조 보존 재귀 렌더링 */\nfunction cellInnerHtml(cell: IRCell): string {\n  if (cell.blocks?.length) {\n    return cell.blocks\n      .map(b => {\n        if (b.type === \"table\" && b.table) {\n          // 중첩표 캡션도 보존 — 표 위에 텍스트로\n          const cap = b.table.caption ? sanitizeText(b.table.caption) : \"\"\n          return (cap ? cap + \"<br>\" : \"\") + tableToHtml(b.table)\n        }\n        if (b.type === \"image\" && b.text) return `<img src=\"${b.text}\" alt=\"image\">`\n        const t = sanitizeText(b.text ?? \"\")\n        return t ? t.replace(/\\n/g, \"<br>\") : \"\"\n      })\n      .filter(Boolean)\n      .join(\"<br>\")\n  }\n  return sanitizeText(cell.text).replace(/\\n/g, \"<br>\")\n}\n\nfunction containsInlineMath(text: string): boolean {\n  // 교대 중복 금지: [^$\\n\\\\]가 백슬래시를 제외해 \\\\.와 겹치지 않는다 —\n  // 겹치면 \"$\"+백슬래시 연속 입력에서 지수 백트래킹(ReDoS)\n  return /(^|[^\\\\])\\$(?=\\S)(?:[^$\\n\\\\]|\\\\.)+?\\S\\$/.test(text)\n}\n\nfunction tableContainsInlineMath(table: IRTable): boolean {\n  for (const row of table.cells) {\n    for (const cell of row) {\n      if (containsInlineMath(cell.text)) return true\n    }\n  }\n  return false\n}\n\n/** 병합 테이블 → HTML <table> 출력 (rowspan/colspan 보존) */\nfunction tableToHtml(table: IRTable): string {\n  const { cells, rows: numRows, cols: numCols } = table\n  const skip = new Set<string>()\n  const lines: string[] = [\"<table>\"]\n\n  for (let r = 0; r < numRows; r++) {\n    const tag = r === 0 ? \"th\" : \"td\"\n    const rowHtml: string[] = []\n    for (let c = 0; c < numCols; c++) {\n      if (skip.has(`${r},${c}`)) continue\n      const cell = cells[r]?.[c]\n      if (!cell) continue\n\n      // 병합 영역 skip 마킹\n      for (let dr = 0; dr < cell.rowSpan; dr++) {\n        for (let dc = 0; dc < cell.colSpan; dc++) {\n          if (dr === 0 && dc === 0) continue\n          if (r + dr < numRows && c + dc < numCols) skip.add(`${r + dr},${c + dc}`)\n        }\n      }\n\n      const text = cellInnerHtml(cell)\n      const attrs: string[] = []\n      if (cell.colSpan > 1) attrs.push(`colspan=\"${cell.colSpan}\"`)\n      if (cell.rowSpan > 1) attrs.push(`rowspan=\"${cell.rowSpan}\"`)\n      const attrStr = attrs.length ? \" \" + attrs.join(\" \") : \"\"\n      rowHtml.push(`<${tag}${attrStr}>${text}</${tag}>`)\n    }\n    if (rowHtml.length) lines.push(`<tr>${rowHtml.join(\"\")}</tr>`)\n  }\n\n  lines.push(\"</table>\")\n  return lines.join(\"\\n\")\n}\n\nfunction tableToMarkdown(table: IRTable): string {\n  if (table.rows === 0 || table.cols === 0) return \"\"\n\n  const { cells, rows: numRows, cols: numCols } = table\n\n  // 병합 셀·중첩표가 있으면 HTML 테이블로 출력하되, 수식이 있으면 GFM 표로 출력한다.\n  // 많은 Markdown 렌더러가 raw HTML table 내부의 $...$를 수식으로 다시 처리하지 않는다.\n  if ((hasMergedCells(table) || hasNestedTables(table)) && !tableContainsInlineMath(table)) {\n    return tableToHtml(table)\n  }\n\n  // 1행 1열 → 구조화된 텍스트 (빈 셀이면 스킵)\n  if (numRows === 1 && numCols === 1) {\n    const content = sanitizeText(cells[0][0].text)\n    if (!content) return \"\"\n    return content\n      .split(/\\n/)\n      .map(line => {\n        const trimmed = line.trim()\n        if (!trimmed) return \"\"\n        if (/^\\d+\\.\\s/.test(trimmed)) return `**${escapeGfm(trimmed)}**`\n        if (/^[가-힣]\\.\\s/.test(trimmed)) return `  ${escapeGfm(trimmed)}`\n        return escapeGfm(trimmed)\n      })\n      .filter(Boolean)\n      .join(\"\\n\")\n  }\n\n  // 1열 다행 테이블 → 각 행을 별도 라인으로 출력 (목록성 데이터)\n  if (numCols === 1 && numRows >= 2) {\n    return cells\n      .map(row => escapeGfm(sanitizeText(row[0].text)).replace(/\\n/g, \" \"))\n      .filter(Boolean)\n      .join(\"\\n\")\n  }\n\n  // 병합 셀: 행/열 병합된 셀은 빈 칸으로\n  const display: string[][] = Array.from({ length: numRows }, () => Array(numCols).fill(\"\"))\n  const skip = new Set<string>()\n\n  for (let r = 0; r < numRows; r++) {\n    for (let c = 0; c < numCols; c++) {\n      if (skip.has(`${r},${c}`)) continue\n      const cell = cells[r]?.[c]\n      if (!cell) continue\n      // 왕복 채널 셀 spans (v4.0.4) — 강조 마커 재방출 (문단별, 개행은 <br> 규약)\n      display[r][c] = (cell.blocks?.some(b => b.spans)\n        ? cell.blocks\n          .map(b => b.type === \"image\" && b.text\n            ? `![image](${b.text})`\n            : b.spans ? spansToMarkdown(b.spans) : escapeGfm(sanitizeText(b.text ?? \"\")))\n          .filter(Boolean)\n          .join(\"<br>\")\n        : escapeGfm(sanitizeText(cell.text)).replace(/\\n/g, \"<br>\")\n      ).replace(/\\|/g, \"\\\\|\")\n\n      // colSpan/rowSpan: 병합된 열은 빈 칸으로 유지 (텍스트 중복 방지)\n      for (let dr = 0; dr < cell.rowSpan; dr++) {\n        for (let dc = 0; dc < cell.colSpan; dc++) {\n          if (dr === 0 && dc === 0) continue\n          if (r + dr < numRows && c + dc < numCols) {\n            skip.add(`${r + dr},${c + dc}`)\n          }\n        }\n      }\n      // colSpan > 1이면 display 열 인덱스를 건너뜀\n      c += cell.colSpan - 1\n    }\n  }\n\n  // rowSpan 잔류 처리:\n  // 1) 완전 빈 행 제거\n  // 2) \"첫 열만 값, 나머지 빈\" 행 → 다음 데이터 행의 첫 열에 값을 전파\n  //    단, colSpan으로 인한 빈 열(skip 셀)은 이 대상이 아님\n  const uniqueRows: string[][] = []\n  let pendingLabelRow: string[] | null = null\n  for (let r = 0; r < display.length; r++) {\n    const row = display[r]\n    if (row.every(cell => cell === \"\")) {\n      // rowSpan 잔류 행(병합 커버 셀 포함 — 병합+수식 GFM 경로)만 제거.\n      // 앵커 셀이 전부 빈 텍스트인 진짜 빈 행은 문서 구조 — 보존해야 왕복이 성립한다.\n      if (row.some((_, c) => skip.has(`${r},${c}`))) continue\n      if (pendingLabelRow) { uniqueRows.push(pendingLabelRow); pendingLabelRow = null }\n      uniqueRows.push(row)\n      continue\n    }\n\n    // 첫 열만 값이 있고 나머지 모두 빈 행 → 다음 데이터 행의 첫 열에 전파\n    // 단, colSpan으로 인한 빈 열(skip 셀)은 \"진짜 빈\"이 아니므로 제외\n    const nonEmptyCols = row.filter(cell => cell !== \"\")\n    const hasSkipInRow = row.some((_, c) => skip.has(`${r},${c}`))\n    if (!hasSkipInRow && nonEmptyCols.length === 1 && row[0] !== \"\" && row.slice(1).every(c => c === \"\")) {\n      if (pendingLabelRow) uniqueRows.push(pendingLabelRow) // 연속 보류 — 앞 행 소실 방지\n      pendingLabelRow = row\n      continue\n    }\n\n    // 보류한 첫 열 값을 현재 행의 빈 첫 열에 전파, 전파 불가면 보류 행 그대로 출력\n    if (pendingLabelRow) {\n      if (row[0] === \"\") row[0] = pendingLabelRow[0]\n      else uniqueRows.push(pendingLabelRow)\n      pendingLabelRow = null\n    }\n    uniqueRows.push(row)\n  }\n  if (pendingLabelRow) uniqueRows.push(pendingLabelRow) // 표 끝 보류 행 소실 방지\n\n  if (uniqueRows.length === 0) return \"\"\n\n  const md: string[] = []\n  md.push(\"| \" + uniqueRows[0].join(\" | \") + \" |\")\n  md.push(\"| \" + uniqueRows[0].map(() => \"---\").join(\" | \") + \" |\")\n  for (let i = 1; i < uniqueRows.length; i++) {\n    md.push(\"| \" + uniqueRows[i].join(\" | \") + \" |\")\n  }\n  return md.join(\"\\n\")\n}\n","/**\n * 순수 JS BMP → PNG 트랜스코더 + 마크다운 이미지 인라이너.\n *\n * HWP5 BinData 임베드 이미지는 비압축 BMP(수 MB)인 경우가 많다. AI 에이전트가\n * 자체 완결형 마크다운을 받도록, BMP 를 PNG(무손실 압축)로 변환한 뒤 base64\n * data URI 로 마크다운에 인라인한다.\n *\n * 의존성 최소화 원칙 — Node 내장(node:zlib, Buffer)만 사용한다.\n */\n\nimport { deflateSync } from \"node:zlib\"\n\n// ─── CRC32 (PNG 청크용) ──────────────────────────────\n\nconst CRC_TABLE = (() => {\n  const table = new Uint32Array(256)\n  for (let n = 0; n < 256; n++) {\n    let c = n\n    for (let k = 0; k < 8; k++) {\n      c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1)\n    }\n    table[n] = c >>> 0\n  }\n  return table\n})()\n\n/** 표준 PNG CRC32 (다항식 0xEDB88320) */\nfunction crc32(bytes: Uint8Array): number {\n  let c = 0xffffffff\n  for (let i = 0; i < bytes.length; i++) {\n    c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)\n  }\n  return (c ^ 0xffffffff) >>> 0\n}\n\n// ─── BMP 디코드 ──────────────────────────────────────\n\nconst BI_RGB = 0\n/** 폭·높이 상한 — 비정상 헤더에 의한 과대 할당 방지 */\nconst MAX_DIM = 0x7fff\n/**\n * 총 픽셀(W×H) 상한 — MAX_DIM 은 각 변만 제한하므로 곱의 폭주(과대 할당 + 긴 deflateSync\n * 동기 블로킹)를 별도로 차단한다. raw 저장 BMP 는 상위 inflate 100MB 캡을 우회하므로 필요.\n * 36MP = A4 전면 600dpi 스캔(≈35MP)까지 허용 — 그 이상은 rgba+raw 로 300MB 를 넘겨\n * MCP 서버 이벤트루프를 수 초 세우므로 원본 바이트 폴백으로 돌린다.\n */\nconst MAX_PIXELS = 36_000_000\n\n/**\n * 비압축 BMP(24/32bpp, BI_RGB) → PNG(8bit RGBA).\n *\n * 지원: BITMAPFILEHEADER + BITMAPINFOHEADER(biSize>=40), biBitCount 24/32,\n * biCompression BI_RGB, bottom-up(양수 biHeight)/top-down(음수), 24bpp 4바이트 행 패딩.\n * 미지원 변형(팔레트/8bpp·RLE·BITFIELDS·OS/2 헤더 등)은 null 을 반환하여\n * 호출부가 원본 바이트로 폴백하게 한다.\n */\nexport function bmpToPng(bmp: Uint8Array): Uint8Array | null {\n  // BITMAPFILEHEADER(14) + BITMAPINFOHEADER(40)\n  if (bmp.length < 54) return null\n  if (bmp[0] !== 0x42 || bmp[1] !== 0x4d) return null // \"BM\"\n\n  const dv = new DataView(bmp.buffer, bmp.byteOffset, bmp.byteLength)\n  const dataOffset = dv.getUint32(10, true)\n  const headerSize = dv.getUint32(14, true)\n  if (headerSize < 40) return null // OS/2 BITMAPCOREHEADER 등 미지원\n\n  const width = dv.getInt32(18, true)\n  const rawHeight = dv.getInt32(22, true)\n  const bitCount = dv.getUint16(28, true)\n  const compression = dv.getUint32(30, true)\n\n  if (compression !== BI_RGB) return null // RLE / BITFIELDS 미지원\n  if (bitCount !== 24 && bitCount !== 32) return null // 팔레트 / 8bpp 등 미지원\n  if (width <= 0 || rawHeight === 0) return null\n  if (width > MAX_DIM || Math.abs(rawHeight) > MAX_DIM) return null\n\n  const topDown = rawHeight < 0\n  const height = Math.abs(rawHeight)\n  // 총 픽셀 상한 검사 — rgba/raw 할당·deflate 전에 조기 차단 (각 변은 MAX_DIM 이하여도 곱은 폭주 가능)\n  if (width * height > MAX_PIXELS) return null\n  const bytesPerPixel = bitCount >> 3\n  // 행 스트라이드 — 4바이트 경계 패딩 (24bpp 에서 유효)\n  const rowStride = (width * bytesPerPixel + 3) & ~3\n  if (dataOffset + rowStride * height > bmp.length) return null // 잘린 파일\n\n  const rgba = new Uint8Array(width * height * 4)\n  let anyAlpha = 0\n\n  for (let y = 0; y < height; y++) {\n    // bottom-up 은 첫 행이 이미지 맨 아래 → 뒤집어 읽는다\n    const srcRow = topDown ? y : height - 1 - y\n    let src = dataOffset + srcRow * rowStride\n    let dst = y * width * 4\n    for (let x = 0; x < width; x++) {\n      // BMP 픽셀은 BGR(A) 순\n      rgba[dst] = bmp[src + 2]     // R\n      rgba[dst + 1] = bmp[src + 1] // G\n      rgba[dst + 2] = bmp[src]     // B\n      const a = bitCount === 32 ? bmp[src + 3] : 255\n      rgba[dst + 3] = a\n      anyAlpha |= a\n      src += bytesPerPixel\n      dst += 4\n    }\n  }\n\n  // 32bpp BI_RGB 의 4번째 바이트는 \"미정의\" — 전부 0 이면 전면 투명으로 오해되므로\n  // 불투명(255)으로 승격한다. 실제 알파가 있으면(anyAlpha!=0) 그대로 보존.\n  if (bitCount === 32 && anyAlpha === 0) {\n    for (let i = 3; i < rgba.length; i += 4) rgba[i] = 255\n  }\n\n  return encodePng(width, height, rgba)\n}\n\n// ─── PNG 인코드 (8bit RGBA, color type 6) ────────────\n\nconst PNG_SIGNATURE = Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)\n\n/** [4B 길이][4B 타입][데이터][4B CRC] 청크 조립. CRC 는 (타입+데이터) 기준. */\nfunction chunk(type: string, data: Uint8Array): Uint8Array {\n  const body = new Uint8Array(4 + data.length)\n  body[0] = type.charCodeAt(0)\n  body[1] = type.charCodeAt(1)\n  body[2] = type.charCodeAt(2)\n  body[3] = type.charCodeAt(3)\n  body.set(data, 4)\n\n  const out = new Uint8Array(8 + data.length + 4)\n  const dv = new DataView(out.buffer)\n  dv.setUint32(0, data.length, false) // 길이(데이터만, big-endian)\n  out.set(body, 4)\n  dv.setUint32(8 + data.length, crc32(body), false)\n  return out\n}\n\n/** 8bit RGBA 픽셀 버퍼 → PNG (PDF 이미지 XObject 추출에서도 사용) */\nexport function encodePng(width: number, height: number, rgba: Uint8Array): Uint8Array {\n  // IHDR: width, height, bit depth 8, color type 6(RGBA), compression 0, filter 0, interlace 0\n  const ihdr = new Uint8Array(13)\n  const iv = new DataView(ihdr.buffer)\n  iv.setUint32(0, width, false)\n  iv.setUint32(4, height, false)\n  ihdr[8] = 8\n  ihdr[9] = 6\n\n  // IDAT: 각 스캔라인 앞에 필터 바이트 0(None) 을 붙인 raw 데이터를 zlib deflate\n  const stride = width * 4\n  const raw = new Uint8Array((stride + 1) * height)\n  for (let y = 0; y < height; y++) {\n    const rowStart = y * (stride + 1)\n    raw[rowStart] = 0 // filter: None\n    raw.set(rgba.subarray(y * stride, y * stride + stride), rowStart + 1)\n  }\n  const idat = deflateSync(raw)\n\n  const ihdrChunk = chunk(\"IHDR\", ihdr)\n  const idatChunk = chunk(\"IDAT\", idat)\n  const iendChunk = chunk(\"IEND\", new Uint8Array(0))\n\n  const out = new Uint8Array(PNG_SIGNATURE.length + ihdrChunk.length + idatChunk.length + iendChunk.length)\n  let o = 0\n  out.set(PNG_SIGNATURE, o); o += PNG_SIGNATURE.length\n  out.set(ihdrChunk, o); o += ihdrChunk.length\n  out.set(idatChunk, o); o += idatChunk.length\n  out.set(iendChunk, o)\n  return out\n}\n\n// ─── 마크다운 이미지 인라이너 ────────────────────────\n\ninterface InlineImage {\n  filename: string\n  data: Uint8Array\n  mimeType: string\n}\n\n/**\n * 마크다운의 이미지 참조를 base64 data URI 로 치환해 자체 완결형 마크다운을 만든다.\n * 두 방출 형태를 모두 커버한다:\n * - `![image](FILENAME)` / `![image](images/FILENAME)` — 일반 문단 (CLI 접두사 포함)\n * - `<img src=\"FILENAME\">` — 병합/중첩 표 셀 (table/builder.ts HTML 표 경로).\n *   이걸 빼면 CLI `--inline-images` 가 이미지 저장을 생략하면서 표 셀 이미지만\n *   dangling 참조로 유실된다.\n *\n * @param opts.compress 기본 true. `image/bmp` 는 PNG 로 트랜스코딩 후 인라인하며,\n *                      트랜스코딩 실패 시 원본 바이트+원본 MIME 으로 폴백한다.\n */\nexport function inlineImagesIntoMarkdown(\n  markdown: string,\n  images: ReadonlyArray<InlineImage>,\n  opts?: { compress?: boolean },\n): string {\n  const compress = opts?.compress !== false\n  // 파일명 → data URI 사전 구축 후 단일 패스 치환 — 이미지 k개마다 전문 재스캔하던\n  // O(k²) 제거. 맵에 없는 파일명 참조는 원문 그대로 둔다 (기존 계약 동일).\n  const uris = new Map<string, string>()\n  for (const img of images) {\n    let bytes = img.data\n    let mime = img.mimeType\n    if (compress && mime === \"image/bmp\") {\n      const png = bmpToPng(img.data)\n      if (png) {\n        bytes = png\n        mime = \"image/png\"\n      }\n    }\n    uris.set(img.filename, `data:${mime};base64,${Buffer.from(bytes).toString(\"base64\")}`)\n  }\n  if (uris.size === 0) return markdown\n  // FILENAME 과 images/FILENAME 접두사를 모두 정확한 파일명 기준으로 치환\n  let out = markdown.replace(/!\\[image\\]\\((?:images\\/)?([^()\\s]+)\\)/g, (m, name: string) => {\n    const uri = uris.get(name)\n    return uri !== undefined ? `![image](${uri})` : m\n  })\n  out = out.replace(/(<img\\b[^>]*\\bsrc=\")(?:images\\/)?([^\"]*)(\")/g, (m, pre: string, name: string, post: string) => {\n    const uri = uris.get(name)\n    return uri !== undefined ? `${pre}${uri}${post}` : m\n  })\n  return out\n}\n"]}