Download
230 lines 8.5 KiBTomoModified 2025-12-06 raw
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)