Source code for repomatic.frontmatter
# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Splitting a Markdown document into its YAML frontmatter and body.
Two unrelated families of bundled Markdown carry frontmatter: skill definitions
(`SKILL.md`, whose fields the [Agent Skills
spec](https://agentskills.io/specification) defines) and PR body templates in
`repomatic/templates/`. Both need the same split, so it lives here once rather
than once per consumer.
"""
from __future__ import annotations
import yaml
DELIMITER = "---"
"""Line that opens and closes a frontmatter block."""
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any
[docs]
def split_frontmatter(raw: str) -> tuple[dict[str, Any], str]:
"""Split a document into its parsed frontmatter mapping and its body.
Values keep their YAML types, so a nested field (the spec's `metadata`
mapping, a template's `args` list) reads back as the structure it was
written as rather than a flat string.
```{note}
Both delimiters must sit alone on their own line, per the frontmatter
convention. Scanning for the closing line, instead of splitting the document
on the first two `---` runs, keeps a value that embeds `---` (like an
`argument-hint` listing a long-form option) from truncating the block.
```
:param raw: Full text of the document.
:return: `(frontmatter, body)`. The frontmatter is an empty mapping when the
document opens no block, leaves one unterminated, or holds something
other than a YAML mapping; in each of those cases the body is *raw*
unchanged, so no content is ever silently dropped.
"""
lines = raw.splitlines(keepends=True)
if not lines or lines[0].strip() != DELIMITER:
return {}, raw
for index, line in enumerate(lines[1:], start=1):
if line.strip() == DELIMITER:
break
else:
# Unterminated block: treat the whole document as body.
return {}, raw
parsed = yaml.safe_load("".join(lines[1:index]))
if not isinstance(parsed, dict):
return {}, raw
return parsed, "".join(lines[index + 1 :]).lstrip("\n")