convert.tpz
// Unicode converter. Turns space-separated decimal code points into text. Written in Topaz.
// Key identifiers: 줄정리=escapeLine, 파싱=parseToken, 변환=convert, 줄=line, 텍스트=text,
// 토=token, 값=value, 문자=char, 파싱결과=parseOutcome (유효=valid / 무효=invalid),
// 누적=accumulator (결과문자=resultChars / 무효=invalidText / 무효개수=invalidCount),
// 누=acc, 판정=verdict, 결과=result.
// 유니코드 변환기. 공백으로 구분된 10진수 코드포인트를 텍스트로 바꿉니다. Topaz로 작성했습니다.
// 예 "44032 110 111" -> "가no". 각 토큰을 파싱하고(toInt) 문자로 만듭니다
// E.g. "44032 110 111" -> "가no". Parses each token (toInt) and builds a char
// (fromCodePoint). 숫자가 아니거나 유효한 유니코드 스칼라가 아닌 토큰은
// (fromCodePoint). Tokens that aren't numbers or aren't valid Unicode scalars are
// 무효로 표시됩니다(조용히 버리지 않습니다). 이 기능은 `fromCodePoint`가 반드시 필요합니다. 임의의 문자를
// flagged invalid (not silently dropped). This needs `fromCodePoint`: making arbitrary chars
// 코드로 만드는 것은 룩업 테이블로는 불가능합니다. 순수하고 결정적입니다.
// from codes is impossible with a lookup table. Pure and deterministic.
// 파싱 성공 여부를 담는 합 타입. 빈 문자열 센티널 대신 명시적 변형으로 표현합니다.
// Sum type for a token's parse outcome, replacing the empty-string sentinel.
enum 파싱결과 { 유효(string), 무효(string) }
// 누적 상태: 만들어진 문자열, 무효 토큰 나열, 무효 개수.
// Accumulator: built chars, listed invalid tokens, invalid count.
record 누적 { 결과문자: string, 무효: string, 무효개수: int }
function 줄정리(줄: string) -> string {
// & 를 가장 먼저 바꿔야 </> 가 도입한 & 가 다시 이스케이프되지 않습니다.
// Replace & first so the & introduced by </> is not re-escaped.
줄.replace("&", "&").replace("<", "<").replace(">", ">")
}
function 파싱(토: string) -> 파싱결과 {
// toInt과 fromCodePoint을 flatMap으로 잇습니다. 둘 중 하나라도 None이면 무효. 센티널이 없습니다.
// Chain toInt and fromCodePoint with flatMap. If either step is None, the token is invalid. No sentinels.
match toInt(토).flatMap(fromCodePoint) {
case Some(문자) => 파싱결과.유효(문자)
case None => 파싱결과.무효(토)
}
}
function 변환(텍스트: string) -> string {
// 줄바꿈을 공백으로 편 뒤 토큰화하고, 각 토큰을 파싱해 누적으로 접습니다.
// Flatten newlines to spaces, tokenize, parse each token, fold into the accumulator.
let 상태 = 텍스트.replace("\n", " ").split(" ")
.filter(토 => 토.byteLength() > 0)
.map(파싱)
.reduce(누적 { 결과문자: "", 무효: "", 무효개수: 0 }, (누, 판정) => match 판정 {
case 유효(c) => 누적 { ...누, 결과문자: 누.결과문자 + c }
case 무효(t) => 누적 { ...누, 무효: 누.무효 + t + " ", 무효개수: 누.무효개수 + 1 }
})
// 무효부는 무효 토큰이 있을 때만 붙는 블록입니다. 순수 if/else 식으로 계산해 마지막에 한 번만 이어붙입니다.
// The invalid block is appended only when there are invalid tokens. Compute it with a pure
// if/else expression, then interpolate everything in a single final string.
let 무효부 = if 상태.무효개수 > 0 {
"<div class=\"bad\">무효 {상태.무효개수}개: {줄정리(상태.무효)}</div>"
} else {
""
}
"<div class=\"uni\"><div class=\"out\">{줄정리(상태.결과문자)}</div>{무효부}</div>"
}
input() |> 변환 |> print