CodeRigor

LESSON

Define the language before writing the parser

Document the supported subset, normalize line endings and retain one-based source locations.

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.

A language contract precedes output
start / 3
Bounded source text is normalized, parsed under an explicit grammar and rendered through a trusted template.
A language contract precedes output
Bounded source text is normalized, parsed under an explicit grammar and rendered through a trusted template.

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.

Python
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.

Locations describe source characters
start / 3
Count normalized newlines for the line and Unicode code points after the last newline for the column. Byte length is a separate output metric.
Locations describe source characters
Count normalized newlines for the line and Unicode code points after the last newline for the column. Byte length is a separate output metric.
Python
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.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Optional file-I/O companion for the local build command. The original parser and HTML-escaping rules are taught in the authored lessons.