105 lines
3.9 KiB
Python
Executable File
105 lines
3.9 KiB
Python
Executable File
#!/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())
|