A small parser is easier to trust when its language is explicit. This project accepts headings beginning # or ## followed by a space, blank-separated paragraphs, flat - lists, fenced text/python code blocks, inline code, plain-text emphasis and plain-label links. It does not claim general Markdown or CommonMark compatibility.
You build on escaping and deterministic reports and exceptions/resources. Block and Token are supplied frozen dataclasses: small value records for structure and source locations. Your work is the parsing/rendering functions.
The final artifact is a styled single HTML page and a repeatable local command. First define the text boundary: normalize newlines without stripping source content.
Windows files commonly use CRLF; other files can use LF or a lone CR. Convert CRLF first, then remaining CR. Reversing that order introduces a second LF and can change paragraph boundaries.
Keep spaces and final newlines. Later fenced code preserves indentation literally, while the block parser rejects unsupported indentation outside fences. Normalization should not preempt that rule by silently deleting the evidence.
source = "# Title\r\n\r\nParagraph\rnext line\n"
normalized = source.replace("\r\n","\n").replace("\r","\n")
print(repr(normalized))
print("same line count", normalized.count("\n"))The source budget is 65,536 Unicode characters. Output bytes are measured separately after HTML generation. Reject a NUL character and report its one-based line/column in normalized text. An é counts as one column even though UTF-8 encodes it in two bytes.
Empty source can normalize successfully; a later compiler rejects an empty page. Keeping these stages distinct makes tests precise. A type or size error belongs at this boundary, while an unsupported heading belongs to parsing.
The subset is intentionally visible in the lab README and examples. Unsupported constructs produce useful failures instead of pretending a partially understood document was published correctly. Raw angle-bracket text is displayed as text; it is never accepted as source-provided HTML.
normalized = "first\néx\x00"
index = normalized.find("\x00")
before = normalized[:index]
print("line", before.count("\n")+1)
print("column", len(before.rsplit("\n",1)[-1])+1)
print("bytes before NUL", len(before.encode("utf-8")))Implement normalization and check CRLF, lone CR, empty text, preserved spaces, the exact size boundary and NUL locations. Do not open files in the graded function; the local CLI handles UTF-8 file ownership later.
Next we turn normalized lines into blocks, keeping the original line and column for every content line.