STUDIO HAZE · jeniapp
Topaz source
Open by house policy. Topaz is our own language.
Topaz 5.20.2 · 63 files
apps/json-format/format.tpz
Topaz · 14454 B · Original file
1
// JSON 포매터. 전체 JSON 문법을 재귀 하강으로 검증하고 두 칸 들여쓰기로 다시 씁니다.2
// 입력은 JSON 문서 하나이고, 출력은 정리한 문서를 담은 pre 조각이나 줄:열 위치가 붙은 한국어 오류 한 줄입니다.3
// 같은 입력이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다.4
// 숫자는 원문 그대로 두고 실수로 다시 읽거나 반올림하지 않습니다. 문자열은 안전한 \uXXXX 만 글자로 풀고 나머지는 원문 그대로 둡니다. 같은 키가 두 번 나와도 오류로 보지 않고 그대로 둡니다.5
// JSON formatter: validates the full grammar and pretty-prints with two-space indentation, reporting errors by line and column.7
// 수와 문자열은 원문 토큰을 그대로 담고, 들여쓰기는 직렬화 단계에서만 붙입니다.8
enum Json값 { 없음, 참거짓(bool), 수(string), 문자열(string), 배열(Array<Json값>), 객체(Array<멤버>) }9
// 키는 따옴표까지 포함한 원문입니다.10
record 멤버 { 키: string, 값: Json값 }11
record 읽은값 { 값: Json값, 다음: int }12
record 읽은문자열 { 원문: string, 다음: int }14
enum 오류종류 {15
문자열미종료, 유니코드이스케이프, 잘못된이스케이프, 제어문자,16
숫자필요, 소수점숫자필요, 지수숫자필요,17
너무깊음, 값필요, 알수없는값,18
키필요, 콜론필요, 닫는중괄호필요, 쉼표나닫는중괄호,19
닫는대괄호필요, 쉼표나닫는대괄호, 잉여내용20
}21
record 구문오류 { 위치: int, 종류: 오류종류 }23
let 최대깊이 = 20024
let 십육진글자 = Set.of(25
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",26
"a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F",27
)28
let 한글자이스케이프 = Set.of("\"", "\\", "/", "b", "f", "n", "r", "t")30
// 공용 도우미 시작 (본문이스케이프 공백인가 숫자인가 연속끝 글자인가)31
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.33
// 요소 본문에 넣는 사용자 입력에 씁니다. 큰따옴표를 그대로 두므로 속성 값에는 이스케이프를 씁니다.34
function 본문이스케이프(원문: string) -> string {35
원문.replace("&", "&").replace("<", "<").replace(">", ">")36
}38
function 공백인가(글자: string) -> bool {39
글자 == " " || 글자 == "\t" || 글자 == "\n" || 글자 == "\r"40
}42
function 숫자인가(글자: string) -> bool {43
let 코드 = 글자.codePointAt(0) ?? 044
코드 >= 48 && 코드 <= 5745
}47
// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.48
function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {49
let mut 끝 = 시작50
while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }51
끝52
}54
// 위치가 글자들 끝을 넘으면 거짓입니다.55
function 글자인가(글자들: Array<string>, 위치: int, 글자: string) -> bool {56
위치 < 글자들.length && 글자들[위치] == 글자57
}59
// 공용 도우미 끝61
function 오류문구(종류: 오류종류) -> string {62
match 종류 {63
case 문자열미종료 => "문자열이 끝나지 않았습니다"64
case 유니코드이스케이프 => "\\u 뒤에 16진수 4자리가 필요합니다"65
case 잘못된이스케이프 => "잘못된 이스케이프 문자입니다"66
case 제어문자 => "문자열 안의 제어문자는 이스케이프해야 합니다"67
case 숫자필요 => "숫자가 필요합니다"68
case 소수점숫자필요 => "소수점 뒤에 숫자가 필요합니다"69
case 지수숫자필요 => "지수 뒤에 숫자가 필요합니다"70
case 너무깊음 => "중첩이 너무 깊습니다 (최대 200단계)"71
case 값필요 => "값이 필요합니다"72
case 알수없는값 => "알 수 없는 값입니다"73
case 키필요 => "키(문자열)가 필요합니다"74
case 콜론필요 => "콜론(:)이 필요합니다"75
case 닫는중괄호필요 => "닫는 중괄호(\})가 필요합니다"76
case 쉼표나닫는중괄호 => "쉼표(,)나 닫는 중괄호(\})가 필요합니다"77
case 닫는대괄호필요 => "닫는 대괄호(])가 필요합니다"78
case 쉼표나닫는대괄호 => "쉼표(,)나 닫는 대괄호(])가 필요합니다"79
case 잉여내용 => "값 뒤에 불필요한 내용이 있습니다"80
}81
}83
function 십육진인가(글자: string) -> bool {84
글자 in 십육진글자85
}87
function 낱말인가(글자들: Array<string>, 위치: int, 낱말: string) -> bool {88
let 길이 = 낱말.scalars().length89
위치 + 길이 <= 글자들.length && 글자들.slice(위치, 위치 + 길이).join("") == 낱말90
}92
// 위치는 백슬래시를 가리킵니다.93
function 이스케이프읽기(글자들: Array<string>, 위치: int) -> Result<읽은문자열, 구문오류> {94
if 위치 + 1 >= 글자들.length { return Err(구문오류 { 위치: 위치, 종류: 오류종류.문자열미종료 }) }95
let 표지 = 글자들[위치 + 1]96
if 표지 in 한글자이스케이프 { return Ok(읽은문자열 { 원문: "\\" + 표지, 다음: 위치 + 2 }) }97
if 표지 != "u" { return Err(구문오류 { 위치: 위치 + 1, 종류: 오류종류.잘못된이스케이프 }) }98
if 위치 + 5 >= 글자들.length { return Err(구문오류 { 위치: 위치, 종류: 오류종류.유니코드이스케이프 }) }99
let 틀린자리 = [for k in 0..<4 if !십육진인가(글자들[위치 + 2 + k]) => 위치 + 2 + k]100
if 틀린자리.length > 0 { return Err(구문오류 { 위치: 틀린자리[0], 종류: 오류종류.유니코드이스케이프 }) }101
let 십육진 = 글자들.slice(위치 + 2, 위치 + 6).join("")102
let 코드 = toIntRadix(십육진, 16) ?? -1103
let 문자 = fromCodePoint(코드) ?? ""104
// 제어문자, 따옴표, 백슬래시, 글자가 될 수 없는 코드는 풀면 문자열이 깨지므로 원문으로 둡니다.105
let 풀어도되는가 = 코드 >= 32 && 코드 != 34 && 코드 != 92 && 문자.byteLength() > 0106
let 원문 = if 풀어도되는가 { 문자 } else { "\\u" + 십육진 }107
Ok(읽은문자열 { 원문: 원문, 다음: 위치 + 6 })108
}110
// 위치는 여는 따옴표를 가리키고, 원문은 양쪽 따옴표를 포함합니다.111
function 문자열읽기(글자들: Array<string>, 위치: int) -> Result<읽은문자열, 구문오류> {112
let mut 부분들 = ["\""]113
let mut i = 위치 + 1114
while i < 글자들.length {115
let 글자 = 글자들[i]116
if 글자 == "\"" {117
부분들.push("\"")118
return Ok(읽은문자열 { 원문: 부분들.join(""), 다음: i + 1 })119
}120
if 글자 == "\\" {121
let 이스케이프 = 이스케이프읽기(글자들, i)?122
부분들.push(이스케이프.원문)123
i = 이스케이프.다음124
} else {125
// RFC 8259 는 U+0000..U+001F 를 문자열 안에 그대로 두지 못하게 합니다.126
if (글자.codePointAt(0) ?? 0) < 32 { return Err(구문오류 { 위치: i, 종류: 오류종류.제어문자 }) }127
부분들.push(글자)128
i += 1129
}130
}131
Err(구문오류 { 위치: 위치, 종류: 오류종류.문자열미종료 })132
}134
// -? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE][+-]?[0-9]+)? 를 받습니다. 앞자리 0 뒤의 숫자는 이 토큰에 넣지 않으므로 01 은 뒤쪽에서 오류가 됩니다.135
function 숫자읽기(글자들: Array<string>, 위치: int) -> Result<읽은값, 구문오류> {136
let 정수시작 = if 글자인가(글자들, 위치, "-") { 위치 + 1 } else { 위치 }137
if 정수시작 >= 글자들.length || !숫자인가(글자들[정수시작]) {138
return Err(구문오류 { 위치: 정수시작, 종류: 오류종류.숫자필요 })139
}140
let 정수끝 = if 글자들[정수시작] == "0" { 정수시작 + 1 } else { 연속끝(글자들, 정수시작, 숫자인가) }141
let 소수끝 = if 글자인가(글자들, 정수끝, ".") {142
let 끝 = 연속끝(글자들, 정수끝 + 1, 숫자인가)143
if 끝 == 정수끝 + 1 { return Err(구문오류 { 위치: 끝, 종류: 오류종류.소수점숫자필요 }) }144
끝145
} else {146
정수끝147
}148
let 지수끝 = if 글자인가(글자들, 소수끝, "e") || 글자인가(글자들, 소수끝, "E") {149
let 부호뒤 = if 글자인가(글자들, 소수끝 + 1, "+") || 글자인가(글자들, 소수끝 + 1, "-") { 소수끝 + 2 } else { 소수끝 + 1 }150
let 끝 = 연속끝(글자들, 부호뒤, 숫자인가)151
if 끝 == 부호뒤 { return Err(구문오류 { 위치: 끝, 종류: 오류종류.지수숫자필요 }) }152
끝153
} else {154
소수끝155
}156
Ok(읽은값 { 값: Json값.수(글자들.slice(위치, 지수끝).join("")), 다음: 지수끝 })157
}159
// 깊이는 공백을 건너뛰기 전에 검사하므로 너무깊음 오류의 위치는 공백 앞입니다.160
function 값읽기(글자들: Array<string>, 위치: int, 깊이: int) -> Result<읽은값, 구문오류> {161
if 깊이 > 최대깊이 { return Err(구문오류 { 위치: 위치, 종류: 오류종류.너무깊음 }) }162
let i = 연속끝(글자들, 위치, 공백인가)163
if i >= 글자들.length { return Err(구문오류 { 위치: i, 종류: 오류종류.값필요 }) }164
let 글자 = 글자들[i]165
match 글자 {166
case "\{" => 객체읽기(글자들, i, 깊이)167
case "[" => 배열읽기(글자들, i, 깊이)168
case "\"" => {169
let 읽음 = 문자열읽기(글자들, i)?170
Ok(읽은값 { 값: Json값.문자열(읽음.원문), 다음: 읽음.다음 })171
}172
case _ => {173
if 글자 == "-" || 숫자인가(글자) {174
숫자읽기(글자들, i)175
} else if 낱말인가(글자들, i, "true") {176
Ok(읽은값 { 값: Json값.참거짓(true), 다음: i + 4 })177
} else if 낱말인가(글자들, i, "false") {178
Ok(읽은값 { 값: Json값.참거짓(false), 다음: i + 5 })179
} else if 낱말인가(글자들, i, "null") {180
Ok(읽은값 { 값: Json값.없음, 다음: i + 4 })181
} else {182
Err(구문오류 { 위치: i, 종류: 오류종류.알수없는값 })183
}184
}185
}186
}188
function 객체읽기(글자들: Array<string>, 위치: int, 깊이: int) -> Result<읽은값, 구문오류> {189
let 첫자리 = 연속끝(글자들, 위치 + 1, 공백인가)190
if 글자인가(글자들, 첫자리, "\}") { return Ok(읽은값 { 값: Json값.객체([]), 다음: 첫자리 + 1 }) }191
let mut 멤버들: Array<멤버> = []192
let mut i = 첫자리193
while true {194
let 키자리 = 연속끝(글자들, i, 공백인가)195
if !글자인가(글자들, 키자리, "\"") { return Err(구문오류 { 위치: 키자리, 종류: 오류종류.키필요 }) }196
let 키 = 문자열읽기(글자들, 키자리)?197
let 콜론자리 = 연속끝(글자들, 키.다음, 공백인가)198
if !글자인가(글자들, 콜론자리, ":") { return Err(구문오류 { 위치: 콜론자리, 종류: 오류종류.콜론필요 }) }199
let 읽음 = 값읽기(글자들, 콜론자리 + 1, 깊이 + 1)?200
멤버들.push(멤버 { 키: 키.원문, 값: 읽음.값 })201
let 뒤 = 연속끝(글자들, 읽음.다음, 공백인가)202
if 뒤 >= 글자들.length { return Err(구문오류 { 위치: 뒤, 종류: 오류종류.닫는중괄호필요 }) }203
if 글자들[뒤] == "\}" { return Ok(읽은값 { 값: Json값.객체(멤버들), 다음: 뒤 + 1 }) }204
if 글자들[뒤] != "," { return Err(구문오류 { 위치: 뒤, 종류: 오류종류.쉼표나닫는중괄호 }) }205
i = 뒤 + 1206
}207
}209
function 배열읽기(글자들: Array<string>, 위치: int, 깊이: int) -> Result<읽은값, 구문오류> {210
let 첫자리 = 연속끝(글자들, 위치 + 1, 공백인가)211
if 글자인가(글자들, 첫자리, "]") { return Ok(읽은값 { 값: Json값.배열([]), 다음: 첫자리 + 1 }) }212
let mut 요소들: Array<Json값> = []213
let mut i = 첫자리214
while true {215
let 읽음 = 값읽기(글자들, i, 깊이 + 1)?216
요소들.push(읽음.값)217
let 뒤 = 연속끝(글자들, 읽음.다음, 공백인가)218
if 뒤 >= 글자들.length { return Err(구문오류 { 위치: 뒤, 종류: 오류종류.닫는대괄호필요 }) }219
if 글자들[뒤] == "]" { return Ok(읽은값 { 값: Json값.배열(요소들), 다음: 뒤 + 1 }) }220
if 글자들[뒤] != "," { return Err(구문오류 { 위치: 뒤, 종류: 오류종류.쉼표나닫는대괄호 }) }221
i = 뒤 + 1222
}223
}225
function 문서읽기(글자들: Array<string>) -> Result<Json값, 구문오류> {226
let 읽음 = 값읽기(글자들, 0, 0)?227
let 끝 = 연속끝(글자들, 읽음.다음, 공백인가)228
if 끝 < 글자들.length { Err(구문오류 { 위치: 끝, 종류: 오류종류.잉여내용 }) } else { Ok(읽음.값) }229
}231
function 들여쓰기(깊이: int) -> string {232
[for i in 0..<깊이 => " "].join("")233
}235
function 블록(여는: string, 닫는: string, 항목들: Array<string>, 깊이: int) -> string {236
if 항목들.length == 0 {237
여는 + 닫는238
} else {239
let 안쪽 = 들여쓰기(깊이 + 1)240
"{여는}\n{항목들.map(항목 => 안쪽 + 항목).join(",\n")}\n{들여쓰기(깊이)}{닫는}"241
}242
}244
function 직렬화(값: Json값, 깊이: int) -> string {245
match 값 {246
case 없음 => "null"247
case 참거짓(참인가) => if 참인가 { "true" } else { "false" }248
case 수(원문) | 문자열(원문) => 원문249
case 배열(요소들) => 블록("[", "]", 요소들.map(요소 => 직렬화(요소, 깊이 + 1)), 깊이)250
case 객체(멤버들) => 블록("\{", "\}", 멤버들.map(쌍 => 쌍.키 + ": " + 직렬화(쌍.값, 깊이 + 1)), 깊이)251
}252
}254
// 줄과 열은 1부터 셉니다. 위치가 입력 끝을 넘으면 끝에 둡니다.255
function 줄열(글자들: Array<string>, 위치: int) -> string {256
let 끝 = 위치.atMost(글자들.length)257
let 줄바꿈들 = [for i in 0..<끝 if 글자들[i] == "\n" => i]258
let 줄시작 = if 줄바꿈들.length > 0 { 줄바꿈들[줄바꿈들.length - 1] + 1 } else { 0 }259
"{줄바꿈들.length + 1}:{끝 - 줄시작 + 1}"260
}262
function 포맷(원본: string) -> string {263
let 글자들 = 원본.scalars()264
match 문서읽기(글자들) {265
case Ok(문서) => "<pre class=\"ok\">{본문이스케이프(직렬화(문서, 0))}</pre>"266
case Err(오류) => "<div class=\"err\">오류 {줄열(글자들, 오류.위치)}: {본문이스케이프(오류문구(오류.종류))}</div>"267
}268
}270
input() |> 포맷 |> printSHA256 b24d5c067504d4a7d6f50c6b5690373a29bac7f7063b99883d746d55ad84508e