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.7
type 목록태그 = "ul" | "ol"8
type 칸태그 = "th" | "td"9
type 강조태그 = "strong" | "em" | "del"10
record 렌더결과 { 출력: string, 다음: int }11
record 목록항목 { 태그: 목록태그, 본문: Array<string> }12
record 링크자리 { 글: Array<string>, 주소: string, 다음: int }14
// 다른 사이트로 가는 프로토콜 상대 주소라 허용 접두보다 먼저 막습니다. 허용 접두의 `/` 는 같은 사이트의 루트 경로입니다.15
let 외부상대접두 = ["//", "/\\", "\\"]16
let 허용접두 = ["http://", "https://", "mailto:", "/", "#", "./", "../"]17
let 블록접두 = ["# ", "## ", "### ", "- ", "> ", "```"]19
// 공용 도우미 시작 (이스케이프 본문이스케이프 숫자인가 연속끝)20
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.22
// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.23
function 이스케이프(원문: string) -> string {24
원문.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)25
}27
// 요소 본문에 넣는 사용자 입력에 씁니다. 큰따옴표를 그대로 두므로 속성 값에는 이스케이프를 씁니다.28
function 본문이스케이프(원문: string) -> string {29
원문.replace("&", "&").replace("<", "<").replace(">", ">")30
}32
function 숫자인가(글자: string) -> bool {33
let 코드 = 글자.codePointAt(0) ?? 034
코드 >= 48 && 코드 <= 5735
}37
// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.38
function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {39
let mut 끝 = 시작40
while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }41
끝42
}44
// 공용 도우미 끝46
function 접두로시작인가(글: string, 접두들: Array<string>) -> bool {47
접두들.filter(접두 => 글.startsWith(접두)).length > 048
}50
function 표식뒤(줄: string, 표식길이: int) -> Array<string> {51
let 글자들 = 줄.scalars()52
글자들.slice(표식길이, 글자들.length)53
}55
function 표식위치(글자들: Array<string>, 시작: int, 표식: string) -> Option<int> {56
let 표식길이 = 표식.scalars().length57
let mut 위치 = 시작58
while 위치 + 표식길이 <= 글자들.length {59
if 글자들.slice(위치, 위치 + 표식길이).join("") == 표식 { return Some(위치) }60
위치 += 161
}62
None63
}65
// 주소 안의 괄호 짝을 맞춰 `foo(1)` 같은 주소가 첫 `)` 에서 잘리지 않게 합니다.66
function 괄호닫기(글자들: Array<string>, 시작: int) -> Option<int> {67
let mut 깊이 = 068
for i in 시작..<글자들.length {69
if 글자들[i] == "(" {70
깊이 += 171
} else if 글자들[i] == ")" {72
if 깊이 == 0 { return Some(i) }73
깊이 -= 174
}75
}76
None77
}79
// 첫 `/` 앞에 `:` 가 있으면 스킴이 붙은 주소입니다.80
function 스킴주소인가(주소: string) -> bool {81
let 구분글자 = 주소.scalars().filter(글자 => 글자 == ":" || 글자 == "/")82
구분글자.length > 0 && 구분글자[0] == ":"83
}85
// 브라우저는 주소 앞 공백을 지우고 해석하므로 지운 꼴로 봅니다. 허용 접두 밖의 스킴은 javascript: 와 data: 를 포함해 모두 막습니다.86
function 안전한주소인가(주소: string) -> bool {87
let 정리주소 = 주소.trimStart()88
if 접두로시작인가(정리주소, 외부상대접두) {89
false90
} else if 접두로시작인가(정리주소, 허용접두) {91
true92
} else {93
!스킴주소인가(정리주소)94
}95
}97
function 링크찾기(글자들: 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 => None108
}109
}110
case None => None111
}112
}114
// 짝이 맞지 않는 표식은 첫 글자 하나만 글로 내보내고 바로 다음 글자부터 다시 읽습니다.115
function 글자그대로(글자들: Array<string>, 위치: int) -> 렌더결과 {116
렌더결과 { 출력: 본문이스케이프(글자들[위치]), 다음: 위치 + 1 }117
}119
function 코드읽기(글자들: Array<string>, 위치: int) -> 렌더결과 {120
match 표식위치(글자들, 위치 + 1, "`") {121
case Some(닫기) => 렌더결과 {122
출력: "<code>{본문이스케이프(글자들.slice(위치 + 1, 닫기).join(""))}</code>",123
다음: 닫기 + 1,124
}125
case None => 글자그대로(글자들, 위치)126
}127
}129
function 강조읽기(글자들: Array<string>, 위치: int, 표식: string, 태그: 강조태그) -> 렌더결과 {130
let 표식길이 = 표식.scalars().length131
let 안시작 = 위치 + 표식길이132
match 표식위치(글자들, 안시작, 표식) {133
case Some(닫기) => 렌더결과 {134
출력: "<{태그}>{인라인변환(글자들.slice(안시작, 닫기))}</{태그}>",135
다음: 닫기 + 표식길이,136
}137
case None => 글자그대로(글자들, 위치)138
}139
}141
// 허용하지 않는 주소의 이미지는 대체 글만 남기고 인라인 표식은 해석하지 않습니다.142
function 이미지읽기(글자들: 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
}155
}157
// 허용하지 않는 주소의 링크는 링크 글만 남깁니다.158
function 링크읽기(글자들: Array<string>, 위치: int) -> 렌더결과 {159
match 링크찾기(글자들, 위치 + 1) {160
case Some(찾음) => {161
let 글 = 인라인변환(찾음.글)162
let 출력 = if 안전한주소인가(찾음.주소) { "<a href=\"{이스케이프(찾음.주소)}\">{글}</a>" } else { 글 }163
렌더결과 { 출력: 출력, 다음: 찾음.다음 }164
}165
case None => 글자그대로(글자들, 위치)166
}167
}169
function 인라인읽기(글자들: 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
}184
}186
function 인라인변환(글자들: Array<string>) -> string {187
let mut 조각들: Array<string> = []188
let mut 위치 = 0189
while 위치 < 글자들.length {190
let 읽음 = 인라인읽기(글자들, 위치)191
조각들.push(읽음.출력)192
위치 = 읽음.다음193
}194
조각들.join("")195
}197
// `1. ` 처럼 숫자, 마침표, 공백으로 된 표식의 글자 수입니다. 표식이 아니면 0 입니다.198
function 번호표식길이(글자들: Array<string>) -> int {199
let 숫자끝 = 연속끝(글자들, 0, 숫자인가)200
if 숫자끝 > 0 && 글자들.slice(숫자끝, 숫자끝 + 2).join("") == ". " { 숫자끝 + 2 } else { 0 }201
}203
// 목록은 공백 두 칸마다 한 단계 깊어지므로 들여쓰기가 정확히 들여깊이 칸인 항목만 읽습니다.204
function 목록항목읽기(줄들: 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
None216
}217
}219
// 깊이와 종류가 같은 항목을 한 목록으로 모읍니다. 두 칸 더 들인 항목은 바로 앞 항목 안의 목록이 됩니다.220
function 목록렌더(줄들: 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
위치 += 1229
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(""), 다음: 위치 }243
}245
// 이어진 인용 줄은 공백 하나로 이어 한 인용문이 됩니다.246
function 인용렌더(줄들: Array<string>, 시작: int) -> 렌더결과 {247
let 끝 = 연속끝(줄들, 시작, 줄 => 줄.startsWith("> "))248
let 본문 = 줄들.slice(시작, 끝).map(줄 => 인라인변환(표식뒤(줄, 2))).join(" ")249
렌더결과 { 출력: "<blockquote>{본문}</blockquote>\n", 다음: 끝 }250
}252
function 파이프포함인가(줄: string) -> bool {253
"|" in 줄.scalars()254
}256
// `\|` 처럼 백슬래시가 붙은 글자에서는 나누지 않고 인라인 변환이 이스케이프로 처리하게 둡니다. 바깥 파이프가 만든 맨 앞과 맨 뒤의 빈 칸은 버립니다.257
function 칸나누기(줄: string) -> Array<string> {258
let 글자들 = 줄.scalars()259
let mut 칸들: Array<string> = []260
let mut 칸시작 = 0261
let mut 위치 = 0262
while 위치 < 글자들.length {263
if 글자들[위치] == "\\" && 위치 + 1 < 글자들.length {264
위치 += 2265
} else if 글자들[위치] == "|" {266
칸들.push(글자들.slice(칸시작, 위치).join("").trim())267
위치 += 1268
칸시작 = 위치269
} else {270
위치 += 1271
}272
}273
칸들.push(글자들.slice(칸시작, 글자들.length).join("").trim())274
let 마지막 = 칸들.length - 1275
[for i in 0..<칸들.length if 칸들[i] != "" || (i > 0 && i < 마지막) => 칸들[i]]276
}278
// 구분 칸은 앞뒤에 `:` 를 하나씩 둘 수 있고 가운데에 `-` 가 하나 이상 있어야 합니다.279
function 구분칸인가(칸: string) -> bool {280
let 글자들 = 칸.scalars()281
let 대시시작 = if 칸.startsWith(":") { 1 } else { 0 }282
let 대시끝 = 연속끝(글자들, 대시시작, 글자 => 글자 == "-")283
let 칸끝 = if 대시끝 < 글자들.length && 글자들[대시끝] == ":" { 대시끝 + 1 } else { 대시끝 }284
대시끝 > 대시시작 && 칸끝 == 글자들.length285
}287
// 머리 줄 바로 다음 줄이 칸 수가 같은 구분 줄일 때만 표가 됩니다.288
function 표머리인가(줄들: Array<string>, 위치: int) -> bool {289
if 위치 + 1 >= 줄들.length || !파이프포함인가(줄들[위치]) || !파이프포함인가(줄들[위치 + 1]) { return false }290
let 구분칸들 = 칸나누기(줄들[위치 + 1])291
구분칸들.length > 0292
&& 구분칸들.filter(칸 => !구분칸인가(칸)).length == 0293
&& 칸나누기(줄들[위치]).length == 구분칸들.length294
}296
function 블록시작인가(줄: string) -> bool {297
줄 == "" || 줄 == "---" || 접두로시작인가(줄, 블록접두) || 번호표식길이(줄.scalars()) > 0298
}300
function 표행(칸들: Array<string>, 태그: 칸태그) -> string {301
let 칸html = 칸들.map(칸 => "<{태그}>{인라인변환(칸.scalars())}</{태그}>").join("")302
"<tr>{칸html}</tr>\n"303
}305
// 표 본문은 파이프가 없는 줄이나 다른 블록이 시작하는 줄에서 끝납니다.306
function 표렌더(줄들: 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", 다음: 끝 }311
}313
function 제목(줄: string, 단계: int) -> string {314
"<h{단계}>{인라인변환(표식뒤(줄, 단계 + 1))}</h{단계}>\n"315
}317
function 한줄블록(출력: string, 위치: int) -> 렌더결과 {318
렌더결과 { 출력: 출력, 다음: 위치 + 1 }319
}321
// 표는 제목, 목록, 인용, 구분선 다음에 보므로 `#`, `-`, `>` 로 시작하는 줄이 표 머리가 되지 않습니다.322
function 블록읽기(줄들: 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
}344
}346
// 펜스 안의 줄은 블록으로 해석하지 않습니다. 페이지가 같은 규칙으로 ```topaz 원본을 문서 순서대로 뽑아 순번에 맞춰 결과를 넣습니다.347
function 마크다운변환(원문: string) -> string {348
let 줄들 = 원문.replace("\r", "").split("\n")349
let mut 조각들: Array<string> = []350
let mut 위치 = 0351
let mut 블록번호 = 0352
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
블록번호 += 1359
} 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("")372
}374
input() |> 마크다운변환 |> printSHA256 3aa803950c2664db23f78c04c9951de67053b64600e11143b260d008f991f384