format.tpz
// Korean identifiers are used throughout; English glosses are added below each comment.
// Key identifiers: 오류=Error, 조각=Piece (parse fragment), 위치=position, 메시지=message,
// 출력=output, 다음=next index, 최대깊이=maxDepth, 글자화=toScalars, 공백인가=isWhitespace,
// 공백건너뛰기=skipWhitespace, 숫자인가=isDigit, 십육진인가=isHexDigit, 매치=match,
// 들여쓰기=indent, 글자정리=escapeChar, 이스케이프HTML=escapeHTML, 문자열읽기=readString,
// 숫자읽기=readNumber, 값읽기=readValue, 객체읽기=readObject, 배열읽기=readArray,
// 줄열=lineCol, 포맷=format; 글자들=scalars, 색인=index, 원문=raw, 줄=line, 열=col.
// JSON 포매터/검증기. Topaz로만 작성한 완전한 재귀 하강 JSON 파서입니다.
// 전체 JSON 문법을 검증하고 2칸 들여쓰기로 포맷합니다. 구문 오류가 나면
// line:column 형식으로 한국어 메시지를 보고합니다. 숫자 토큰은 그대로 보존합니다. 숫자는
// 텍스트 그대로 보존하며 부동소수점으로 재해석하거나 반올림하지 않습니다. 문자열 토큰도 보존하되, 안전한
// \uXXXX 이스케이프(" 나 \ 가 아닌 출력 가능한 스칼라)는 해당 문자로 디코딩합니다.
// 순수하고 결정적입니다. 같은 입력은 인터프리터와 네이티브/wasm 빌드에서 같은 출력을 냅니다.
// Validates the full JSON grammar and pretty-prints with 2-space indentation. On a syntax
// error, reports a Korean message in line:column form. Number tokens are preserved verbatim:
// numbers are preserved as text, with no floating-point reinterpretation or rounding. String tokens are
// preserved too, except safe \uXXXX escapes (printable scalars other than " or \) are decoded
// to the actual character. Pure and deterministic: the same input yields the same output under
// the interpreter and the native/wasm builds.
type 오류 = { 위치: int, 메시지: string }
type 조각 = { 출력: string, 다음: int }
// 닫힌 JSON 리터럴 토큰. 문자열/숫자 원문과 오류 메시지는 입력·출력 경계라 string으로 둔다.
// Closed JSON literal tokens. Raw strings/numbers and error messages stay string at the boundary.
type 제이슨리터럴 = "true" | "false" | "null"
let 최대깊이 = 200
function 리터럴길이(값: 제이슨리터럴) -> int {
match 값 {
case "true" => 4
case "false" => 5
case "null" => 4
}
}
function 리터럴조각(값: 제이슨리터럴, 색인: int) -> 조각 {
{ 출력: 값, 다음: 색인 + 리터럴길이(값) }
}
function 글자화(s: string) -> Array<string> { s.scalars() }
function 공백인가(글: string) -> bool {
글 == " " || 글 == "\n" || 글 == "\t" || 글 == "\r"
}
function 공백건너뛰기(글자들: Array<string>, 색인: int) -> int {
let mut i = 색인
while i < 글자들.length && 공백인가(글자들[i]) { i = i + 1 }
i
}
function 숫자인가(글: string) -> bool {
글 == "0" || 글 == "1" || 글 == "2" || 글 == "3" || 글 == "4"
|| 글 == "5" || 글 == "6" || 글 == "7" || 글 == "8" || 글 == "9"
}
function 십육진인가(글: string) -> bool {
숫자인가(글) || 글 == "a" || 글 == "b" || 글 == "c" || 글 == "d" || 글 == "e" || 글 == "f"
|| 글 == "A" || 글 == "B" || 글 == "C" || 글 == "D" || 글 == "E" || 글 == "F"
}
function 매치(글자들: Array<string>, 색인: int, 낱말: string) -> bool {
let w = 글자화(낱말)
if 색인 + w.length > 글자들.length { return false }
let mut k = 0
while k < w.length {
if 글자들[색인 + k] != w[k] { return false }
k = k + 1
}
true
}
function 들여쓰기(깊이: int) -> string {
let mut s = ""
let mut i = 0
while i < 깊이 { s = s + " "; i = i + 1 }
s
}
function 글자정리(글: string) -> string {
if 글 == "&" { "&" } else if 글 == "<" { "<" } else if 글 == ">" { ">" } else { 글 }
}
function 이스케이프HTML(s: string) -> string {
let mut 출력 = ""
for 글 in s.scalars() { 출력 = 출력 + 글자정리(글) }
출력
}
// JSON 문자열을 읽습니다. 여는 따옴표부터 닫는 따옴표까지 이스케이프를 검증하며 텍스트는 보존합니다.
// Reads a JSON string: validates escapes from opening to closing quote, preserving the text,
// 단 안전한 \uXXXX 이스케이프는 해당 문자로 디코딩합니다(아래 \u 블록 참고).
// except safe \uXXXX escapes, which are decoded to the actual character (see the \u block below).
function 문자열읽기(글자들: Array<string>, 색인: int) -> Result<조각, 오류> {
let mut i = 색인 + 1
let mut 원문 = "\""
while i < 글자들.length {
let c = 글자들[i]
if c == "\"" {
return Ok({ 출력: 원문 + "\"", 다음: i + 1 })
} else if c == "\\" {
if i + 1 >= 글자들.length { return Err({ 위치: i, 메시지: "문자열이 끝나지 않았습니다" }) }
let e = 글자들[i + 1]
if e == "\"" || e == "\\" || e == "/" || e == "b" || e == "f" || e == "n" || e == "r" || e == "t" {
원문 = 원문 + c + e
i = i + 2
} else if e == "u" {
if i + 5 >= 글자들.length { return Err({ 위치: i, 메시지: "\\u 뒤에 16진수 4자리가 필요합니다" }) }
let mut k = 0
while k < 4 {
if !십육진인가(글자들[i + 2 + k]) {
return Err({ 위치: i + 2 + k, 메시지: "\\u 뒤에 16진수 4자리가 필요합니다" })
}
k = k + 1
}
// JSON 문자열에서 안전할 때 \uXXXX 이스케이프를 실제 문자로 디코딩합니다.
// Decode a \uXXXX escape to its actual character when it is safe to do so in a JSON string:
// 즉 `"`(34) 나 `\`(92) 가 아닌 출력 가능한 스칼라. 제어문자, 따옴표,
// that is, a printable scalar other than `"`(34) or `\`(92). Control chars, quotes,
// 백슬래시, 잘못된 스칼라(고립 대리쌍)는 이스케이프 상태로 둡니다. 이 앱이 끌어낸 컴파일러
// backslashes, and invalid scalars (lone surrogates) stay escaped. Chains the compiler
// 기능을 연결합니다. toIntRadix(hex, 16) 다음 fromCodePoint(codepoint).
// features this app drove: toIntRadix(hex, 16) then fromCodePoint(codepoint).
let 십육 = 글자들[i + 2] + 글자들[i + 3] + 글자들[i + 4] + 글자들[i + 5]
let 코드 = toIntRadix(십육, 16) ?? -1
let 문자 = fromCodePoint(코드) ?? ""
if 코드 >= 32 && 코드 != 34 && 코드 != 92 && 문자.byteLength() > 0 {
원문 = 원문 + 문자
} else {
원문 = 원문 + "\\u" + 십육
}
i = i + 6
} else {
return Err({ 위치: i + 1, 메시지: "잘못된 이스케이프 문자입니다" })
}
} else {
// RFC 8259. 원시 제어문자 U+0000..U+001F는 문자열 안에서 반드시 이스케이프해야 합니다.
// RFC 8259: raw control chars U+0000..U+001F must be escaped inside a string.
let 코드 = c.codePointAt(0) ?? 0
if 코드 < 32 { return Err({ 위치: i, 메시지: "문자열 안의 제어문자는 이스케이프해야 합니다" }) }
원문 = 원문 + c
i = i + 1
}
}
Err({ 위치: 색인, 메시지: "문자열이 끝나지 않았습니다" })
}
// JSON 숫자. -? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE][+-]?[0-9]+)? 형식이며 텍스트는 보존합니다.
// JSON number of the form -? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE][+-]?[0-9]+)?, text preserved.
function 숫자읽기(글자들: Array<string>, 색인: int) -> Result<조각, 오류> {
let mut i = 색인
if i < 글자들.length && 글자들[i] == "-" { i = i + 1 }
// RFC 8259 정수부. `0` 단독, 또는 `[1-9][0-9]*`. 앞자리 0(01, 00)은 잘못된 값입니다.
// RFC 8259 integer part: bare `0`, or `[1-9][0-9]*`. Leading zeros (01, 00) are invalid.
if i >= 글자들.length || !숫자인가(글자들[i]) { return Err({ 위치: i, 메시지: "숫자가 필요합니다" }) }
if 글자들[i] == "0" {
i = i + 1
} else {
while i < 글자들.length && 숫자인가(글자들[i]) { i = i + 1 }
}
if i < 글자들.length && 글자들[i] == "." {
i = i + 1
let 소수시작 = i
while i < 글자들.length && 숫자인가(글자들[i]) { i = i + 1 }
if i == 소수시작 { return Err({ 위치: i, 메시지: "소수점 뒤에 숫자가 필요합니다" }) }
}
if i < 글자들.length && (글자들[i] == "e" || 글자들[i] == "E") {
i = i + 1
if i < 글자들.length && (글자들[i] == "+" || 글자들[i] == "-") { i = i + 1 }
let 지수시작 = i
while i < 글자들.length && 숫자인가(글자들[i]) { i = i + 1 }
if i == 지수시작 { return Err({ 위치: i, 메시지: "지수 뒤에 숫자가 필요합니다" }) }
}
let 원문 = 글자들.slice(색인, i).join("")
Ok({ 출력: 원문, 다음: i })
}
function 값읽기(글자들: Array<string>, 색인: int, 깊이: int) -> Result<조각, 오류> {
if 깊이 > 최대깊이 {
return Err({ 위치: 색인, 메시지: "중첩이 너무 깊습니다 (최대 200단계)" })
}
let i = 공백건너뛰기(글자들, 색인)
if i >= 글자들.length { return Err({ 위치: i, 메시지: "값이 필요합니다" }) }
let c = 글자들[i]
if c == "\{" { return 객체읽기(글자들, i, 깊이) }
if c == "[" { return 배열읽기(글자들, i, 깊이) }
if c == "\"" { return 문자열읽기(글자들, i) }
if c == "-" || 숫자인가(c) { return 숫자읽기(글자들, i) }
if 매치(글자들, i, "true") { return Ok(리터럴조각("true", i)) }
if 매치(글자들, i, "false") { return Ok(리터럴조각("false", i)) }
if 매치(글자들, i, "null") { return Ok(리터럴조각("null", i)) }
Err({ 위치: i, 메시지: "알 수 없는 값입니다" })
}
function 객체읽기(글자들: Array<string>, 색인: int, 깊이: int) -> Result<조각, 오류> {
let mut j = 공백건너뛰기(글자들, 색인 + 1)
if j < 글자들.length && 글자들[j] == "\}" { return Ok({ 출력: "\{\}", 다음: j + 1 }) }
let mut 출력 = "\{\n"
let mut 처음 = true
while true {
j = 공백건너뛰기(글자들, j)
if j >= 글자들.length || 글자들[j] != "\"" { return Err({ 위치: j, 메시지: "키(문자열)가 필요합니다" }) }
let 키 = 문자열읽기(글자들, j)?
j = 공백건너뛰기(글자들, 키.다음)
if j >= 글자들.length || 글자들[j] != ":" { return Err({ 위치: j, 메시지: "콜론(:)이 필요합니다" }) }
let 값 = 값읽기(글자들, j + 1, 깊이 + 1)?
if !처음 { 출력 = 출력 + ",\n" }
출력 = 출력 + 들여쓰기(깊이 + 1) + 키.출력 + ": " + 값.출력
처음 = false
j = 공백건너뛰기(글자들, 값.다음)
if j >= 글자들.length { return Err({ 위치: j, 메시지: "닫는 중괄호(\})가 필요합니다" }) }
if 글자들[j] == "\}" { return Ok({ 출력: 출력 + "\n" + 들여쓰기(깊이) + "\}", 다음: j + 1 }) }
if 글자들[j] != "," { return Err({ 위치: j, 메시지: "쉼표(,)나 닫는 중괄호(\})가 필요합니다" }) }
j = j + 1
}
}
function 배열읽기(글자들: Array<string>, 색인: int, 깊이: int) -> Result<조각, 오류> {
let mut j = 공백건너뛰기(글자들, 색인 + 1)
if j < 글자들.length && 글자들[j] == "]" { return Ok({ 출력: "[]", 다음: j + 1 }) }
let mut 출력 = "[\n"
let mut 처음 = true
while true {
let 값 = 값읽기(글자들, j, 깊이 + 1)?
if !처음 { 출력 = 출력 + ",\n" }
출력 = 출력 + 들여쓰기(깊이 + 1) + 값.출력
처음 = false
j = 공백건너뛰기(글자들, 값.다음)
if j >= 글자들.length { return Err({ 위치: j, 메시지: "닫는 대괄호(])가 필요합니다" }) }
if 글자들[j] == "]" { return Ok({ 출력: 출력 + "\n" + 들여쓰기(깊이) + "]", 다음: j + 1 }) }
if 글자들[j] != "," { return Err({ 위치: j, 메시지: "쉼표(,)나 닫는 대괄호(])가 필요합니다" }) }
j = j + 1
}
}
// 스칼라 색인을 "line:col"(1부터 시작)로 바꿉니다. 해당 위치 앞의 개행 수를 셉니다.
// Converts a scalar index to "line:col" (1-based) by counting newlines before that position.
function 줄열(글자들: Array<string>, 위치: int) -> string {
let mut 줄 = 1
let mut 열 = 1
let mut i = 0
while i < 위치 && i < 글자들.length {
if 글자들[i] == "\n" { 줄 = 줄 + 1; 열 = 1 } else { 열 = 열 + 1 }
i = i + 1
}
"{줄}:{열}"
}
function 포맷(원본: string) -> string {
let 글자들 = 글자화(원본)
match 값읽기(글자들, 0, 0) {
case Ok(조) => {
let 끝 = 공백건너뛰기(글자들, 조.다음)
if 끝 < 글자들.length {
let 위치 = 줄열(글자들, 끝)
"<div class=\"err\">오류 {위치}: 값 뒤에 불필요한 내용이 있습니다</div>"
} else {
"<pre class=\"ok\">" + 이스케이프HTML(조.출력) + "</pre>"
}
}
case Err(오) => {
let 위치 = 줄열(글자들, 오.위치)
"<div class=\"err\">오류 " + 위치 + ": " + 이스케이프HTML(오.메시지) + "</div>"
}
}
}
print(포맷(input()))