"""Narrow repairs for malformed nested Markdown fence examples. Models occasionally wrap a fenced example in an equally long ``markdown`` fence. CommonMark cannot nest those fences: the inner closing fence closes the outer block and the intended outer close opens a new, unclosed block. Only that unambiguous shape is repaired here; arbitrary unclosed fences are reported but left untouched. """ from __future__ import annotations import re from dataclasses import dataclass _FENCE_RE = re.compile(r"^( {0,3})(`{3,}|~{3,})([^\r\n]*)(\r?\n)?$") @dataclass(frozen=True) class MarkdownFenceResult: text: str repairs: int = 0 unclosed_fence: bool = False def _fence(line: str) -> tuple[str, str, int, str] | None: match = _FENCE_RE.match(line) if not match: return None token = match.group(2) return match.group(1), token[0], len(token), match.group(3).strip() def _next_nonblank(lines: list[str], start: int) -> int | None: for idx in range(start, len(lines)): if lines[idx].strip(): return idx return None def _replace_fence(line: str, length: int) -> str: match = _FENCE_RE.match(line) if not match: return line return ( match.group(1) + match.group(2)[0] * length + match.group(3) + (match.group(4) or "") ) def _has_unclosed_fence(lines: list[str]) -> bool: opened: tuple[str, int] | None = None for line in lines: parsed = _fence(line) if parsed is None: continue _indent, char, length, info = parsed if opened is None: opened = (char, length) elif not info and char == opened[0] and length >= opened[1]: opened = None return opened is not None def normalize_markdown_fences(text: str) -> MarkdownFenceResult: """Repair only an equally-long fenced block nested in markdown/md. Recognised shape (blank lines between the two closing fences are allowed): ``markdown opener -> language opener -> language close -> outer close``. The outer pair is lengthened by one character. Other malformed input is preserved so the platform never guesses broadly at author intent. """ if not text: return MarkdownFenceResult(text=text) lines = text.splitlines(keepends=True) repairs = 0 idx = 0 while idx < len(lines): outer = _fence(lines[idx]) if outer is None or outer[3].lower() not in {"markdown", "md"}: idx += 1 continue inner_idx = _next_nonblank(lines, idx + 1) inner = _fence(lines[inner_idx]) if inner_idx is not None else None if ( inner is None or not inner[3] or inner[1] != outer[1] or inner[2] < outer[2] ): idx += 1 continue assert inner_idx is not None inner_close_idx = None for candidate in range(inner_idx + 1, len(lines)): closing = _fence(lines[candidate]) if ( closing is not None and not closing[3] and closing[1] == inner[1] and closing[2] >= inner[2] ): inner_close_idx = candidate break if inner_close_idx is None: idx += 1 continue outer_close_idx = _next_nonblank(lines, inner_close_idx + 1) outer_close = _fence(lines[outer_close_idx]) if outer_close_idx is not None else None if ( outer_close is None or outer_close[3] or outer_close[1] != outer[1] or outer_close[2] < outer[2] ): idx += 1 continue assert outer_close_idx is not None repaired_length = max(outer[2], inner[2]) + 1 lines[idx] = _replace_fence(lines[idx], repaired_length) lines[outer_close_idx] = _replace_fence(lines[outer_close_idx], repaired_length) repairs += 1 idx = outer_close_idx + 1 normalized = "".join(lines) return MarkdownFenceResult( text=normalized, repairs=repairs, unclosed_fence=_has_unclosed_fence(lines), )