feat(okf): add okf_common frontmatter/link parser with tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-16 19:35:13 +09:00
parent 4416833cb3
commit 9762ee97ab
2 changed files with 125 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
"""Shared OKF v0.1 parsing utilities (Python standard library only)."""
from __future__ import annotations
import re
from pathlib import Path
RESERVED = {"index.md", "log.md"}
_LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
class FrontmatterError(ValueError):
"""Raised when a frontmatter block is present but cannot be parsed."""
def split_frontmatter(text):
"""Return (raw_frontmatter, body). raw is None if there is no leading '---' block."""
if not text.startswith("---"):
return None, text
lines = text.splitlines()
if lines[0].strip() != "---":
return None, text
for i in range(1, len(lines)):
if lines[i].strip() == "---":
return "\n".join(lines[1:i]), "\n".join(lines[i + 1:])
raise FrontmatterError("opening '---' without a closing '---'")
def _scalar(value):
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
return value[1:-1]
return value
def parse_yaml_subset(raw):
"""Parse the small YAML subset OKF uses: `key: value` and `key: [a, b]`."""
meta = {}
for line in raw.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if ":" not in line:
raise FrontmatterError("unparseable frontmatter line: %r" % line)
key, _, value = line.partition(":")
key, value = key.strip(), value.strip()
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)
return meta
def parse_frontmatter(text):
"""Return (meta, body). meta is None when no frontmatter block is present."""
raw, body = split_frontmatter(text)
if raw is None:
return None, text
return parse_yaml_subset(raw), body
def iter_concepts(bundle_dir):
"""Yield Path for every non-reserved .md file under bundle_dir, sorted."""
root = Path(bundle_dir)
for path in sorted(root.rglob("*.md")):
if path.name not in RESERVED:
yield path
def concept_id(bundle_dir, path):
"""Concept ID = bundle-relative path with the .md suffix removed."""
rel = Path(path).relative_to(Path(bundle_dir)).as_posix()
return rel[:-3] if rel.endswith(".md") else rel
def extract_links(body):
"""Return the list of link targets from markdown links in body."""
return _LINK_RE.findall(body)