feat(content-build): scaffold package + Markdown yaml-block parser
This commit is contained in:
0
tools/content_build/__init__.py
Normal file
0
tools/content_build/__init__.py
Normal file
12
tools/content_build/errors.py
Normal file
12
tools/content_build/errors.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""The single failure type. Every validation/parse problem raises BuildError so
|
||||
the CLI can print a uniform 'BUILD FAILED: <what> [<id>] (<file:line>)'."""
|
||||
|
||||
|
||||
class BuildError(Exception):
|
||||
def __init__(self, message: str, source: str = "", entry_id: str = ""):
|
||||
self.message = message
|
||||
self.source = source
|
||||
self.entry_id = entry_id
|
||||
loc = f" [{entry_id}]" if entry_id else ""
|
||||
src = f" ({source})" if source else ""
|
||||
super().__init__(f"{message}{loc}{src}")
|
||||
40
tools/content_build/parse.py
Normal file
40
tools/content_build/parse.py
Normal file
@@ -0,0 +1,40 @@
|
||||
"""Markdown bible -> raw yaml-block entries. Knows only fences + YAML; nothing
|
||||
about the schema's meaning. A block is opened by a line that is exactly ```yaml
|
||||
(after strip) and closed by a line that is exactly ```."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from .errors import BuildError
|
||||
|
||||
|
||||
@dataclass
|
||||
class RawEntry:
|
||||
data: dict
|
||||
source: str # "duncarrow.md:53"
|
||||
|
||||
|
||||
def parse_bible(path: Path) -> list[RawEntry]:
|
||||
lines = path.read_text().splitlines()
|
||||
entries: list[RawEntry] = []
|
||||
i, n = 0, len(lines)
|
||||
while i < n:
|
||||
if lines[i].strip() == "```yaml":
|
||||
fence_line = i + 1 # 1-based line of the opening fence
|
||||
i += 1
|
||||
block: list[str] = []
|
||||
while i < n and lines[i].strip() != "```":
|
||||
block.append(lines[i])
|
||||
i += 1
|
||||
if i >= n:
|
||||
raise BuildError("unterminated ```yaml block",
|
||||
source=f"{path.name}:{fence_line}")
|
||||
data = yaml.safe_load("\n".join(block))
|
||||
if not isinstance(data, dict):
|
||||
raise BuildError("yaml block is not a mapping",
|
||||
source=f"{path.name}:{fence_line}")
|
||||
entries.append(RawEntry(data=data, source=f"{path.name}:{fence_line}"))
|
||||
i += 1
|
||||
return entries
|
||||
0
tools/content_build/tests/__init__.py
Normal file
0
tools/content_build/tests/__init__.py
Normal file
61
tools/content_build/tests/test_parse.py
Normal file
61
tools/content_build/tests/test_parse.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from content_build.errors import BuildError
|
||||
from content_build.parse import parse_bible
|
||||
|
||||
BIBLE = """# A bible
|
||||
|
||||
Some prose that must be ignored.
|
||||
|
||||
```yaml
|
||||
id: town.a
|
||||
type: town
|
||||
status: canon
|
||||
secrecy: 0
|
||||
related: []
|
||||
body: >
|
||||
Town A.
|
||||
```
|
||||
|
||||
More prose.
|
||||
|
||||
```yaml
|
||||
id: rumor.x
|
||||
type: rumor
|
||||
status: canon
|
||||
secrecy: 1
|
||||
related: [town.a]
|
||||
body: >
|
||||
A rumor.
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
def _write(tmp_path: Path, text: str) -> Path:
|
||||
p = tmp_path / "bible.md"
|
||||
p.write_text(text)
|
||||
return p
|
||||
|
||||
|
||||
def test_extracts_yaml_blocks_and_ignores_prose(tmp_path):
|
||||
entries = parse_bible(_write(tmp_path, BIBLE))
|
||||
assert [e.data["id"] for e in entries] == ["town.a", "rumor.x"]
|
||||
|
||||
|
||||
def test_source_carries_filename_and_line(tmp_path):
|
||||
entries = parse_bible(_write(tmp_path, BIBLE))
|
||||
assert entries[0].source.startswith("bible.md:")
|
||||
|
||||
|
||||
def test_unterminated_block_raises(tmp_path):
|
||||
bad = "```yaml\nid: town.a\n" # no closing fence
|
||||
with pytest.raises(BuildError):
|
||||
parse_bible(_write(tmp_path, bad))
|
||||
|
||||
|
||||
def test_non_mapping_block_raises(tmp_path):
|
||||
bad = "```yaml\n- just\n- a\n- list\n```\n"
|
||||
with pytest.raises(BuildError):
|
||||
parse_bible(_write(tmp_path, bad))
|
||||
3
tools/requirements-dev.txt
Normal file
3
tools/requirements-dev.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# Build-tool dev/CI deps. NOT installed into the api proxy runtime.
|
||||
PyYAML==6.0.3
|
||||
pytest==8.3.2
|
||||
Reference in New Issue
Block a user