feat(okf): add okf_validate; harden YAML parser for block lists + folded scalars

Verified conformant against Google reference bundles (crypto_bitcoin, ga4, stackoverflow).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 19:38:31 +09:00
parent 9762ee97ab
commit 7b239dda8f
4 changed files with 223 additions and 4 deletions

View File

@@ -6,6 +6,7 @@ from pathlib import Path
RESERVED = {"index.md", "log.md"}
_LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
_BLOCK_INDICATORS = {">", "|", ">-", "|-", ">+", "|+"}
class FrontmatterError(ValueError):
@@ -33,24 +34,74 @@ def _scalar(value):
def parse_yaml_subset(raw):
"""Parse the small YAML subset OKF uses: `key: value` and `key: [a, b]`."""
"""Parse the small YAML subset OKF bundles use in practice.
Handles: ``key: value``; inline lists ``key: [a, b]``; block lists
(``key:`` followed by ``- item`` lines); folded multi-line scalars
(a value continued on following indented lines); and ``>``/``|`` block
scalars. Surrounding quotes are stripped from scalar values. This is a
deliberately small parser — not a full YAML implementation.
"""
lines = raw.splitlines()
n = len(lines)
meta = {}
for line in raw.splitlines():
i = 0
while i < n:
line = lines[i]
stripped = line.strip()
if not stripped or stripped.startswith("#"):
i += 1
continue
if ":" not in line:
raise FrontmatterError("unparseable frontmatter line: %r" % line)
key, _, value = line.partition(":")
key, value = key.strip(), value.strip()
i += 1
# Inline list: key: [a, b]
if value.startswith("[") and value.endswith("]"):
inner = value[1:-1].strip()
meta[key] = [s for s in (_scalar(x) for x in inner.split(",")) if s] if inner else []
else:
meta[key] = _scalar(value)
continue
# Empty value: either a block list (- item lines) or a continued scalar.
if value == "":
items = []
while i < n and lines[i].strip().startswith("-"):
items.append(_scalar(lines[i].strip()[1:].strip()))
i += 1
if items:
meta[key] = items
else:
cont = []
i = _collect_indented(lines, i, n, cont)
meta[key] = " ".join(cont)
continue
# Explicit block scalar indicator (> folded, | literal).
if value in _BLOCK_INDICATORS:
cont = []
i = _collect_indented(lines, i, n, cont)
meta[key] = ("\n" if value[0] == "|" else " ").join(cont)
continue
# Plain scalar, possibly folded across following indented lines.
parts = [value]
i = _collect_indented(lines, i, n, parts, skip_list_items=True)
meta[key] = _scalar(" ".join(parts))
return meta
def _collect_indented(lines, i, n, out, skip_list_items=False):
"""Append stripped indented continuation lines to ``out``; return new index."""
while i < n and lines[i][:1].isspace() and lines[i].strip():
if skip_list_items and lines[i].strip().startswith("-"):
break
out.append(lines[i].strip())
i += 1
return i
def parse_frontmatter(text):
"""Return (meta, body). meta is None when no frontmatter block is present."""
raw, body = split_frontmatter(text)