본문으로 이동

마크다운 반영과 배포

요약

thinkflow md는 마크다운 문서 안의 ```thinkflow 코드 블록을 SVG로 만들고, 블록 바로 아래에 그 그림을 보이는 이미지 줄을 넣거나 고친다. 같은 문서에 다시 돌려도 결과가 같고, --check는 문서와 SVG가 낡았는지만 알린다. --fold를 주면 그림을 먼저 보이고 원본 블록은 <details> 안에 접는다. 이 기능을 쓰기 위한 설치 경로(npm 패키지, 저장소 직접 실행, GitHub Action)와 배포 절차도 이 문서가 맡는다.

동기

문서 저장소는 그림 원본을 .thinkflow 파일로 따로 두고, 문서에는 이미지 줄을 손으로 넣는다. 원본과 문서가 어긋나도 아무도 모르고, 글 옆에서 그림을 고치려면 파일을 두 개 열어야 한다. 원본을 문서 안에 두면 글과 그림이 한 파일에서 같이 바뀐다. GitHub는 ```thinkflow 블록을 그림으로 그려 주지 않으므로 SVG를 저장소에 함께 두어야 하고, 그 SVG가 원본과 같은지 CI가 확인해야 한다.

예시

문서에 그림 넣기

  1. 문서에 이름 붙인 블록을 쓴다.
Plain text
```thinkflow name=flow
thinkflow
box doc "doc.md"
box cli "thinkflow md"
doc -> cli
```
  1. thinkflow md doc.md를 돌린다. 명령이 doc-flow.svg를 쓰고, 블록 아래에 이미지 줄을 넣는다.
Plain text
```thinkflow name=flow
...
```

![flow](doc-flow.svg)<!-- thinkflow -->
  1. 이름을 name=path로 바꾸고 다시 돌리면 이미지 줄이 doc-path.svg를 가리키고 옛 doc-flow.svg는 지워진다.

CI에서 낡은 그림 잡기

  1. 블록을 고치고 md를 돌리지 않은 채 올린다.
  2. thinkflow md doc.md --check가 doc-flow.svg: is out of date. Run thinkflow md to update it을 내고 종료 코드 1로 끝난다. 아무 파일도 쓰지 않는다.

원본 접기

  1. thinkflow md doc.md --fold를 돌린다. 그림이 앞에 오고 원본 블록이 <details> 안으로 들어간다. 감싸는 줄은 이 도구의 표식으로 구분한다.
Plain text
<!-- thinkflow fold v1 name=flow -->
![flow](doc-flow.svg)<!-- thinkflow -->

<details>
<summary>그림 원본</summary>

```thinkflow name=flow
...
```

</details>
<!-- /thinkflow fold v1 name=flow -->
  1. 옵션 없이 thinkflow md doc.md를 다시 돌리면 접힌 채로 그림만 갱신한다. --unfold를 돌리면 표식과 <details>를 걷고 블록 아래에 그림을 두는 처음 모양으로 돌아온다.

이 문서의 아래 그림은 이 문서의 ```thinkflow 블록을 thinkflow md로 만든 것이다.

상세 설계

명령

Plain text
thinkflow md <file.md ...> [--check] [--out-dir dir] [--fold [--fold-title "text"] | --unfold] [--static] [--scene n|이름] [--strict] [--budget 이름=값 ...] [--json]
옵션 뜻
--check 쓰지 않고 갱신이 필요한지만 알린다. 필요하면 종료 코드 1
--out-dir dir SVG를 dir에 쓴다. 기본은 문서와 같은 폴더. dir이 파일이거나 경로 중간이 파일이면 아무것도 쓰기 전에 {dir}: cannot be used as --out-dir: … 진단 code io 한 줄과 종료 1이다(--check도 같다)
--fold 그림을 먼저 두고 원본 블록을 <details>로 접는다. 자세한 규칙은 원본 접기 절
--fold-title "text" --fold가 새로 만드는 <summary> 글. 기본은 그림 원본. --fold와만 쓴다
--unfold 이 도구가 만든 접기만 걷는다. --fold와 함께 쓰면 인자 오류
--static 멈춘 SVG를 쓴다
-- 이 뒤의 인자는 모두 문서 이름이다. -로 시작하는 이름의 문서에 쓴다(Action이 문서 목록을 넘길 때 쓴다). 값을 받는 옵션(--out-dir, --fold-title, --scene, --budget)의 값이 비어 있거나, --budget 이외의 값을 받는 옵션을 두 번 쓰거나, 문서 이름이 빈 글이면 인자 오류다
--scene n|이름 SVG로 만들 장면을 고른다(번호는 1부터, render와 같다). 생략하면 첫 장면이고 장면이 없는 블록은 정지 그림 하나다. 장면 없는 블록에 주면 그 블록 줄에 오류를 내고(종료 1) 아무것도 쓰지 않는다
--strict, --budget, --json render와 같은 뜻. 블록마다 적용한다. 예산은 블록 하나의 그림마다 검사한다

render의 --out, --html과 gallery의 --title은 이 명령이 받지 않고, --check, --out-dir, --fold, --fold-title, --unfold는 다른 명령이 받지 않는다. 기존 명령의 동작은 바뀌지 않는다(명령 추가만).

블록

  • 대상은 설명 글자의 첫 낱말이 thinkflow인 울타리 블록이다. 백틱과 물결표 울타리, 목록 안 들여쓴 울타리를 읽는다. 울타리를 여는 줄은 앞 칸(인용 표시, 목록 항목)을 뗀 뒤 3칸까지만 들여쓸 수 있고, 닫는 울타리도 같은 칸을 뗀 뒤 3칸까지다. 여는 울타리 자신의 들여쓰기는 떼지 않으므로 2칸 들여 연 울타리를 5칸이나 탭으로 들인 줄은 닫지 못한다. 4칸 이상 들여쓴 줄은 들여쓴 코드이거나 울타리 안 글이라 닫는 울타리가 아니다.
  • 마크다운이 코드나 원문으로 읽는 곳은 읽지도 고치지도 않는다. 들여쓴 코드(4칸 이상, 문단 중간은 제외), HTML 블록과 HTML 주석 안(<!--부터 -->가 있는 줄까지, <div>부터 빈 줄까지) 글은 울타리로 읽지 않고, 그 안의 <!-- thinkflow --> 이미지 줄도 지우지 않으며 접기 표식도 보지 않는다. 블록 찾기와 <details> 세기는 같은 걸음(md-tags.js)을 쓰므로 두 규칙이 갈라지지 않는다.
  • 인용(>) 안 울타리는 --fold나 --unfold를 준 실행이거나, 그 블록에 이미 이 도구의 표식(접기 표식 또는 <!-- thinkflow --> 이미지 줄)이 있을 때만 읽는다. 표식은 사용자가 옵션으로 처리를 골랐다는 근거다. 그 밖의 옵션 없는 실행은 인용 안 블록을 예전처럼 읽지 않고, 인용 안 이미지 줄도 지우지 않으며, 오류도 내지 않는다. 그런 문서에 옵션 없이 돌린 결과는 인용 안 블록 지원이 없던 판과 바이트 단위로 같다. 읽는 인용 안 울타리는 인용이 끝나기 전에 닫혀야 하고, 인용 표시 없는 줄을 만나면 닫힌 것으로 읽지 않고 그 줄 번호로 오류를 낸다. 다른 울타리(text 등) 안에 든 ```thinkflow 줄은 블록이 아니다.
  • 설명 글자의 선택 항목은 name=<이름> 하나다. 이름은 소문자, 숫자, -다. 다른 항목이나 형식이 틀린 이름은 오류다.
  • 블록 안 글은 .thinkflow 원본이다. data, icons 경로는 문서가 있는 폴더 기준이다.
  • 오류 줄 번호는 문서 안 줄 번호다.

이미지 줄

  • 블록의 닫는 울타리 아래에 빈 줄 하나를 두고 ![대체 글](주소)<!-- thinkflow --> 한 줄을 둔다. 끝의 <!-- thinkflow -->가 이 도구가 만든 줄이라는 표시다. 표시가 있는 줄만 고치거나 지운다. 표시 없는 이미지 줄은 건드리지 않는다.
  • 대체 글은 블록의 title이다. title이 없으면 이름, 이름도 없으면 {문서 이름} figure {순번}이다. 줄 바꿈과 대괄호는 이미지 문법을 깨므로 바꾼다.
  • 주소는 실제 문서 폴더 기준 상대 경로이고 구분자는 /다. 문서와 SVG 폴더는 소유 판정과 같이 심볼릭 링크를 푼 실제 위치로 재므로, 링크 폴더로 열어도 링크가 실제 문서에서 풀린다. 이미지 문법을 깨는 글자는 퍼센트 인코딩한다. 상대 경로로 이을 수 없는 위치(Windows의 다른 드라이브)에 쓰라고 하면 아무것도 쓰기 전에 그 블록 줄 오류를 낸다. 이 분기는 Node의 경로 규칙에서 읽은 것이고 Windows에서 실행해 확인하지 않았다(검증 요구사항, 미완료).
  • 이미지 줄 뒤에 글이 바로 이어지면 빈 줄을 하나 더 둔다.
  • 손대지 않는 줄은 줄마다 원래 줄바꿈(LF 또는 CRLF)을 그대로 두고, 새로 쓰는 줄은 문서에 CRLF가 하나라도 있으면 CRLF, 아니면 LF를 쓴다. 줄바꿈이 섞인 문서도 블록 본문의 바이트는 그대로다.
  • 인용 안 블록의 이미지 줄은 인용 표시를 붙이고(> ![…](…)<!-- thinkflow -->), 사이 빈 줄은 >다.
  • 블록이 없어졌는데 남은 표시 있는 이미지 줄(울타리, 들여쓴 코드, HTML 블록 밖)은 지운다.

원본 접기

--fold는 블록마다 아래 배치로 바꾼다. 앞머리는 블록의 여는 울타리 줄과 같은 인용 표시와 들여쓰기이고, 아래는 앞머리를 뗀 모양이다.

Plain text
<!-- thinkflow fold v1 {식별자} -->
![대체 글](주소)<!-- thinkflow -->

<details>
<summary>그림 원본</summary>

(원본 블록. 울타리 줄과 본문은 한 글자도 바꾸지 않는다)

</details>
<!-- /thinkflow fold v1 {식별자} -->
  • 표식은 <!-- thinkflow fold v1 {식별자} -->(시작)와 <!-- /thinkflow fold v1 {식별자} -->(끝)이다. v1은 표식의 판 번호다. 식별자는 이름 있는 블록이면 name=이름, 이름 없는 블록이면 이름 없는 블록의 순번으로 n=순번이다. SVG 이름을 정하는 순번과 같다. HTML 주석은 문단 중간에서도 새 블록을 시작하므로 시작 표식 앞에는 빈 줄을 넣지 않는다. 앞 글의 줄을 옮기거나 지우지 않으려는 것이다. 끝 표식 뒤에 글이 바로 이어지면 빈 줄을 하나 둔다. </details>가 연 HTML 블록이 빈 줄에서 끝나므로, 없으면 뒤따르는 글이 마크다운으로 읽히지 않는다.
  • 기본 <summary> 글은 그림 원본이다. --fold-title의 글은 &, <, >, ", '를 문자 참조로 바꿔 쓰므로 어떤 글이든 태그로 읽히지 않는다. 한 줄이어야 하고 공백뿐이면 인자 오류다. 새로 만드는 감싸기에만 기본 글이 들어가고, 이미 있는 감싸기는 --fold-title을 줄 때만 글이 바뀌며 생략하면 쓰인 글을 그대로 둔다.
  • 접기 선택은 옵션으로 한다. 옵션이 없으면 문서의 접힘 상태를 지킨다. 새 블록은 접지 않은 배치로 만들고, 접힌 블록은 접힌 채 그림만 갱신한다. --fold는 아직 접지 않은 블록을 접고 이미 접힌 블록은 그림만 갱신한다. --unfold는 접힌 블록을 풀어 블록 아래에 (빈 줄) 그림을 두는 배치로 돌린다. 세 실행 모두 같은 입력에 두 번째는 바꾸지 않는다. 접은 문서를 풀면 접기 전에 이 명령이 한 번 돌린 문서와 바이트 단위로 같다.
  • 이 도구가 만든 감싸기는 표식 두 줄과 그 사이 줄(그림, 빈 줄, <details>, <summary>, 빈 줄, 블록, 빈 줄, </details>)이 앞머리까지 정확히 맞을 때만 읽는다. 두 표식의 판 번호와 식별자가 같아야 한다. 식별자가 지금 블록의 것과 다르면(이름을 바꿨거나 앞에 이름 없는 블록이 끼었다) 온전한 감싸기로 읽고 표식의 식별자만 고친다.
  • 풀 때는 표식, <details>, <summary>, </details>와 그 사이 빈 줄만 걷는다. 원본 블록, 사용자가 쓴 이미지와 문단은 건드리지 않는다.
  • 사용자가 쓴 <details>(표식이 없는 것)는 어떤 옵션도 고치거나 풀지 않고 summary도 그대로 둔다. 블록이 사용자 <details> 안에 있으면 --fold도 한 번 더 감싸지 않고 접지 않은 배치(블록 아래 그림)로 그 자리에서 그림만 갱신한다. 문서에서 <details>와 </details>를 세어 블록 앞에 열린 것이 있으면 사용자 <details> 안으로 본다. 이 도구의 감싸기 태그는 세지 않는다.
  • 태그는 실제 HTML 태그일 때만 센다. 줄 단위로 CommonMark의 블록 구조를 따라가며 아래 문맥 안의 글자는 태그로 세지 않는다.
    • 울타리 코드와 들여쓴 코드(앞 공백 4칸 이상, 문단 중간은 제외)
    • 코드 span. 여는 백틱 묶음과 길이가 같은 묶음이 닫는다. 같은 문단 안에 짝이 없으면 백틱은 글자다. 줄을 넘는 span도 하나로 읽는다.
    • 여러 줄에 걸친 HTML 주석. 문단 안 주석은 같은 문단에서 닫혀야 주석이고, 줄 시작의 주석은 -->까지 이어진다.
    • 백슬래시 이스케이프(\<details>)와 문자 참조(&lt;details&gt;)
  • HTML 블록(줄 시작이 <details>, <div> 같은 블록 태그인 줄부터 빈 줄까지)은 원문이다. 코드 span과 이스케이프가 없고 안의 태그는 브라우저가 읽는 대로 센다. 그래서 <details> 다음 줄의 `</details>`는 닫는 태그다.
  • 인용(>)과 목록 항목은 칸이다. 칸 안에서 연 <details>는 그 칸이 끝나면 닫힌 것으로 보고, 다른 칸의 태그는 블록의 부모가 아니다. 바깥에서 연 <details>는 안쪽 칸의 블록에도 부모다. 인용 표시 없이 이어지는 문단 줄(lazy continuation)은 인용 안으로 본다. 지원하지 않는 것은 링크 참조 정의, 표 셀 경계, <script> 같은 원문 요소 안 태그다.
  • 구조를 확정할 수 없으면 추측해 고치지 않고 줄 번호와 이유를 알리고(진단 code md-fold) 아무 파일도 쓰지 않는다. 표식 한 줄만 남았거나, 감싸기 안에 사용자 줄이 끼었거나, 두 표식의 식별자나 판 번호가 다르거나, 그림 줄이 지워진 경우가 그렇다. 사용자 줄은 지우지 않으므로 직접 고치거나 지운다. --fold가 아직 접지 않은 블록 앞에서 짝 없는 </details>를 만나 사용자 <details> 안인지 알 수 없을 때도 같고, 메시지에 그 </details>의 줄 번호를 담는다. 인용 안에서 연 <details>를 인용 밖에서 닫는 것도 짝이 없는 것이다. 다른 울타리(text 등) 안의 표식 예시는 표식이 아니다.
  • 목록과 인용 안 블록은 감싸는 줄도 같은 앞머리를 쓴다. 인용 안 빈 줄은 >다.
  • --check와 쓰기는 같은 계획으로 문서 글을 만들고, --check는 만든 글이 디스크와 다르면 갱신 필요로 센다. 그림 SVG, 문서, 낡은 SVG를 쓰고 지우는 길은 아래 쓰기 순서와 실패 절 하나뿐이다. 접기가 따로 파일을 쓰지 않는다.

SVG 이름과 위치

  • 이름은 {문서 이름}-{블록 이름}.svg다. 문서 이름은 확장자를 뺀 파일 이름이다. 이름 없는 블록은 {문서 이름}-{순번}.svg이고, 순번은 이름 없는 블록만 세어 1부터다. 이름 있는 블록은 문서 안에서 순서가 바뀌거나 앞에 블록이 늘어도 같은 이름이다.
  • 위치는 문서 옆이고 --out-dir로 바꾼다.
  • 한 실행 안에서 두 블록이 같은 파일을 쓰려 하면 오류다(같은 이름, 같은 문서 이름 두 개). 겹침은 대소문자를 구분하지 않고 비교한다. 폴더는 실제 경로로 풀고 파일 이름은 NFC로 맞춘 뒤 소문자로 낮춰 비교하므로, Doc.md와 doc.md처럼 이름만 다른 두 출력도 같은 파일로 친다. 대소문자를 구분하는 파일 시스템에서도 결과가 같다.
  • 만든 SVG의 둘째 줄에는 <!-- thinkflow md v2 {문서 경로} --> 표시가 들어 있다. v2는 표시의 판 번호다. 문서 경로는 SVG 폴더에서 문서까지의 상대 경로(구분자 /)이고, 문서와 SVG 폴더를 모두 심볼릭 링크를 푼 실제 경로로 바꾼 뒤 잰다. 링크 별칭으로 연 같은 문서는 같은 소유이고 실행 위치에도 영향받지 않는다. 같은 SVG 폴더를 쓰는 서로 다른 문서는 경로가 늘 달라 소유가 갈린다. 문서가 SVG 폴더 안에 있으면(기본 위치) 파일 이름만이다.
  • 문서 경로는 되돌릴 수 있게 적는다. %는 %25로, 줄바꿈은 %0A와 %0D로 쓰고, 그다음 XML 주석에 --가 들 수 없으므로 -가 이어지면 앞쪽 -를 %2D로 쓴다. 그래서 x--y는 x%2D-y, 폴더 이름이 정말 x%2D-y인 문서는 x%252D-y로 적혀 두 표시가 다르다. %, 줄바꿈, 연속 -가 없는 경로는 글자가 그대로다.
  • 쓸 SVG가 이미 있으면 앞 세 줄의 표시로 소유자를 확인한다. thinkflow md v2 표시의 글이 이 문서의 표시 글과 같을 때만 이 문서 것이고, 이 문서 것이면 덮어쓴다. 다른 문서의 표시이거나, thinkflow md v2 표시가 없는 파일(손으로 만든 그림, 판 번호 없는 표시나 다른 이름의 표시가 붙은 파일 포함)이면 그 블록 줄에 오류를 내고 이번 실행의 어떤 SVG와 문서도 쓰지 않는다. 오류 문장은 블록에 다른 name=을 주거나 이 문서를 다른 --out-dir에 쓰라고 안내한다. --check도 같은 충돌을 오류로 알린다. 한 문서가 놓은 이름을 다른 문서가 이어받으려면 놓는 문서를 먼저 반영한다(같은 실행이어도 쓰는 문서는 오류다).
  • 소유와 충돌 검사는 파일을 하나라도 바꾸기 전에 모든 문서에 대해 끝낸다. 충돌 오류로 끝난 실행은 SVG와 문서를 바이트 단위로 그대로 둔다.
  • 쓸 그림 파일이 이미 있는 심볼릭 링크이면 출력 이름의 충돌은 이름이 아니라 링크가 가리키는 실제 파일로 비교한다(render와 같은 한 함수). 이름이 다른 그림 링크 둘이 같은 파일을 가리키면 같은 출력이라 쓰기 전에 충돌 오류이고(--check도 같다) 링크 둘과 가리키는 파일은 그대로다. 가리키는 곳이 없는 깨진 링크 자리는 파일로 바꾸지 않고 io 진단 한 줄과 종료 1로 거절하며 링크는 그대로다.
  • 고치는 문서 파일도 그림 파일과 같은 출력 등록(같은 실제 파일 키)에 든다. 문서와 그 문서로 이어진 링크를 함께 주면(md a.md b.md에서 b.md가 a.md의 링크) 같은 문서를 두 번 쓰는 것이라 쓰기 전에 … would be written twice: for … and for … 오류(진단 code output-collision, render와 gallery가 같은 함수로 낸다)와 종료 1이고(--check도 같다) 아무 파일도 바뀌지 않는다. 링크 하나만 주면 링크는 그대로 두고 가리키는 실제 문서를 고친다. 깨진 링크 문서는 읽을 수 없는 문서로 io 진단이다.
  • 낡은 SVG를 지울 때도 같은 키로 비교한다. 이 문서가 이미 소유한 SVG의 이름이 이번 실행이 쓴 이름과 대소문자나 유니코드 정규형만 다르면 같은 출력이라 낡은 파일로 지우지 않는다. 대소문자를 접는 파일 시스템에서는 둘이 같은 파일이라 방금 쓴 파일을 지우는 일을 막는다. 대소문자를 구분하는 파일 시스템에서도 같은 키로 세므로 옛 이름의 SVG는 지워지지 않고 남는다(데이터를 잃지 않는 쪽의 선택이다). 블록 이름은 소문자·숫자·-만 받으므로 대문자 이름은 손으로 옮긴 파일이나 이름 검사가 생기기 전 실행 결과에만 있다. 이 시험은 대소문자를 구분하는 파일 시스템(CI의 ubuntu)에서는 건너뛴다.

오래된 SVG 정리

실행이 끝나면 SVG 폴더에서 {문서 이름}-로 시작하는 .svg 중 이 문서 것으로 판정되고 이번 실행이 쓰지 않는 파일을 지운다. 쓰기 전 확인과 같은 소유 판정을 쓴다. 소유는 표시의 문서 경로로만 판정하므로, 다른 폴더의 같은 이름 문서(a/readme.md, b/readme.md)가 같은 --out-dir을 써도 서로의 SVG를 지우지 않는다. 표시가 없는 파일(손으로 만든 그림)과 이름이 비슷한 다른 문서의 SVG도 지우지 않는다.

  • 한 번에 여러 문서를 넘기면 문서마다 낡은 SVG를 구한 뒤 합치고, 어느 문서든 이번 실행에 쓰는 파일은 지우는 목록에서 뺀다. 어떤 파일도 한 실행의 쓰기 대상과 삭제 대상에 함께 들지 않는다. 입력 순서는 결과에 영향이 없다.
  • 소유 표시는 thinkflow md v2 {경로} 하나다. 판 번호가 없는 표시(thinkflow md {경로})나 다른 이름의 표시가 붙은 파일은 표시가 없는 파일과 같아서 사용자 파일이다. 이 도구는 그런 파일을 가져가지도 덮어쓰지도 지우지도 않고, 같은 이름으로 쓰려 하면 충돌 오류를 낸다. 지우거나 이름을 바꾼 뒤 다시 반영한다. 이미지 줄도 같다. <!-- thinkflow -->가 붙은 줄만 이 도구가 만든 줄이고, 다른 표시가 붙은 줄은 문서의 본문이다.
  • --check는 지우지 않고 지울 파일이 있으면 갱신 필요로 센다.

동시 실행

쓰는 실행은 문서의 실제 폴더와 출력 폴더마다 잠금 파일 .thinkflow-md.lock을 배타적으로 만들어(wx) 잡는다. 파일에는 JSON 한 덩어리로 pid, 호스트 이름, 만든 시각, 무작위 nonce를 적는다. nonce가 그 잠금의 신원이다. 소유 검사부터 쓰기와 지우기까지 잠금을 쥐고, 성공과 실패 모두 끝나면 푼다. 풀 때는 파일의 nonce가 자기 것일 때만 지우므로 다른 프로세스의 잠금은 건드리지 않는다. 이 실행이 만든 빈 출력 폴더는 치운다.

  • 같은 출력 폴더를 다른 프로세스가 쥐고 있으면 파일을 하나도 바꾸기 전에 오류(진단 code md-locked)로 끝나고 종료 코드는 1이다. 기다리지 않는다. 진단은 {폴더}/.thinkflow-md.lock: another thinkflow md is writing to this folder (pid N on host H, created T). If no thinkflow md is running, delete this file by hand and run again 모양이다. 쥔 쪽이 끝난 뒤 다시 돌린다.
  • 낡은 잠금은 주인이 같은 호스트에 있고 process.kill(pid, 0)이 ESRCH로 확실히 없다고 답할 때만 자동으로 치운다. 강제 종료(SIGKILL)로 남은 잠금이 이 경우다.
  • 그 밖의 잠금은 지우지 않고 파일 경로, 주인 정보(pid, 호스트, 만든 시각), 직접 지우는 방법을 담은 같은 형식의 진단으로 끝낸다. 주인이 살아 있는 경우, 권한이 없어 확인하지 못한 경우(EPERM), 다른 호스트의 잠금, 읽을 수 없거나 형식이 틀린 잠금이 해당한다. 낡은 잠금의 pid가 다른 프로세스에 다시 쓰였으면 살아 있는 것으로 보여 지우지 않는다. 지우지 않는 쪽이 안전하므로 이 경우는 직접 지운다.
  • 잠금 획득은 .thinkflow-md.lock.guard 폴더를 배타적으로 만드는 것으로 시작한다. 이 guard를 쥔 실행만 기존 잠금을 읽고, 낡은 잠금을 고유 이름으로 옮겨 nonce를 확인해 치우고, wx로 새 잠금을 만든다. 획득이 끝나면 guard를 지우며 문서를 만드는 동안에는 잠금 파일만 유지한다. guard가 이미 있으면 기다리지 않고 md-locked로 끝난다. 획득 도중 강제 종료되어 guard가 남았다면 실행 중인 thinkflow md가 없는 것을 확인하고 guard 폴더를 직접 지운 뒤 다시 실행한다. guard가 없는 일반 잠금은 기존의 자동 stale 복구를 따른다. 폴더나 guard를 만들고, guard를 지우거나 잠금 파일을 획득하는 파일 시스템 오류는 io로 끝난다.
  • --check는 쓰지 않으므로 잠그지 않는다.
  • 지원 범위는 한 호스트 안의 로컬 파일 시스템에서 같은 출력 폴더(링크를 푼 실제 경로)를 쓰는 thinkflow md 프로세스끼리다. 네트워크 파일 시스템과 여러 호스트가 한 폴더를 나눠 쓰는 경우는 보장하지 않고, 분산 잠금은 만들지 않는다. 다른 호스트의 잠금은 자동으로 치우지 않고 진단만 낸다. 문서 폴더도 잠그므로 서로 다른 --out-dir를 쓰는 두 실행도 같은 문서를 동시에 고치지 못한다. 심볼릭 링크로 같은 문서를 가리키면 실제 문서 폴더에서 같은 잠금을 쓴다.

오류와 종료 코드

상황 종료 코드 파일
갱신 없음 또는 갱신 완료 0 바뀐 파일만 쓴다(SVG가 먼저, 문서가 마지막). 쓴 경로를 stdout에 한 줄씩 알리고, 지운 파일은 removed {경로}로 알린다
--check에서 갱신 필요 1 쓰지 않는다. 낡은 파일마다 stderr에 {경로}: is out of date. Run thinkflow md to update it 또는 is a stale figure를 알린다
어느 문서의 블록이든 오류(원본 오류, --strict 경고, 울타리 형식, 이름 겹침, 다른 문서나 표시 없는 파일과의 SVG 충돌, 읽기 실패) 1 모든 문서를 먼저 만들고 하나라도 오류면 아무 파일도 쓰거나 지우지 않는다(그림 검사의 gallery와 같은 계약)
같은 출력 폴더를 다른 프로세스가 쓰는 중 1 아무 파일도 바꾸지 않는다. 진단 code md-locked
--out-dir이 파일이거나 경로 중간이 파일 1 아무 파일도 쓰거나 잠그지 않는다. 진단 code io, {dir}: cannot be used as --out-dir: … 한 줄
쓰기나 지우기 실패(권한, 디스크 가득 참 등) 1 아래 쓰기 순서와 실패 절. 진단 code io, 메시지 {경로}: cannot write the file: {코드}(지우기는 cannot remove the file)
인자 오류 2 없음

쓰기 순서와 실패

만들기가 모두 끝나 오류가 없을 때만 쓴다. 쓰는 순서와 실패 때 보장은 다음과 같다.

  1. 쓸 파일(SVG와 문서)마다 같은 폴더에 임시 이름(.{이름}.{프로세스 번호}.{순번}.tmp)으로 먼저 놓는다. 하나라도 실패하면 임시 파일을 지우고 아무 대상도 바꾸지 않는다. 출력 폴더만 읽기 전용인 경우가 여기서 걸러진다.
  2. 임시 파일이 모두 놓인 뒤 rename으로 제자리에 바꾼다. SVG를 모두 먼저 바꾸고 문서를 마지막에 바꾼다. 그래서 중간에 멈춰도 문서가 아직 없는 SVG를 가리키는 일이 없다.
  3. 바꾸는 도중 하나가 실패하면 이미 바꾼 파일을 되돌린다. 새로 만든 SVG는 지우고, 덮어쓴 파일은 바꾸기 전에 읽어 둔 내용을 같은 방식(임시 파일, rename)으로 다시 놓는다. 되돌리지 못한 파일은 could not be restored after a failed write로 따로 알린다.
  4. 낡은 SVG 지우기는 모든 쓰기가 끝난 뒤 마지막이다. 지우기가 실패하면 문서와 새 SVG는 새 상태로 일관되고 낡은 SVG만 남으며, 종료 1과 cannot remove the file을 알린다. 다시 돌리면 지운다.

파일 하나의 rename은 원자적이지만 여러 파일 전체의 바꾸기는 트랜잭션이 아니다. 프로세스가 강제로 끝나거나 전원이 나가면 되돌리기가 돌지 못해 새 SVG만 먼저 놓인 상태가 남을 수 있다. 이때 문서는 옛 상태라 링크는 깨지지 않고, 다시 돌리면 맞춰진다. 이미 있는 문서의 모드와 심볼릭 링크는 유지한다(링크가 가리키는 실제 파일을 바꾼다).

같은 입력에 다시 돌리면 쓸 파일이 없다. SVG가 바이트 단위로 같게 나오는 것은 배치의 결정성 요구사항이 지킨다.

GitHub에서 보이는 모양

GitHub 마크다운은 ```thinkflow 블록을 코드로 보이고 아래 이미지를 그림으로 보인다. --fold로 접은 문서는 그림이 접기 밖에 보이고, 원본 블록은 <summary>를 눌러(키보드는 Tab으로 옮기고 Enter 또는 Space) 열고 닫는다. 열린 블록의 글은 선택해 복사할 수 있다. <details> 안 울타리는 앞뒤에 빈 줄이 있으면 코드 블록으로 렌더되므로 감싸는 줄이 그 빈 줄을 늘 둔다. GitHub가 이 문서를 실제로 어떻게 렌더하는지는 자동 시험이 확인하지 않는다(검증 요구사항, 미완료).

설치와 Action

경로 명령 상태
npm 설치 npm install --save-dev thinkflow 공개 npm 패키지
설치 후 실행 npx thinkflow md doc.md 잠금 파일에 고정된 버전
GitHub Action uses: woonyong-choi/thinkflow@main 지금

설치한 프로젝트의 잠금 파일을 함께 커밋하고 npm ci로 같은 버전을 설치한다.

저장소 루트 action.yml은 composite Action이다. 의존 패키지를 Action 폴더에 설치한 뒤 입력에 맞춰 명령을 돌린다.

입력 기본값 뜻
paths **/*.thinkflow **/*.md 공백으로 나눈 git 글롭 목록. 추적 중인 파일만 대상이다. 다른 확장자 파일은 모으지 않는다. 글롭이 .thinkflow나 .md와 맞는 파일이 하나도 없으면 no tracked .thinkflow or .md file matches 오류로 실패한다
mode check check는 .thinkflow에 check, .md에 md --check를 돈다. render는 render와 md를 돌려 파일을 쓴다
strict false true면 경고도 실패다
fold keep .md에만 적용하고 두 모드 모두에 쓴다. keep은 옵션 없이 돌리고, fold는 --fold, unfold는 --unfold를 넘긴다. 그 밖의 값은 실패다
fold-title 빈 값 fold: fold일 때 --fold-title로 넘기는 <summary> 글. 다른 fold 값과 쓰면 실패다
budget 빈 값 생성 예산을 올리는 이름=값 목록(공백이나 쉼표로 나눈다). 예: grid-elements=2000000. --budget과 같은 해석이라 이름이 없거나 값이 양의 안전한 정수가 아니면 그림을 만들기 전에 실패한다. 이름과 기본 한도는 칸 격자

경로는 git ls-files -z의 NUL 구분 출력으로 모으고 명령에는 -- 뒤에 넘기므로 -로 시작하는 이름도 옵션으로 읽히지 않는다. 추적 중인 심볼릭 링크는 가리키는 문서와 같은 출력이라 함께 넘기면 거절되므로 모으지 않는다(가리키는 문서가 추적 중이면 그 문서가 처리된다). 줄 단위 출력은 Git이 한글 이름을 따옴표와 escape로 감싸(core.quotepath 기본값) 확장자 분류에서 빠지기 때문이다. 한글, 공백, 줄바꿈이 든 이름도 같게 검사하고, 여러 글롭이 고른 같은 파일은 한 번만 검사한다. Action이 모으는 것은 글롭이 고른 .thinkflow와 .md 파일뿐이다. 글롭이 다른 확장자 파일을 골라도 그 파일은 읽지 않고 모으지도 않으며, 이름이나 확장자가 특이한 .thinkflow와 .md는 빠지지 않고 모은다. 대상 파일이 하나도 없으면 실패한다. 빈 글롭이 조용히 통과하는 일을 막기 위해서다. render 모드는 파일을 쓸 뿐 커밋하지 않는다.

배포

.github/workflows/release.yml은 v* 태그에서 돈다.

  1. 태그가 v{package.json version}과 다르면 실패한다.
  2. secrets.NPM_TOKEN이 없으면 해당 메시지로 실패한다. 현재 인증 방식은 대상 패키지에 쓰기 권한과 bypass 2FA를 가진 granular access token이다. 토큰을 금지한 패키지 설정에서는 이 방식으로 배포할 수 없다. 인증 조건은 npm 공식 안내를 따른다.
  3. npm ci로 설치하고 npm test, npm run check, npm run check:sources(예제와 문서 그림, showcase 원본 --strict), npm run check:figures(모든 마크다운 문서의 md --check --strict와 showcase SVG 낡음 검사)를 돈다. 이어서 npm run check:package로 실제 배포물의 독립 설치를 확인한다. npm test는 브라우저를 열지 않는 공개 계약 시험이라 브라우저 설치가 필요 없다. 워크플로의 실행 여부와 날짜는 GitHub Actions 기록과 이슈가 맡고 이 문서는 설정과 요구만 적는다.
  4. npm publish --provenance --access public을 한다. 버전에 -가 있으면 --tag next를 붙인다.
  5. gh release create로 GitHub Release를 만든다. 버전에 -가 있으면 사전 배포 표시를 한다.

패키지

package.json의 files는 src, 로고 PNG(docs/assets/thinkflow-logo*.png), LICENSE, NOTICE다. README와 package.json은 npm이 늘 넣는다. 글꼴은 의존 패키지(pretendard, @expo-google-fonts/*, jetbrains-mono)로 설치된다. 내장 아이콘·공통 스타일·그 라이선스는 peer 의존성인 @woonyong-choi/design-tokens 패키지에 있고 src/icons는 공통 카탈로그 읽기와 사용자 SVG 검증을 맡는다. 시험, 문서, 예제, 스크립트는 올라가지 않는다. 실행 파일은 bin의 thinkflow 하나다. 라이브러리는 buildFigure, toSvg, toHtml, FigureError를 내보낸다. Node.js ESM API의 입력 옵션과 진단 형식은 공개 선언이 정본이고 사용법에 호출 예시가 있다. 빌드 결과는 경고 목록만 공개하며 내부 모형을 변경하거나 직렬화하는 API는 제공하지 않는다.

npm run check:package -- /absolute/new-consumer-path는 저장소 밖의 새 소비 환경을 만들고 실제 npm pack 산출물을 설치한다. 경로가 이미 있으면 거절하며 자신이 만든 경로는 성공·실패 뒤 모두 지운다. npm 공개 레지스트리에 접근해야 한다. 사용자 npm 설정과 인증 환경을 제외하고 공개 런타임 의존성과 peer 의존성인 디자인(개발 의존성에 고정한 commit)만 설치한다. 검사 입력은 기존 flow 예제이고 CLI와 공개 API가 같은 SVG·HTML을 만드는지, Markdown 반영과 내장 디자인·글꼴을 확인한다. TypeScript 소비자는 설치된 선언을 NodeNext로 컴파일한 뒤 실행하고, 잘못된 옵션과 임의 객체를 빌드 결과로 전달하는 호출은 컴파일에서 거부하는지 확인한다. 컴파일러는 개발 의존성으로만 두고 소비 환경에는 설치하지 않는다. 실제 npm 발행과 사용자 인증 권한은 이 검사에 포함하지 않는다.

README 로고의 출처

밝은 PNG와 어두운 PNG는 README와 생성 HTML 탭 아이콘이 공유하는 원본이다. 두 파일을 패키지에 포함하고 재생기·목록 HTML에 data URL로 내장한다. 로고를 갱신하면 기존 HTML을 다시 생성하고 실행 중인 렌더링 프로세스를 재시작한다. 두 PNG의 출처·라이선스·해시는 docs/assets/sources.json에 있다. 저장소의 자체 래스터 그림은 이 기록이 있어야 하고, 출처가 기록되지 않은 그림이나 Things 웹사이트(culturedcode.com)의 파일·파일 이름 형식은 두지 않는다. test/asset-provenance.test.js가 npm test에서 이를 검사하며 공통 디자인 패키지(@woonyong-choi/design-tokens)의 출처는 design-tokens가 검사한다.

밝은 PNG의 최초 저장소 반영은 29254b5다. 현재 밝은 PNG와 어두운 PNG는 1906d00에 함께 반영된 파일이다. 이 Git 이력은 저장소에 들어온 파일을 식별하는 근거이며 원본 제작자나 참고 자산의 권리를 증명하지 않는다.

두 PNG에는 제작 도구와 변환 작업을 선언하는 C2PA 메타데이터가 들어 있다. 내장 선언의 서명은 검증하지 않았다. 최초 생성 입력, 참조 이미지, 별도 사용 허락은 확인된 자료가 없다. 따라서 내장 도구 이름이나 커밋 작성자를 저작자로 삼거나, 해당 정보만으로 자산의 라이선스를 새로 정하지 않는다. 원본·참조 자료나 사용 조건이 확인되면 그 근거를 이 절에 연결한다.

요구사항

시험 칸이 "검증 요구사항, 미완료"인 항목은 직접 확인하는 자동 시험이 아직 확인되지 않았다는 뜻이다. 명령 쪽(출력 이름 겹침, 쓰기 실패)의 계약은 test/cli.test.js가 확인한다.

요구사항 검증 계획
블록 아래에 이미지 줄이 생기고 대체 글이 title이다. test/markdown.test.js(M1)
다시 돌려도 결과가 같고 파일을 쓰지 않는다(멱등). test/markdown.test.js(M2)
이름 있는 블록은 순서가 바뀌어도 같은 SVG 이름이다. test/markdown.test.js(M3)
이름이 바뀌면 옛 SVG를 지우고, 손으로 만든 SVG와 다른 문서의 SVG는 둔다. test/markdown.test.js(M3, M4)
같은 이름의 문서 둘이 --out-dir을 공유해도 서로의 SVG를 지우지 않고, 한 실행의 쓰기와 삭제 대상이 겹치지 않는다. test/markdown.test.js(M6)
이미 있는 SVG가 다른 문서 것이거나 표시가 없으면 쓰기 전에 오류를 내고, 오류가 난 실행은 어떤 파일도 바꾸지 않는다(--check 포함). 표시는 %, --, 한글, 상대 경로를 섞어도 서로 다르다. 판 번호가 없거나 이름이 다른 표시가 붙은 파일은 사용자 파일이라 쓰지도 지우지도 않는다. test/markdown.test.js(M4, M5)
대소문자나 유니코드 정규형만 다른 출력 이름은 쓰기 전에 충돌이다. 링크 별칭으로 연 같은 문서와 출력 폴더는 같은 소유다. 같은 파일을 가리키는 그림 링크 둘은 쓰기 전에 충돌이고(#176), 깨진 링크는 파일로 바꾸지 않고 io 한 줄로 거절한다. 문서와 그 문서로 이어진 링크를 함께 주면 같은 출력이라 쓰기 전에 거절하고, 링크 하나만 주면 링크를 두고 실제 문서를 고친다. 대소문자만 다른 소유 SVG는 낡은 파일로 지우지 않는다. test/markdown.test.js(M6, M11), test/cli.test.js(L5, L6)
장면 없는 블록에 준 --scene은 그 블록 줄의 오류이고 아무것도 쓰지 않는다. --scene이 없으면 정지 그림 하나다. test/markdown.test.js(M12)
같은 출력 폴더를 다른 프로세스가 쓰는 중이면 파일을 바꾸기 전에 오류로 끝나고, 낡은 잠금은 같은 호스트에서 주인이 확실히 없을 때만 치우며, 끝나면 잠금을 푼다. test/markdown.test.js(M10)
--fold는 그림을 앞에, 원본 블록을 <details> 안에 둔다. 제목은 이스케이프한 글이다. --fold와 --unfold는 함께 못 쓴다. test/markdown.test.js(M7)
접기, 접기 해제, 옵션 생략을 각각 두 번 돌리면 두 번째는 바꾸지 않는다. 접기 뒤 해제하면 접기 전에 돌린 문서와 바이트 단위로 같다. test/markdown.test.js(M7)
여러 블록, 긴 울타리, CRLF와 섞인 줄바꿈, 목록, 인용, 한글 제목을 처리한다. test/markdown.test.js(M8, M9)
옵션 없는 실행은 표식 없는 인용 안 블록을 읽지 않고 그 문서를 인용 지원 이전 판과 같은 결과로 둔다. 표식이 있는 인용 블록은 옵션 없이도 갱신한다. 옵션을 준 실행은 인용 안 블록을 처리하고 닫히지 않은 인용 울타리를 오류로 알린다. test/markdown.test.js(M9)
코드 span, 여러 줄 주석, 이스케이프 안의 <details>는 세지 않고 실제 태그만 센다. 인용과 목록의 다른 칸에 있는 태그는 블록의 부모가 아니다. 인용 안이든 밖이든 짝 없는 </details>는 줄 번호로 알린다. 직접 확인하는 자동 시험이 아직 확인되지 않았다(검증 요구사항, 미완료)
사용자 <details>와 안의 블록, 사용자 이미지는 고치지 않고 --unfold도 풀지 않는다. 표식이 손상되거나 모호하면 사용자 내용을 지우지 않고 줄 번호로 알린다. test/markdown.test.js(M7)
같은 옵션의 --check 판정과 실제 변환 결과가 같고, 접기와 해제의 쓰기 실패는 문서와 SVG를 함께 옛 상태로 둔다. test/markdown.test.js(M2, M7, M11)
Action의 fold, fold-title 입력이 CLI와 같은 선택을 한다. 직접 확인하는 자동 시험이 아직 확인되지 않았다(검증 요구사항, 미완료)
블록이 없어지면 이미지 줄과 SVG를 정리하고, 다른 울타리 안의 표시 줄은 둔다. test/markdown.test.js(M4)
SVG 쓰기가 실패하면 원문을 바꾸지 않고 종료 1, 진단 code io다. 원문 교체나 둘째 SVG 교체가 실패하면 이미 바꾼 SVG를 옛 상태로 되돌리고, 지우기 실패는 새 상태를 유지하며 알린다. 원문은 임시 파일을 거쳐 모드와 링크를 유지하고 SVG보다 늦게 바뀐다. test/markdown.test.js(M11)
오류가 하나라도 있으면 어떤 파일도 쓰지 않는다. test/markdown.test.js(M5)
--check는 갱신이 필요하면 1, 아니면 0이고 쓰지 않는다. test/markdown.test.js(M2)
--out-dir의 이미지 주소는 실제 문서 기준 상대 경로이고, 링크 폴더나 링크 문서로 열어도 실제 문서에서 풀린다. 다른 드라이브(Windows)는 쓰기 전 오류다. 링크: test/markdown.test.js(M6). 다른 드라이브: 직접 확인하는 자동 시험이 아직 확인되지 않았다(검증 요구사항, 미완료)
--out-dir이 파일이면 쓰기 전에 io 한 줄이고 아무것도 쓰지 않는다. test/markdown.test.js(M11)
-로 시작하는 파일 이름은 -- 뒤에서 받고, 비어 있거나 겹친 옵션 값은 인자 오류다. test/cli.test.js(L2)
다른 울타리 안의 thinkflow 줄은 그리지 않는다. 3칸까지 들여쓴 블록과 목록 칸 안의 블록은 그리고, 들여쓴 코드와 HTML 주석 안은 읽지도 고치지도 않으며, 4칸 들여쓴 줄은 울타리를 닫지 못한다. test/markdown.test.js(M4, M9)
CRLF 문서는 CRLF로 다시 쓰고 두 번째 실행은 바꾸지 않는다. test/markdown.test.js(M8)
옵션은 명령마다 받는 것만 받는다. test/cli.test.js(L2, L3, L7)
패키지에는 실행에 필요한 파일, 공유 로고 PNG, 라이선스, NOTICE만 든다. test/package.test.js(P1), npm run check:package: 실제 tarball의 허용 파일 목록 검사
패키지를 설치해 실행할 수 있다. npm run check:package: 실제 tarball 설치, CLI의 check·render·md와 공개 API 출력 대조. Node 20·22 CI와 릴리스가 실행
공개 API의 타입과 구조화 진단을 설치 소비자가 사용하고 잘못된 호출은 컴파일에서 거부한다. test/package.test.js, npm run check:package의 TypeScript 소비자
Action이 저장소 CI에서 돈다. ci.yml의 action 작업
Action이 한글, 공백, 줄바꿈 이름의 잘못된 원본을 실패시키고 .thinkflow와 .md가 아닌 파일은 모으지 않으며, 겹치는 글롭은 한 번만 검사한다. 직접 확인하는 자동 시험이 아직 확인되지 않았다(검증 요구사항, 미완료)

단점

  • 문서와 같은 폴더에 SVG가 늘어난다. --out-dir로 모을 수 있지만 같은 이름의 문서가 서로 다른 폴더에 있으면 같은 --out-dir을 함께 쓸 수 없다(이름 겹침 오류).
  • 이미지 줄 표시(<!-- thinkflow -->)가 문서 원문에 남는다.
  • --fold를 쓰면 표식 주석 두 줄과 <details> 줄이 문서 원문에 남는다. GitHub는 주석을 그리지 않지만 원문을 읽는 사람에게는 보인다.
  • 저장소를 이미지 서버로 쓰므로 GitHub 밖(npm 패키지 페이지 등)에서는 상대 주소 이미지가 깨진다.

대안

  • 블록을 이미지로 바꾸고 원본을 지우는 방식(mermaid-cli의 기본)은 문서에서 원본을 잃으므로 쓰지 않는다. 원본은 블록으로 남긴다.
  • 접기를 늘 켜 두는 방식은 문서 구조를 모든 사용자에게 바꾸므로 쓰지 않는다. 옵션으로 고르고, 옵션이 없으면 접힘 상태를 지킨다(#39).
  • 접은 곳을 시작과 끝 표식 없이 <details> 모양만으로 알아보는 방식은 사용자가 쓴 <details>와 구분할 수 없어 쓰지 않는다.
  • 이미지 줄을 구분하는 표시로 시작과 끝 주석 두 줄을 쓰는 방식은 문서가 길어져 한 줄 표시를 고른다.
  • 이름 없는 블록의 이름을 문서 전체 순번으로 하는 방식은 이름 있는 블록이 끼면 이름이 밀려 쓰지 않는다.

미해결 질문

  • 없음