slow.monster

STUDIO HAZE · jeniapp

Topaz source

Open by house policy. Topaz is our own language.

Topaz 5.20.2 · 63 files

apps/living-docs/render-ko.tpz

Topaz · 16666 B · Original file

1// 살아있는 문서 렌더러. 마크다운을 HTML 조각으로 바꿉니다. 제목, 문단, 인용, 구분선, 중첩 목록, 표, 펜스 코드와 굵게, 기울임, 취소선, 코드, 링크, 이미지를 다룹니다.
2// 입력은 마크다운 글이고 출력은 HTML 조각입니다. ```topaz 펜스는 순번을 단 빈 자리 요소가 되고 페이지가 그 자리에 실행 결과를 채웁니다.
3// 같은 입력이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다.
4// 원시 HTML 은 통과시키지 않고 이스케이프합니다. 느슨한 목록, 탭 들여쓰기, 표 칸 정렬, 참조 링크는 하지 않습니다.
5// Living docs renderer: turns Markdown into an HTML fragment with numbered slots for Topaz blocks.
7type 목록태그 = "ul" | "ol"
8type 칸태그 = "th" | "td"
9type 강조태그 = "strong" | "em" | "del"
10record 렌더결과 { 출력: string, 다음: int }
11record 목록항목 { 태그: 목록태그, 본문: Array<string> }
12record 링크자리 { 글: Array<string>, 주소: string, 다음: int }
14// 다른 사이트로 가는 프로토콜 상대 주소라 허용 접두보다 먼저 막습니다. 허용 접두의 `/` 는 같은 사이트의 루트 경로입니다.
15let 외부상대접두 = ["//", "/\\", "\\"]
16let 허용접두 = ["http://", "https://", "mailto:", "/", "#", "./", "../"]
17let 블록접두 = ["# ", "## ", "### ", "- ", "> ", "```"]
19// 공용 도우미 시작 (이스케이프 본문이스케이프 숫자인가 연속끝)
20// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.
22// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.
23function 이스케이프(원문: string) -> string {
24 원문.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
27// 요소 본문에 넣는 사용자 입력에 씁니다. 큰따옴표를 그대로 두므로 속성 값에는 이스케이프를 씁니다.
28function 본문이스케이프(원문: string) -> string {
29 원문.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
32function 숫자인가(글자: string) -> bool {
33 let 코드 = 글자.codePointAt(0) ?? 0
34 코드 >= 48 && 코드 <= 57
37// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.
38function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {
39 let mut 끝 = 시작
40 while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }
41 끝
44// 공용 도우미 끝
46function 접두로시작인가(글: string, 접두들: Array<string>) -> bool {
47 접두들.filter(접두 => 글.startsWith(접두)).length > 0
50function 표식뒤(줄: string, 표식길이: int) -> Array<string> {
51 let 글자들 = 줄.scalars()
52 글자들.slice(표식길이, 글자들.length)
55function 표식위치(글자들: Array<string>, 시작: int, 표식: string) -> Option<int> {
56 let 표식길이 = 표식.scalars().length
57 let mut 위치 = 시작
58 while 위치 + 표식길이 <= 글자들.length {
59 if 글자들.slice(위치, 위치 + 표식길이).join("") == 표식 { return Some(위치) }
60 위치 += 1
61 }
62 None
65// 주소 안의 괄호 짝을 맞춰 `foo(1)` 같은 주소가 첫 `)` 에서 잘리지 않게 합니다.
66function 괄호닫기(글자들: Array<string>, 시작: int) -> Option<int> {
67 let mut 깊이 = 0
68 for i in 시작..<글자들.length {
69 if 글자들[i] == "(" {
70 깊이 += 1
71 } else if 글자들[i] == ")" {
72 if 깊이 == 0 { return Some(i) }
73 깊이 -= 1
74 }
75 }
76 None
79// 첫 `/` 앞에 `:` 가 있으면 스킴이 붙은 주소입니다.
80function 스킴주소인가(주소: string) -> bool {
81 let 구분글자 = 주소.scalars().filter(글자 => 글자 == ":" || 글자 == "/")
82 구분글자.length > 0 && 구분글자[0] == ":"
85// 브라우저는 주소 앞 공백을 지우고 해석하므로 지운 꼴로 봅니다. 허용 접두 밖의 스킴은 javascript: 와 data: 를 포함해 모두 막습니다.
86function 안전한주소인가(주소: string) -> bool {
87 let 정리주소 = 주소.trimStart()
88 if 접두로시작인가(정리주소, 외부상대접두) {
89 false
90 } else if 접두로시작인가(정리주소, 허용접두) {
91 true
92 } else {
93 !스킴주소인가(정리주소)
94 }
97function 링크찾기(글자들: Array<string>, 글시작: int) -> Option<링크자리> {
98 match 표식위치(글자들, 글시작, "]") {
99 case Some(글끝) => {
100 if 글끝 + 1 >= 글자들.length || 글자들[글끝 + 1] != "(" { return None }
101 match 괄호닫기(글자들, 글끝 + 2) {
102 case Some(주소끝) => Some(링크자리 {
103 글: 글자들.slice(글시작, 글끝),
104 주소: 글자들.slice(글끝 + 2, 주소끝).join(""),
105 다음: 주소끝 + 1,
106 })
107 case None => None
108 }
109 }
110 case None => None
111 }
114// 짝이 맞지 않는 표식은 첫 글자 하나만 글로 내보내고 바로 다음 글자부터 다시 읽습니다.
115function 글자그대로(글자들: Array<string>, 위치: int) -> 렌더결과 {
116 렌더결과 { 출력: 본문이스케이프(글자들[위치]), 다음: 위치 + 1 }
119function 코드읽기(글자들: Array<string>, 위치: int) -> 렌더결과 {
120 match 표식위치(글자들, 위치 + 1, "`") {
121 case Some(닫기) => 렌더결과 {
122 출력: "<code>{본문이스케이프(글자들.slice(위치 + 1, 닫기).join(""))}</code>",
123 다음: 닫기 + 1,
124 }
125 case None => 글자그대로(글자들, 위치)
126 }
129function 강조읽기(글자들: Array<string>, 위치: int, 표식: string, 태그: 강조태그) -> 렌더결과 {
130 let 표식길이 = 표식.scalars().length
131 let 안시작 = 위치 + 표식길이
132 match 표식위치(글자들, 안시작, 표식) {
133 case Some(닫기) => 렌더결과 {
134 출력: "<{태그}>{인라인변환(글자들.slice(안시작, 닫기))}</{태그}>",
135 다음: 닫기 + 표식길이,
136 }
137 case None => 글자그대로(글자들, 위치)
138 }
141// 허용하지 않는 주소의 이미지는 대체 글만 남기고 인라인 표식은 해석하지 않습니다.
142function 이미지읽기(글자들: Array<string>, 위치: int) -> 렌더결과 {
143 match 링크찾기(글자들, 위치 + 2) {
144 case Some(찾음) => {
145 let 대체글 = 찾음.글.join("")
146 let 출력 = if 안전한주소인가(찾음.주소) {
147 "<img src=\"{이스케이프(찾음.주소)}\" alt=\"{이스케이프(대체글)}\">"
148 } else {
149 본문이스케이프(대체글)
150 }
151 렌더결과 { 출력: 출력, 다음: 찾음.다음 }
152 }
153 case None => 글자그대로(글자들, 위치)
154 }
157// 허용하지 않는 주소의 링크는 링크 글만 남깁니다.
158function 링크읽기(글자들: Array<string>, 위치: int) -> 렌더결과 {
159 match 링크찾기(글자들, 위치 + 1) {
160 case Some(찾음) => {
161 let 글 = 인라인변환(찾음.글)
162 let 출력 = if 안전한주소인가(찾음.주소) { "<a href=\"{이스케이프(찾음.주소)}\">{글}</a>" } else { 글 }
163 렌더결과 { 출력: 출력, 다음: 찾음.다음 }
164 }
165 case None => 글자그대로(글자들, 위치)
166 }
169function 인라인읽기(글자들: Array<string>, 위치: int) -> 렌더결과 {
170 let 다음글자 = if 위치 + 1 < 글자들.length { 글자들[위치 + 1] } else { "" }
171 match 글자들[위치] {
172 case "\\" => if 위치 + 1 < 글자들.length {
173 렌더결과 { 출력: 본문이스케이프(다음글자), 다음: 위치 + 2 }
174 } else {
175 글자그대로(글자들, 위치)
176 }
177 case "!" => if 다음글자 == "[" { 이미지읽기(글자들, 위치) } else { 글자그대로(글자들, 위치) }
178 case "`" => 코드읽기(글자들, 위치)
179 case "*" => if 다음글자 == "*" { 강조읽기(글자들, 위치, "**", "strong") } else { 강조읽기(글자들, 위치, "*", "em") }
180 case "~" => if 다음글자 == "~" { 강조읽기(글자들, 위치, "~~", "del") } else { 글자그대로(글자들, 위치) }
181 case "[" => 링크읽기(글자들, 위치)
182 case _ => 글자그대로(글자들, 위치)
183 }
186function 인라인변환(글자들: Array<string>) -> string {
187 let mut 조각들: Array<string> = []
188 let mut 위치 = 0
189 while 위치 < 글자들.length {
190 let 읽음 = 인라인읽기(글자들, 위치)
191 조각들.push(읽음.출력)
192 위치 = 읽음.다음
193 }
194 조각들.join("")
197// `1. ` 처럼 숫자, 마침표, 공백으로 된 표식의 글자 수입니다. 표식이 아니면 0 입니다.
198function 번호표식길이(글자들: Array<string>) -> int {
199 let 숫자끝 = 연속끝(글자들, 0, 숫자인가)
200 if 숫자끝 > 0 && 글자들.slice(숫자끝, 숫자끝 + 2).join("") == ". " { 숫자끝 + 2 } else { 0 }
203// 목록은 공백 두 칸마다 한 단계 깊어지므로 들여쓰기가 정확히 들여깊이 칸인 항목만 읽습니다.
204function 목록항목읽기(줄들: Array<string>, 위치: int, 들여깊이: int) -> Option<목록항목> {
205 if 위치 >= 줄들.length { return None }
206 let 글자들 = 줄들[위치].scalars()
207 if 연속끝(글자들, 0, 글자 => 글자 == " ") != 들여깊이 { return None }
208 let 나머지 = 글자들.slice(들여깊이, 글자들.length)
209 let 번호길이 = 번호표식길이(나머지)
210 if 나머지.slice(0, 2).join("") == "- " {
211 Some(목록항목 { 태그: "ul", 본문: 나머지.slice(2, 나머지.length) })
212 } else if 번호길이 > 0 {
213 Some(목록항목 { 태그: "ol", 본문: 나머지.slice(번호길이, 나머지.length) })
214 } else {
215 None
216 }
219// 깊이와 종류가 같은 항목을 한 목록으로 모읍니다. 두 칸 더 들인 항목은 바로 앞 항목 안의 목록이 됩니다.
220function 목록렌더(줄들: Array<string>, 시작: int, 들여깊이: int, 태그: 목록태그) -> 렌더결과 {
221 let mut 조각들 = ["<{태그}>\n"]
222 let mut 위치 = 시작
223 while 위치 < 줄들.length {
224 match 목록항목읽기(줄들, 위치, 들여깊이) {
225 case Some(항목) => {
226 if 항목.태그 != 태그 { break }
227 조각들.push("<li>{인라인변환(항목.본문)}")
228 위치 += 1
229 match 목록항목읽기(줄들, 위치, 들여깊이 + 2) {
230 case Some(자식) => {
231 let 하위목록 = 목록렌더(줄들, 위치, 들여깊이 + 2, 자식.태그)
232 조각들.push("\n{하위목록.출력}</li>\n")
233 위치 = 하위목록.다음
234 }
235 case None => { 조각들.push("</li>\n") }
236 }
237 }
238 case None => { break }
239 }
240 }
241 조각들.push("</{태그}>\n")
242 렌더결과 { 출력: 조각들.join(""), 다음: 위치 }
245// 이어진 인용 줄은 공백 하나로 이어 한 인용문이 됩니다.
246function 인용렌더(줄들: Array<string>, 시작: int) -> 렌더결과 {
247 let 끝 = 연속끝(줄들, 시작, 줄 => 줄.startsWith("> "))
248 let 본문 = 줄들.slice(시작, 끝).map(줄 => 인라인변환(표식뒤(줄, 2))).join(" ")
249 렌더결과 { 출력: "<blockquote>{본문}</blockquote>\n", 다음: 끝 }
252function 파이프포함인가(줄: string) -> bool {
253 "|" in 줄.scalars()
256// `\|` 처럼 백슬래시가 붙은 글자에서는 나누지 않고 인라인 변환이 이스케이프로 처리하게 둡니다. 바깥 파이프가 만든 맨 앞과 맨 뒤의 빈 칸은 버립니다.
257function 칸나누기(줄: string) -> Array<string> {
258 let 글자들 = 줄.scalars()
259 let mut 칸들: Array<string> = []
260 let mut 칸시작 = 0
261 let mut 위치 = 0
262 while 위치 < 글자들.length {
263 if 글자들[위치] == "\\" && 위치 + 1 < 글자들.length {
264 위치 += 2
265 } else if 글자들[위치] == "|" {
266 칸들.push(글자들.slice(칸시작, 위치).join("").trim())
267 위치 += 1
268 칸시작 = 위치
269 } else {
270 위치 += 1
271 }
272 }
273 칸들.push(글자들.slice(칸시작, 글자들.length).join("").trim())
274 let 마지막 = 칸들.length - 1
275 [for i in 0..<칸들.length if 칸들[i] != "" || (i > 0 && i < 마지막) => 칸들[i]]
278// 구분 칸은 앞뒤에 `:` 를 하나씩 둘 수 있고 가운데에 `-` 가 하나 이상 있어야 합니다.
279function 구분칸인가(칸: string) -> bool {
280 let 글자들 = 칸.scalars()
281 let 대시시작 = if 칸.startsWith(":") { 1 } else { 0 }
282 let 대시끝 = 연속끝(글자들, 대시시작, 글자 => 글자 == "-")
283 let 칸끝 = if 대시끝 < 글자들.length && 글자들[대시끝] == ":" { 대시끝 + 1 } else { 대시끝 }
284 대시끝 > 대시시작 && 칸끝 == 글자들.length
287// 머리 줄 바로 다음 줄이 칸 수가 같은 구분 줄일 때만 표가 됩니다.
288function 표머리인가(줄들: Array<string>, 위치: int) -> bool {
289 if 위치 + 1 >= 줄들.length || !파이프포함인가(줄들[위치]) || !파이프포함인가(줄들[위치 + 1]) { return false }
290 let 구분칸들 = 칸나누기(줄들[위치 + 1])
291 구분칸들.length > 0
292 && 구분칸들.filter(칸 => !구분칸인가(칸)).length == 0
293 && 칸나누기(줄들[위치]).length == 구분칸들.length
296function 블록시작인가(줄: string) -> bool {
297 줄 == "" || 줄 == "---" || 접두로시작인가(줄, 블록접두) || 번호표식길이(줄.scalars()) > 0
300function 표행(칸들: Array<string>, 태그: 칸태그) -> string {
301 let 칸html = 칸들.map(칸 => "<{태그}>{인라인변환(칸.scalars())}</{태그}>").join("")
302 "<tr>{칸html}</tr>\n"
305// 표 본문은 파이프가 없는 줄이나 다른 블록이 시작하는 줄에서 끝납니다.
306function 표렌더(줄들: Array<string>, 시작: int) -> 렌더결과 {
307 let 끝 = 연속끝(줄들, 시작 + 2, 줄 => 파이프포함인가(줄) && !블록시작인가(줄))
308 let 머리 = 표행(칸나누기(줄들[시작]), "th")
309 let 본문 = 줄들.slice(시작 + 2, 끝).map(줄 => 표행(칸나누기(줄), "td")).join("")
310 렌더결과 { 출력: "<table>\n<thead>\n{머리}</thead>\n<tbody>\n{본문}</tbody>\n</table>\n", 다음: 끝 }
313function 제목(줄: string, 단계: int) -> string {
314 "<h{단계}>{인라인변환(표식뒤(줄, 단계 + 1))}</h{단계}>\n"
317function 한줄블록(출력: string, 위치: int) -> 렌더결과 {
318 렌더결과 { 출력: 출력, 다음: 위치 + 1 }
321// 표는 제목, 목록, 인용, 구분선 다음에 보므로 `#`, `-`, `>` 로 시작하는 줄이 표 머리가 되지 않습니다.
322function 블록읽기(줄들: Array<string>, 위치: int) -> 렌더결과 {
323 let 줄 = 줄들[위치]
324 match 목록항목읽기(줄들, 위치, 0) {
325 case Some(항목) => 목록렌더(줄들, 위치, 0, 항목.태그)
326 case None => if 줄.startsWith("> ") {
327 인용렌더(줄들, 위치)
328 } else if 줄 == "---" {
329 한줄블록("<hr>\n", 위치)
330 } else if 줄.startsWith("### ") {
331 한줄블록(제목(줄, 3), 위치)
332 } else if 줄.startsWith("## ") {
333 한줄블록(제목(줄, 2), 위치)
334 } else if 줄.startsWith("# ") {
335 한줄블록(제목(줄, 1), 위치)
336 } else if 표머리인가(줄들, 위치) {
337 표렌더(줄들, 위치)
338 } else if 줄 == "" {
339 한줄블록("", 위치)
340 } else {
341 한줄블록("<p>{인라인변환(줄.scalars())}</p>\n", 위치)
342 }
343 }
346// 펜스 안의 줄은 블록으로 해석하지 않습니다. 페이지가 같은 규칙으로 ```topaz 원본을 문서 순서대로 뽑아 순번에 맞춰 결과를 넣습니다.
347function 마크다운변환(원문: string) -> string {
348 let 줄들 = 원문.replace("\r", "").split("\n")
349 let mut 조각들: Array<string> = []
350 let mut 위치 = 0
351 let mut 블록번호 = 0
352 while 위치 < 줄들.length {
353 let 줄 = 줄들[위치]
354 if 줄.startsWith("```") {
355 let 끝 = 연속끝(줄들, 위치 + 1, 코드줄 => !코드줄.startsWith("```"))
356 if 줄 == "```topaz" {
357 조각들.push("<div class=\"topaz-block\" data-topaz-block=\"{블록번호}\"></div>\n")
358 블록번호 += 1
359 } else {
360 let 코드 = 줄들.slice(위치 + 1, 끝).map(코드줄 => "{본문이스케이프(코드줄)}\n").join("")
361 조각들.push("<pre><code>{코드}</code></pre>\n")
362 }
363 // 닫는 펜스가 없으면 문서 끝까지가 코드입니다.
364 위치 = (끝 + 1).atMost(줄들.length)
365 } else {
366 let 블록 = 블록읽기(줄들, 위치)
367 조각들.push(블록.출력)
368 위치 = 블록.다음
369 }
370 }
371 조각들.join("")
374input() |> 마크다운변환 |> print

SHA256 3aa803950c2664db23f78c04c9951de67053b64600e11143b260d008f991f384