123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 | # Base 64 encoding and decoding
use patterns
enum JSONDecodeResult(
Success(json:JSON)
Failure(reason:Text)
)
func invalid(text:Text -> JSONDecodeResult)
return Failure("Unrecognized JSON: $(text.quoted())")
func quote_text(text:Text -> Text)
return '"' ++ text.translate({
"\\": "\\\\",
'"': '\\"',
"\f": "\\f",
"\r": "\\r",
"\n": "\\n",
"\b": "\\b",
"\t": "\\t",
}) ++ '"'
enum JSON(
Object(items:{Text:JSON})
Array(items:[JSON])
Boolean(value:Bool)
String(text:Text)
InvalidUnicodeString(utf32:[Int32])
Number(n:Num)
Null
)
func encode(j:JSON -> Text)
when j is Object(items)
return "{" ++ ", ".join([
'$(quote_text(k)): $(v.encode())'
for k,v in items
]) ++ "}"
is Array(items)
return "[" ++ ", ".join([item.encode() for item in items]) ++ "]"
is Boolean(value)
return (if value then "true" else "false")
is String(text)
return quote_text(text)
is InvalidUnicodeString(utf32)
return '"' ++ ((++: (
if text := Text.from_utf32([u])
text
else
"\\u$(u.hex(digits=4, prefix=no))"
) for u in utf32) or "") ++ '"'
is Number(n)
return "$n"
is Null
return "null"
func pretty_print(j:JSON, max_line:Int=80, indent:Text=" ", current_indent:Text="" -> Text)
inline := j.encode()
if inline.length > max_line
next_indent := current_indent ++ indent
when j is Object(items)
return "{\n$next_indent" ++ ",\n$next_indent".join([
'$(quote_text(k)): $(v.pretty_print(max_line, indent, next_indent))'
for k,v in items
]) ++ "\n$current_indent}"
is Array(items)
return "[\n$next_indent" ++ ",\n$next_indent".join([item.pretty_print(max_line, indent, next_indent) for item in items]) ++ "\n$current_indent]"
else pass
return inline
func add_codepoint(cur:JSON, codepoint:Int32 -> JSON)
when cur is String(str)
if byte_str := Text.from_utf32([codepoint])
return JSON.String(str ++ byte_str)
else
return JSON.InvalidUnicodeString(str.utf32() ++ [codepoint])
is InvalidUnicodeString(utf32)
return JSON.InvalidUnicodeString(utf32 ++ [codepoint])
else
fail("I expected this to be a String or InvalidUnicodeString, not: $cur")
func add_text(cur:JSON, text:Text -> JSON)
when cur is String(str)
return JSON.String(str ++ text)
is InvalidUnicodeString(utf32)
return JSON.InvalidUnicodeString(utf32 ++ text.utf32())
else
fail("I expected this to be a String or InvalidUnicodeString, not: $cur")
func parse_text(text:Text, remainder:&Text?=none, strict=no -> JSONDecodeResult)
if text.starts_with('"')
ret := JSON.String("")
pos := 2
escapes := {"n":"\n", "t":"\t", "r":"\r", '"':'"', "\\":"\\", "/":"/", "b":"\b", "f":"\f"}
while pos <= text.length
c := text[pos]!
if c == '"'
if remainder
remainder[] = text.from(pos + 1)
return Success(ret)
if c == "\\"
stop if pos + 1 > text.length
if esc := escapes[text[pos+1]!]
ret = ret.add_text(esc)
pos += 2
else if m := $Pat'u{4 hex}'.match(text, pos=pos + 1)
ret = ret.add_codepoint(Int32.parse(m.captures[1]!, 16)!)
pos += 1 + m.text.length
else
stop
else if c.utf32()[1]! <= 31
if strict
return JSONDecodeResult.invalid(text.from(pos))
ret = ret.add_text(c)
pos += 1
else
ret = ret.add_text(c)
pos += 1
if remainder
remainder[] = text
return JSONDecodeResult.invalid(text)
func parse(text:Text, remainder:&Text?=none, strict=no, max_depth=25 -> JSONDecodeResult)
if max_depth <= 0
return JSONDecodeResult.Failure("Maximum depth exceeded")
if text.starts_with("true", remainder)
return Success(JSON.Boolean(yes))
else if text.starts_with("false", remainder)
return Success(JSON.Boolean(no))
else if text.starts_with("null", remainder)
return Success(JSON.Null)
lower4 := text.to(4).lower()
if (
not strict or not (
lower4.starts_with("+") or
lower4.starts_with("nan") or
lower4.starts_with("inf") or
lower4.starts_with("-inf") or
lower4.starts_with("-nan") or
lower4.starts_with("0x") or
lower4.starts_with("-0x") or
lower4.starts_with("0o") or
lower4.starts_with("-0o") or
lower4.starts_with("0b") or
lower4.starts_with("-0b")
)
)
if n := Num.parse(text, remainder)
return Success(JSON.Number(n))
if text.starts_with('"')
return JSON.parse_text(text, remainder, strict=strict)
else if text.starts_with("[")
elements : &[JSON]
text = $Pat"{whitespace}".trim(text.from(2), right=no)
has_trailing_comma := no
repeat
when JSON.parse(text, &text, strict=strict, max_depth=max_depth-1) is Success(elem)
elements.insert(elem)
has_trailing_comma = no
else stop
if delim := $Pat'{0+ ws},{0+ ws}'.match(text)
text = text.from(delim.text.length + 1)
has_trailing_comma = yes
else stop
if has_trailing_comma and strict
return JSONDecodeResult.invalid(text)
if terminator := $Pat'{0+ ws}]'.match(text)
if remainder
remainder[] = text.from(terminator.text.length + 1)
return Success(JSON.Array(elements))
else if text.starts_with("{")
object : &{Text:JSON}
text = $Pat"{whitespace}".trim(text.from(2), right=no)
has_trailing_comma := no
repeat
key_text := text
when JSON.parse_text(text, &text, strict=strict) is Success(key_json)
key := key_json.String or return JSONDecodeResult.invalid(key_text)
if separator := $Pat'{0+ ws}:{0+ ws}'.match(text)
text = text.from(separator.text.length + 1)
else
return JSONDecodeResult.invalid(text)
when JSON.parse(text, &text, strict=strict, max_depth=max_depth-1) is Success(value)
object[key.text] = value
has_trailing_comma = no
else
return JSONDecodeResult.invalid(text)
else stop
if delim := $Pat'{0+ ws},{0+ ws}'.match(text)
text = text.from(delim.text.length + 1)
has_trailing_comma = yes
else stop
if has_trailing_comma and strict
return JSONDecodeResult.invalid(text)
if terminator := $Pat'{0+ ws}{1}}'.match(text)
if remainder
remainder[] = text.from(terminator.text.length + 1)
return Success(JSON.Object(object))
return JSONDecodeResult.invalid(text)
func main(input=(/dev/stdin), pretty_print:Bool = no, strict:Bool = no, max_depth=100)
text := $Pat"{whitespace}".trim(input.read() or exit("Invalid file: $input"))
while text.length > 0
when JSON.parse(text, remainder=&text, strict=strict, max_depth=max_depth) is Success(json)
if pretty_print
say(json.pretty_print())
else
say(json.encode())
if strict
text = $Pat"{whitespace}".trim(text)
if text.length > 0
exit(code=1)
exit(code=0)
is Failure(msg)
exit("\033[31;1m$msg\033[m", code=1)
|