Files
2025-11-30 09:03:57 +08:00

81 lines
2.1 KiB
Python

"""JSON frontmatter parsing - zero dependencies."""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@dataclass
class Document:
metadata: dict[str, Any]
content: str
def __getitem__(self, key: str) -> Any:
return self.metadata[key]
def get(self, key: str, default: Any = None) -> Any:
return self.metadata.get(key, default)
def _find_json_end(text: str) -> int:
depth = in_string = escape_next = 0
for i, char in enumerate(text):
if escape_next:
escape_next = 0
continue
if char == '\\':
escape_next = 1
continue
if char == '"' and not escape_next:
in_string = not in_string
continue
if in_string:
continue
if char == '{':
depth += 1
elif char == '}':
depth -= 1
if depth == 0:
return i + 1
return -1
def parse(text: str) -> Document:
text = text.lstrip()
if not text.startswith('{'):
raise ValueError("JSON frontmatter must start with '{'")
end_pos = _find_json_end(text)
if end_pos == -1:
raise ValueError("JSON frontmatter not properly closed")
try:
metadata = json.loads(text[:end_pos])
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in frontmatter: {e}")
if not isinstance(metadata, dict):
raise ValueError("JSON frontmatter must be object")
return Document(metadata=metadata, content=text[end_pos:].lstrip('\n'))
def dumps(metadata: dict[str, Any], content: str) -> str:
return f"{json.dumps(metadata, indent=2)}\n\n{content}"
def load(filepath: Path | str) -> Document:
return parse(Path(filepath).read_text())
def dump(doc: Document, filepath: Path | str) -> None:
Path(filepath).write_text(dumps(doc.metadata, doc.content))
def update_metadata(filepath: Path | str, **updates: Any) -> None:
doc = load(filepath)
doc.metadata.update(updates)
dump(doc, filepath)