"""Phase T-1: MDX 4-Layer 파서. Stage 0에서 호출. 원본 MDX를 정규화하여 이후 모든 Stage에 깨끗한 입력 제공. Layer 1: python-frontmatter — YAML frontmatter 분리, title 추출 Layer 2: regex — 코드블록 보호 + MDX 전용 패턴 (details, :::, JSX, import) Layer 3: markdown-it-py — AST 파싱 → 이미지/표/헤딩 구조 추출 Layer 4: regex — 텍스트 정리, 빈 줄 정리, clean_text 조사 결과 (T-1): - python-frontmatter: parse() → (dict, str). frontmatter 없으면 안전하게 {} - markdown-it-py: js-default 프리셋에 table 기본 포함. 한국어 정상 - 코드블록 보호: backtick 10→3 순서 매칭. 중첩/inline 검증됨 """ from __future__ import annotations import re import logging from typing import Any import frontmatter from markdown_it import MarkdownIt logger = logging.getLogger(__name__) # ══════════════════════════════════════ # 코드블록 보호 (Layer 2 선행) # ══════════════════════════════════════ class _CodeBlockProtector: """코드블록을 placeholder로 보호하고 복원. backtick 개수가 많은 순서(10→3)로 매칭하여 중첩 코드블록 안전 처리. """ def __init__(self): self._store: dict[str, str] = {} self._counter = 0 def _make_key(self) -> str: self._counter += 1 return f"__CODEBLOCK_{self._counter}__" def protect(self, text: str) -> str: # fenced code blocks (큰 backtick부터) for n in range(10, 2, -1): pattern = rf"^(`{{{n}}})([^\n]*)\n(.*?)\n\1\s*$" def _replacer(m, _n=n): key = self._make_key() self._store[key] = m.group(0) return key text = re.sub(pattern, _replacer, text, flags=re.MULTILINE | re.DOTALL) # inline code def _inline_replacer(m): key = self._make_key() self._store[key] = m.group(0) return key text = re.sub(r"`[^`\n]+`", _inline_replacer, text) return text def restore(self, text: str) -> str: for key, original in self._store.items(): text = text.replace(key, original) return text # ══════════════════════════════════════ # Layer 2: MDX 전용 패턴 처리 # ══════════════════════════════════════ def _convert_md_list_to_html(text: str) -> str: """마크다운 리스트(* item, - item)를 HTML