from __future__ import annotations import re from dataclasses import dataclass from typing import TypedDict _FENCE_LINE_RE = re.compile(r"^( {0,3})(`{3,}|~{3,})(.*)$") class _OpenFence(TypedDict): start: int marker_char: str marker_len: int open_line: str marker: str indent: str @dataclass class FenceSpan: start: int end: int open_line: str marker: str indent: str def parse_fence_spans(buffer: str) -> list[FenceSpan]: spans: list[FenceSpan] = [] open_fence: _OpenFence | None = None offset = 0 while offset <= len(buffer): next_newline = buffer.find("\n", offset) line_end = next_newline if next_newline != -1 else len(buffer) line = buffer[offset:line_end] match = _FENCE_LINE_RE.match(line) if match: indent = match.group(1) marker = match.group(2) marker_char = marker[0] marker_len = len(marker) if open_fence is None: open_fence = { "start": offset, "marker_char": marker_char, "marker_len": marker_len, "open_line": line, "marker": marker, "indent": indent, } elif open_fence["marker_char"] == marker_char and marker_len >= open_fence["marker_len"]: spans.append( FenceSpan( start=open_fence["start"], end=line_end, open_line=open_fence["open_line"], marker=open_fence["marker"], indent=open_fence["indent"], ) ) open_fence = None if next_newline == -1: break offset = next_newline + 1 if open_fence is not None: spans.append( FenceSpan( start=open_fence["start"], end=len(buffer), open_line=open_fence["open_line"], marker=open_fence["marker"], indent=open_fence["indent"], ) ) return spans def find_fence_span_at(spans: list[FenceSpan], index: int) -> FenceSpan | None: low, high = 0, len(spans) - 1 while low <= high: mid = (low + high) // 2 span = spans[mid] if index < span.start: high = mid - 1 elif index > span.end: low = mid + 1 else: return span return None def is_safe_fence_break(spans: list[FenceSpan], index: int) -> bool: return find_fence_span_at(spans, index) is None