#!/usr/bin/env python3 """Studio 편집기가 다시 쓰는 공백을 저장소에서 미리 잡는다. Studio 의 블록 편집기는 본문을 화면에 풀 때 두 가지를 바꾼다. 이어진 빈 줄 둘 -> 하나 닫는 코드펜스 뒤에 빈 줄이 없으면 -> 하나를 넣는다 저장된 값이 저장소와 같아도, 사람이 편집 화면을 열고 `저장` 을 누르는 순간 그 공백이 바뀌어 양쪽이 갈린다. 내용은 그대로인데 SHA 만 달라지므로 어느 쪽이 정본인지 알 수 없게 된다. 그래서 저장소를 미리 편집기 모양으로 맞춰 둔다. `check_body` 는 이것을 못 잡는다 — 공백이 달라도 파싱은 성립한다. python3 scripts/check-studio-whitespace.py # 전부 python3 scripts/check-studio-whitespace.py <프로젝트> # 한 프로젝트 python3 scripts/check-studio-whitespace.py --fix # 고친다 코드블록 안의 빈 줄과 frontmatter 는 건드리지 않는다. """ import glob import io import os import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def rendered(path): """Studio 로 보낼 모양. 저장소의 마크다운 이미지가 :::evidence 로 바뀐다.""" out = subprocess.run( ["python3", os.path.join(ROOT, "scripts", "studio-body.py"), path, "--body-only"], capture_output=True, text=True) return out.stdout if out.returncode == 0 else None def scan(lines): """(종류, 줄번호) 목록. 줄번호는 1부터.""" found = [] infence = False for i, line in enumerate(lines): if line.strip().startswith("```"): was = infence infence = not infence if was and i + 1 < len(lines): nxt = lines[i + 1].strip() if nxt and not nxt.startswith("```"): found.append(("닫는 펜스 뒤에 빈 줄이 없다", i + 1)) continue if infence: continue if line == "" and i + 1 < len(lines) and lines[i + 1] == "": found.append(("빈 줄이 둘 이어진다", i + 1)) return found def fix_file(path): src = io.open(path, encoding="utf-8").read() head, sep, rest = src.partition("\n---\n") # frontmatter 는 그대로 둔다 if not sep: return 0 lines = rest.split("\n") out, infence, changed = [], False, 0 for i, line in enumerate(lines): if line.strip().startswith("```"): was = infence infence = not infence out.append(line) if was and i + 1 < len(lines): nxt = lines[i + 1].strip() if nxt and not nxt.startswith("```"): out.append("") changed += 1 continue if infence: out.append(line) continue if line == "" and out and out[-1] == "": changed += 1 continue out.append(line) if changed: io.open(path, "w", encoding="utf-8").write(head + sep + "\n".join(out)) return changed def main(argv): do_fix = "--fix" in argv args = [a for a in argv if not a.startswith("--")] project = args[0] if args else "*" # 본문이 있는 종류만 본다. Reference·Question·Decision 의 칸은 평문으로 # 렌더링되므로 블록 편집기를 거치지 않고, 그래서 이 공백 규칙의 대상이 아니다. paths = [] for kind in ("case", "concept", "setup"): paths += glob.glob(os.path.join( ROOT, "docs", project, "tech-log-studio", "*", kind, "*.md")) paths = sorted(paths) skipped = len(glob.glob(os.path.join( ROOT, "docs", project, "tech-log-studio", "*", "*", "*.md"))) - len(paths) if not paths: print("STUDIO WHITESPACE: 대상이 성립하지 않는다 — " f"docs/{project} 에 Case·Concept·Setup 기록이 없다") return 2 unreadable, hits, fixed = [], [], 0 for path in paths: body = rendered(path) if body is None: unreadable.append(path) continue found = scan(body.split("\n")) if not found: continue if do_fix: fixed += fix_file(path) else: for kind, line in found: hits.append((path, line, kind)) for path, line, kind in hits: print(f" ! {os.path.relpath(path, ROOT)}:{line} {kind}") for path in unreadable: print(f" ? {os.path.relpath(path, ROOT)} studio-body.py 가 못 읽었다") seen = len(paths) - len(unreadable) if do_fix: print(f"STUDIO WHITESPACE: 고침 {fixed}곳 · 기록 {len(paths)} · 본 것 {seen}") return 0 verdict = "FAIL" if hits else "PASS" print(f"STUDIO WHITESPACE: {verdict} — 본문 있는 기록 {len(paths)} · 본 것 {seen} " f"· 어긋난 곳 {len(hits)} · 못 본 기록 {len(unreadable)} " f"· 본문 없는 종류라 안 본 기록 {skipped}") return 1 if hits else 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))