slow.monster

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// 수와 문자열은 원문 토큰을 그대로 담고, 들여쓰기는 직렬화 단계에서만 붙입니다.
8enum Json값 { 없음, 참거짓(bool), 수(string), 문자열(string), 배열(Array<Json값>), 객체(Array<멤버>) }
9// 키는 따옴표까지 포함한 원문입니다.
10record 멤버 { 키: string, 값: Json값 }
11record 읽은값 { 값: Json값, 다음: int }
12record 읽은문자열 { 원문: string, 다음: int }
14enum 오류종류 {
15 문자열미종료, 유니코드이스케이프, 잘못된이스케이프, 제어문자,
16 숫자필요, 소수점숫자필요, 지수숫자필요,
17 너무깊음, 값필요, 알수없는값,
18 키필요, 콜론필요, 닫는중괄호필요, 쉼표나닫는중괄호,
19 닫는대괄호필요, 쉼표나닫는대괄호, 잉여내용
21record 구문오류 { 위치: int, 종류: 오류종류 }
23let 최대깊이 = 200
24let 십육진글자 = 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",
28let 한글자이스케이프 = Set.of("\"", "\\", "/", "b", "f", "n", "r", "t")
30// 공용 도우미 시작 (본문이스케이프 공백인가 숫자인가 연속끝 글자인가)
31// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.
33// 요소 본문에 넣는 사용자 입력에 씁니다. 큰따옴표를 그대로 두므로 속성 값에는 이스케이프를 씁니다.
34function 본문이스케이프(원문: string) -> string {
35 원문.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
38function 공백인가(글자: string) -> bool {
39 글자 == " " || 글자 == "\t" || 글자 == "\n" || 글자 == "\r"
42function 숫자인가(글자: string) -> bool {
43 let 코드 = 글자.codePointAt(0) ?? 0
44 코드 >= 48 && 코드 <= 57
47// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.
48function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {
49 let mut 끝 = 시작
50 while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }
51 끝
54// 위치가 글자들 끝을 넘으면 거짓입니다.
55function 글자인가(글자들: Array<string>, 위치: int, 글자: string) -> bool {
56 위치 < 글자들.length && 글자들[위치] == 글자
59// 공용 도우미 끝
61function 오류문구(종류: 오류종류) -> 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 }
83function 십육진인가(글자: string) -> bool {
84 글자 in 십육진글자
87function 낱말인가(글자들: Array<string>, 위치: int, 낱말: string) -> bool {
88 let 길이 = 낱말.scalars().length
89 위치 + 길이 <= 글자들.length && 글자들.slice(위치, 위치 + 길이).join("") == 낱말
92// 위치는 백슬래시를 가리킵니다.
93function 이스케이프읽기(글자들: 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) ?? -1
103 let 문자 = fromCodePoint(코드) ?? ""
104 // 제어문자, 따옴표, 백슬래시, 글자가 될 수 없는 코드는 풀면 문자열이 깨지므로 원문으로 둡니다.
105 let 풀어도되는가 = 코드 >= 32 && 코드 != 34 && 코드 != 92 && 문자.byteLength() > 0
106 let 원문 = if 풀어도되는가 { 문자 } else { "\\u" + 십육진 }
107 Ok(읽은문자열 { 원문: 원문, 다음: 위치 + 6 })
110// 위치는 여는 따옴표를 가리키고, 원문은 양쪽 따옴표를 포함합니다.
111function 문자열읽기(글자들: Array<string>, 위치: int) -> Result<읽은문자열, 구문오류> {
112 let mut 부분들 = ["\""]
113 let mut i = 위치 + 1
114 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 += 1
129 }
130 }
131 Err(구문오류 { 위치: 위치, 종류: 오류종류.문자열미종료 })
134// -? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE][+-]?[0-9]+)? 를 받습니다. 앞자리 0 뒤의 숫자는 이 토큰에 넣지 않으므로 01 은 뒤쪽에서 오류가 됩니다.
135function 숫자읽기(글자들: 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("")), 다음: 지수끝 })
159// 깊이는 공백을 건너뛰기 전에 검사하므로 너무깊음 오류의 위치는 공백 앞입니다.
160function 값읽기(글자들: 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 }
188function 객체읽기(글자들: 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 = 뒤 + 1
206 }
209function 배열읽기(글자들: 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 = 뒤 + 1
222 }
225function 문서읽기(글자들: Array<string>) -> Result<Json값, 구문오류> {
226 let 읽음 = 값읽기(글자들, 0, 0)?
227 let 끝 = 연속끝(글자들, 읽음.다음, 공백인가)
228 if 끝 < 글자들.length { Err(구문오류 { 위치: 끝, 종류: 오류종류.잉여내용 }) } else { Ok(읽음.값) }
231function 들여쓰기(깊이: int) -> string {
232 [for i in 0..<깊이 => " "].join("")
235function 블록(여는: string, 닫는: string, 항목들: Array<string>, 깊이: int) -> string {
236 if 항목들.length == 0 {
237 여는 + 닫는
238 } else {
239 let 안쪽 = 들여쓰기(깊이 + 1)
240 "{여는}\n{항목들.map(항목 => 안쪽 + 항목).join(",\n")}\n{들여쓰기(깊이)}{닫는}"
241 }
244function 직렬화(값: 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 }
254// 줄과 열은 1부터 셉니다. 위치가 입력 끝을 넘으면 끝에 둡니다.
255function 줄열(글자들: 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}"
262function 포맷(원본: string) -> string {
263 let 글자들 = 원본.scalars()
264 match 문서읽기(글자들) {
265 case Ok(문서) => "<pre class=\"ok\">{본문이스케이프(직렬화(문서, 0))}</pre>"
266 case Err(오류) => "<div class=\"err\">오류 {줄열(글자들, 오류.위치)}: {본문이스케이프(오류문구(오류.종류))}</div>"
267 }
270input() |> 포맷 |> print

SHA256 b24d5c067504d4a7d6f50c6b5690373a29bac7f7063b99883d746d55ad84508e