Step 1 of 5 · Reading · ~3 min
Read
Block Elements
Headings
A heading is the simplest block in Markdown, which makes it the right place to learn the habit the whole parser runs on: a line either matches a block rule exactly, or it is not that block at all. There is no "close enough". Getting comfortable with that all-or-nothing test now is what makes lists, fences and blockquotes tractable later.
What turns a line into a heading?
CommonMark calls this style an ATX heading: one to six # characters at the
start of a line, then a space or tab, then the text.
# Release notes -> <h1>Release notes</h1>
###### Deep -> <h6>Deep</h6>
####### Seven -> not a heading — seven hashes
#Tag -> not a heading — no space
Six is the ceiling because HTML stops at <h6>. A seventh # does not clamp
down to six: the line stops being a heading and becomes ordinary paragraph text.
Why insist on the space?
Because without it every hashtag anyone types becomes an <h1>. #5 bolt is a
bolt size. #Tag is a tag. That single space is the entire boundary between
"structure the author meant" and "a character that happened to be typed first".
The spec is explicit that many older parsers skipped this check — which is
exactly why documents written for them render differently everywhere else.
What happens to hashes at the end of the line?
A run of # after the text is optional decoration and gets dropped:
## Ship it ## is just <h2>Ship it</h2>. But the closing run only counts
when a space or tab comes before it. # foo# keeps the hash — it is
<h1>foo#</h1>, not <h1>foo</h1>.
That one rule separates a spec-accurate pattern from a sloppy one:
| Pattern | ## Ship it ## | # foo# |
|---|---|---|
✓ ^(#{1,6})[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$ | Ship it | foo# |
✗ ^(#{1,6})\s+(.+?)\s*#*\s*$ | Ship it | foo — hash eaten |
Both pass this lesson's tests. Only the first is right.
None is the signal "this was not a heading — hand the line back to whoever
else wants it". Your parser will grow more of these handlers; every one of them
answers yes or no and passes on cleanly.
The decision, as the block phase sees it
Markdown's other heading style — text underlined with = or - — has its own
lesson, and its own much nastier ambiguity.
Your exercise
Turn each input line into a heading tag, or pass it through untouched.
The mistake the grader catches is clamping instead of rejecting. If you
write the level as min(len(hashes), 6), the line ####### Not a heading (too many #) comes back as <h6>Not a heading (too many #)</h6>, and the very
first visible test fails on that line. Seven hashes is not a level-7 heading and
not a level-6 heading — it is a plain line, echoed byte for byte.
The second trap is the same shape: #NoSpace must also come back verbatim.
Require the space in the pattern rather than stripping hashes first, and both
cases fall out for free.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…