Block structure decides whether a line is a heading, paragraph, list item or literal code. Parse it before looking for inline emphasis and links. Otherwise a # inside a code fence can accidentally become a heading.
Our subset uses blank-separated ordinary blocks. A heading occupies one line; a flat list has one - item per source line; a paragraph can wrap across ordinary lines. Inline constructs must open and close on one source line. These rules are smaller than general Markdown and are documented deliberately.
A fence opener is exactly with optional text or python immediately after it. The closing line is exactly. Everything between them is literal, including blank lines, indentation and strings that look like markup. An absent closer reports the opener’s location.
For other groups, classify the first line and reject mixed structure. A paragraph followed immediately by a - item needs a blank separator under this language contract.
lines = ["# Title", "", "First paragraph", "second line", "", "- one", "- two"]
groups, current = [], []
for number,text in enumerate(lines,1):
if not text.strip():
if current:groups.append(current);current=[]
else:current.append((text,number))
if current:groups.append(current)
print(groups)Store content with its original line and content-start column. For # headings the first text column is three; for ## it is four; for - items it is three. A later unclosed inline marker can then report the actual source position rather than a column in a silently joined paragraph.
The supplied Block dataclass contains kind, located lines, first line and optional code language. Its tuples are immutable. Parsing creates structure only; it does not emit HTML or open files.
Unsupported indentation, deeper headings, ordered lists, quotes, tables and horizontal rules fail with location/code pairs. A narrow parser should report those limits rather than guessing an interpretation that the renderer cannot honor.
source_line = "## A *small* heading"
prefix = 3
content = source_line[prefix:]
marker = content.index("*")
print("content",content)
print("asterisk source column", prefix+1+marker)Implement the splitter and inspect records for the supplied heading/paragraph/list/code fixture. Test blank code lines and indentation as well as an unclosed fence, mixed blocks and an unsupported language.
Next the inline parser will turn each ordinary content line into text/code/emphasis/link tokens without generating HTML.
Follow the sample through the compiler.
Inspect the authored source, its located block records and the actual styled output from the downloadable generator.
Markdown source
# A small page, built deliberately
This page started as *plain text*. Its structure was parsed before any HTML was generated.
## A useful boundary
- Keep source text as data.
- Give links an explicit policy.
- Publish only after the complete page succeeds.
Literal HTML-like input stays visible: <script>this is displayed, never run</script>.
```python
from pathlib import Path
message = "<source is data>"
print(message)
```
## Continue the project
Inspect [the HTML escaping reference](https://docs.python.org/3.14/library/html.html) and then change one paragraph and rebuild.
The larger [Boot.dev Static Site Generator project](https://www.boot.dev/courses/build-static-site-generator-python) continues into richer nodes and a complete website after classes, recursion and functional programming.
Located block records
[
{
"kind": "h1",
"lines": [
[
"A small page, built deliberately",
1,
3
]
],
"line": 1,
"language": ""
},
{
"kind": "paragraph",
"lines": [
[
"This page started as *plain text*. Its structure was parsed before any HTML was generated.",
3,
1
]
],
"line": 3,
"language": ""
},
{
"kind": "h2",
"lines": [
[
"A useful boundary",
5,
4
]
],
"line": 5,
"language": ""
},
{
"kind": "list",
"lines": [
[
"Keep source text as data.",
7,
3
],
[
"Give links an explicit policy.",
8,
3
],
[
"Publish only after the complete page succeeds.",
9,
3
]
],
"line": 7,
"language": ""
},
{
"kind": "paragraph",
"lines": [
[
"Literal HTML-like input stays visible: <script>this is displayed, never run</script>.",
11,
1
]
],
"line": 11,
"language": ""
},
{
"kind": "code",
"lines": [
[
"from pathlib import Path",
14,
1
],
[
"",
15,
1
],
[
"message = \"<source is data>\"",
16,
1
],
[
"print(message)",
17,
1
]
],
"line": 13,
"language": "python"
},
{
"kind": "h2",
"lines": [
[
"Continue the project",
20,
4
]
],
"line": 20,
"language": ""
},
{
"kind": "paragraph",
"lines": [
[
"Inspect [the HTML escaping reference](https://docs.python.org/3.14/library/html.html) and then change one paragraph and rebuild.",
22,
1
]
],
"line": 22,
"language": ""
},
{
"kind": "paragraph",
"lines": [
[
"The larger [Boot.dev Static Site Generator project](https://www.boot.dev/courses/build-static-site-generator-python) continues into richer nodes and a complete website after classes, recursion and functional programming.",
24,
1
]
],
"line": 24,
"language": ""
}
]The generated page
9 blocks · 2,977 UTF-8 bytes · Repeatable source and output included in the lab.
Download the generator and this sample →