#!/usr/bin/env python3 """ Checks that no `.md` files contain invalid syntax. Usage: python mdlint.py python mdlint.py explain e001 python mdlint.py lint docs/content/**/*.md """ from __future__ import annotations import argparse import re import sys import textwrap from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from glob import glob from pathlib import Path from typing import Self @dataclass class Span: start: int end: int def offset(self, by: int) -> Self: self.start += by self.end += by return self def slice(self, content: str) -> str: return content[self.start : self.end] def extend_span(content: str, span: Span) -> Span: """ Extends the span (start, end) to contain all lines it inhabits. For example (with `|` denoting span bounds): ``` ``` Turns into: ``` | | ``` """ if content[span.start] == "\n": actual_start = span.start + 1 else: actual_start = content.rfind("\n", 0, span.start) if actual_start == -1: actual_start = 0 if content[span.end] == "\n": actual_end = span.end else: actual_end = content.find("\n", span.end) if actual_end == -1: actual_end = len(content) return Span(start=actual_start, end=actual_end) def get_line_num(content: str, start: int) -> int: return 1 + content[:start].count("\n") @dataclass class Error: code: str message: str span: Span def render(self, filepath: str, content: str) -> str: span = extend_span(content, self.span) line_num = get_line_num(content, span.end) lines = span.slice(content).splitlines() line_num_align = len(f"{line_num + len(lines)}") out: list[str] = [ f"error[{self.code}]: {self.message}", f"{' '.rjust(line_num_align)}--> {filepath}:{line_num}", ] for line in lines: num = f"{line_num}".ljust(line_num_align) out.append(f"{num} | {line}") line_num += 1 return "\n".join(out) + "\n" class NoClosingTagError(Error): CODE = "E001" def __init__(self, tagname: str, span: Span) -> None: super().__init__(type(self).CODE, f"<{tagname}> has no closing tag", span) @staticmethod def explain() -> str: return textwrap.dedent( """ If a block element such as `