from __future__ import annotations import re import markdown as md_lib from .errors import ValidationIssue _HEADING_RE = re.compile(r"^(#{1,6})(\s+.*)$") def extract_title(markdown_text: str, level: int, strict: bool, context: str, issues: list[ValidationIssue]) -> tuple[str, str] | None: pattern = re.compile(rf"^{'#' * level}\s+(.*)$", re.MULTILINE) matches = list(pattern.finditer(markdown_text)) if strict and len(matches) != 1: issues.append( ValidationIssue( f"Expected exactly one level-{level} heading, found {len(matches)}", context=context, ) ) return None if not matches: issues.append(ValidationIssue(f"Missing level-{level} heading", context=context)) return None match = matches[0] title = match.group(1).strip() if not title: issues.append(ValidationIssue("Heading title cannot be empty", context=context)) return None lines = markdown_text.splitlines() line_index = markdown_text[: match.start()].count("\n") lines.pop(line_index) body = "\n".join(lines) body = _promote_headings(body) return title, body def _promote_headings(text: str) -> str: promoted_lines = [] for line in text.splitlines(): match = _HEADING_RE.match(line) if not match: promoted_lines.append(line) continue hashes, rest = match.groups() level = len(hashes) if level > 1: level -= 1 promoted_lines.append("#" * level + rest) return "\n".join(promoted_lines) def convert_markdown(markdown_text: str, context: str, issues: list[ValidationIssue]) -> str | None: try: return md_lib.markdown(markdown_text, extensions=["extra"], output_format="html5") except Exception as exc: # pragma: no cover - depends on markdown internals issues.append(ValidationIssue(f"Markdown conversion failed: {exc}", context=context)) return None