feat: 가상화 문서들 추가
This commit is contained in:
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""기록의 본문을 Studio 가 받는 형태로 바꿔 낸다.
|
||||
|
||||
저장소의 `.md` 는 사람이 읽는 파일이라 그림을 마크다운 이미지로 적는다. 그래야 편집기에서
|
||||
그대로 보인다. Studio 는 그 자리에 `:::evidence key="…"` 를 요구하고 상대 경로 이미지는
|
||||
`unsafe image URL` 로 거절한다. 두 형태를 한 파일에 담을 수 없으므로 **저장소는 읽는 형태로
|
||||
두고 Studio 로 보낼 때 이 스크립트가 바꾼다.**
|
||||
|
||||
python3 scripts/studio-body.py <기록.md> # 바꾼 전문을 표준출력으로
|
||||
python3 scripts/studio-body.py <기록.md> -o /tmp/x.md # 파일로 (check_body 용)
|
||||
python3 scripts/studio-body.py <기록.md> --body-only # 본문 구간만 (Studio 붙여넣기용)
|
||||
python3 scripts/studio-body.py <기록.md> --key a=a-1234 # 서버가 준 키로 바꿔서
|
||||
|
||||
`--key` 를 주지 않으면 frontmatter 의 `assets` key 를 그대로 쓴다. Studio 에 올린 뒤에는
|
||||
서버가 `<이름>-<해시8>` 을 주므로 그때 `--key` 로 짝지어 준다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
BODY = re.compile(r"(<!-- body:start -->)(.*?)(<!-- body:end -->)", re.S)
|
||||
IMAGE = re.compile(r"^!\[([^\]]*)\]\(([^)]+)\)$", re.M)
|
||||
|
||||
|
||||
def asset_keys(text: str) -> dict[str, str]:
|
||||
"""frontmatter 의 assets 를 상대경로 → key 로 읽는다."""
|
||||
head = text.split("\n---\n", 1)[0]
|
||||
keys = re.findall(r"^ - key: (\S+)$", head, re.M)
|
||||
files = re.findall(r"^ file: (\S+)$", head, re.M)
|
||||
return {f: k for k, f in zip(keys, files)}
|
||||
|
||||
|
||||
# 본문이 있는 종류는 둘뿐이다. 나머지 셋은 본문 구간이 없는 것이 정상이다
|
||||
BODY_KINDS = {"CASE", "CONCEPT"}
|
||||
|
||||
|
||||
def record_kind(text: str) -> str:
|
||||
"""frontmatter 의 kind. 없으면 빈 문자열."""
|
||||
head = text.split("\n---\n", 1)[0]
|
||||
m = re.search(r"^kind:\s*(\S+)", head, re.M)
|
||||
return m.group(1).upper() if m else ""
|
||||
|
||||
|
||||
def convert(text: str, overrides: dict[str, str]) -> tuple[str, list[str]]:
|
||||
by_path = asset_keys(text)
|
||||
missing: list[str] = []
|
||||
|
||||
def one(m: re.Match) -> str:
|
||||
alt, path = m.group(1), m.group(2)
|
||||
key = by_path.get(path)
|
||||
if key is None:
|
||||
missing.append(path)
|
||||
return m.group(0)
|
||||
key = overrides.get(key, key)
|
||||
return f':::evidence key="{key}" alt="{alt}" caption=" " zoom="true"\n:::'
|
||||
|
||||
m = BODY.search(text)
|
||||
if not m:
|
||||
if record_kind(text) in BODY_KINDS:
|
||||
return text, ["본문 구간(body:start ~ body:end)이 없다"]
|
||||
# Reference·Question·Decision 은 본문을 둘 자리가 없다. 결함이 아니다
|
||||
return text, []
|
||||
body = IMAGE.sub(one, m.group(2))
|
||||
return text[:m.start(2)] + body + text[m.end(2):], missing
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description="기록 본문을 Studio 형태로 바꾼다.")
|
||||
ap.add_argument("record")
|
||||
ap.add_argument("-o", "--out")
|
||||
ap.add_argument("--body-only", action="store_true")
|
||||
ap.add_argument("--key", action="append", default=[],
|
||||
help="frontmatter key=서버가 준 key. 여러 번 줄 수 있다")
|
||||
args = ap.parse_args()
|
||||
|
||||
overrides = {}
|
||||
for pair in args.key:
|
||||
if "=" not in pair:
|
||||
sys.exit(f"--key 는 a=b 형태다: {pair}")
|
||||
a, b = pair.split("=", 1)
|
||||
overrides[a] = b
|
||||
|
||||
text = open(args.record, encoding="utf-8").read()
|
||||
out, missing = convert(text, overrides)
|
||||
for path in missing:
|
||||
print(f"경고: frontmatter 의 assets 에 없다 — {path}", file=sys.stderr)
|
||||
|
||||
if args.body_only:
|
||||
m = BODY.search(out)
|
||||
out = m.group(2).strip("\n") if m else out
|
||||
|
||||
if args.out:
|
||||
open(args.out, "w", encoding="utf-8").write(out)
|
||||
print(args.out)
|
||||
else:
|
||||
sys.stdout.write(out)
|
||||
return 1 if missing else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user