The same three characters, twice in one document, meaning two different things.
text = '''---
title: Hello
---
Some words.
---
More words.'''
# The first --- opens the block. The second closes it.
# The third is a horizontal rule in the body and must be left alone.
text.split('---') # -> four pieces, and the article is now confetti
- Splitting on the fence loses the thing that made it a fence
split('---') treats every occurrence identically, because a split has no concept of where it is in the document. The rule here is positional — first line, then the next matching line, then stop looking — so the code has to walk the lines and keep track of what it is doing. Any rule that mentions 'the first' or 'until' is a scan, not a split.
- Check the opening fence before doing anything else
If the first line is not exactly ---, there is no front matter and the whole text is the body — a two-line early return that removes every other question. Deciding that first also means the rest of the function can assume it is inside a block, which is what keeps it short.
- An unclosed fence is not an empty block
Guessing — treating the whole file as metadata, or silently deciding there was none — produces a page that renders as gibberish or one that quietly loses its title. Both are hard to trace back to a missing line of dashes. Refusing points straight at the file.
Split the text into lines first. If the first one is not the fence, you are already finished — return an empty dict and the original text. Otherwise walk forward looking for the closing fence.
Keep the index of the closing fence as you find it, then the metadata lines are the ones between and the body is everything after. Rejoin the body lines with newlines rather than trying to slice the original string.
text.splitlines() drops the line endings, so joining with '\n' rebuilds the body — but it also loses a trailing newline, so use splitlines(True) or handle the tail deliberately. Parse each metadata line with partition(':'), where an empty separator means there was no colon at all.
def split_front_matter(text): lines = text.split('\n') if not lines or lines[0].strip() != '____': return {}, text closing = None for index in range(1, len(lines)): if lines[index].strip() == '---': closing = index ____ if closing is None: raise ____('front matter is never closed') meta = {} for line in lines[1:closing]: if not line.strip(): continue key, colon, value = line.____(':') if not colon: raise ValueError(f’not a key: value line: {line!r}') meta[key.strip()] = value.strip() rest = lines[closing + 1:] if rest and not rest[0].strip(): rest = rest[1:] return meta, '\n'.____(rest)
def split_front_matter(text):
lines = text.split("\n")
# The fence only opens a block on the very first line.
if not lines or lines[0].strip() != "---":
return {}, text
closing = None
for index in range(1, len(lines)):
if lines[index].strip() == "---":
closing = index
break
if closing is None:
raise ValueError("front matter is opened but never closed")
meta = {}
for line in lines[1:closing]:
if not line.strip():
continue
key, colon, value = line.partition(":")
if not colon:
raise ValueError(f"not a key: value line: {line!r}")
meta[key.strip()] = value.strip()
rest = lines[closing + 1:]
if rest and not rest[0].strip():
rest = rest[1:]
return meta, "\n".join(rest)
Without it the loop keeps going and `closing` ends up pointing at the last row of dashes in the document — so a horizontal rule three paragraphs down silently becomes the end of the metadata block, and everything above it is parsed as keys. The bug produces a ValueError about a line with no colon, which sends you looking in entirely the wrong place.
splitlines() also breaks on form feeds, vertical tabs and a handful of Unicode line separators, which is more than a Markdown file means by 'a line' — and it drops a trailing empty piece that split keeps, so joining back would lose the file’s final newline. When you intend to rejoin, split and join should be exact inverses.
Every generator writes that blank line and no author means it as content, so removing it makes the body start where the writing starts. Removing *all* leading blank lines would be tidier and would silently change a document that deliberately opens with space. One is a convention; more is an opinion.
Real front matter is YAML, and this parses about four per cent of it — no lists, no nesting, no quoting, no multi-line values, and 'no' is a string here where YAML would make it False. Use python-frontmatter, which wraps PyYAML and handles TOML and JSON blocks too. Write this when you control the files and have deliberately kept the metadata to flat key-value pairs, which is a defensible choice — a full YAML parser is a large dependency and a surprisingly large surface area for a blog.
- Support a list value written as a comma-separated string by reusing normalise-tags, and decide which keys should get that treatment.
- Write the inverse — rebuild a document from metadata and body — and check the round trip on every case here.
- Accept +++ and TOML-style fences as well, and see how much of the function has to stop mentioning specific characters.