code / tomo-json

Lines232 Tomo203 Markdown26 INI3
(230 lines)
1 # Base 64 encoding and decoding
2 use patterns
4 enum JSONDecodeResult(
5 Success(json:JSON)
6 Failure(reason:Text)
7 )
8 func invalid(text:Text -> JSONDecodeResult)
9 return Failure("Unrecognized JSON: $(text.quoted())")
11 func quote_text(text:Text -> Text)
12 return '"' ++ text.translate({
13 "\\": "\\\\",
14 '"': '\\"',
15 "\f": "\\f",
16 "\r": "\\r",
17 "\n": "\\n",
18 "\b": "\\b",
19 "\t": "\\t",
20 }) ++ '"'
22 enum JSON(
23 Object(items:{Text:JSON})
24 Array(items:[JSON])
25 Boolean(value:Bool)
26 String(text:Text)
27 InvalidUnicodeString(utf32:[Int32])
28 Number(n:Num)
29 Null
31 func encode(j:JSON -> Text)
32 when j is Object(items)
33 return "{" ++ ", ".join([
34 '$(quote_text(k)): $(v.encode())'
35 for k,v in items
36 ]) ++ "}"
37 is Array(items)
38 return "[" ++ ", ".join([item.encode() for item in items]) ++ "]"
39 is Boolean(value)
40 return (if value then "true" else "false")
41 is String(text)
42 return quote_text(text)
43 is InvalidUnicodeString(utf32)
44 return '"' ++ ((++: (
45 if text := Text.from_utf32([u])
46 text
47 else
48 "\\u$(u.hex(digits=4, prefix=no))"
49 ) for u in utf32) or "") ++ '"'
50 is Number(n)
51 return "$n"
52 is Null
53 return "null"
55 func pretty_print(j:JSON, max_line:Int=80, indent:Text=" ", current_indent:Text="" -> Text)
56 inline := j.encode()
57 if inline.length > max_line
58 next_indent := current_indent ++ indent
59 when j is Object(items)
60 return "{\n$next_indent" ++ ",\n$next_indent".join([
61 '$(quote_text(k)): $(v.pretty_print(max_line, indent, next_indent))'
62 for k,v in items
63 ]) ++ "\n$current_indent}"
64 is Array(items)
65 return "[\n$next_indent" ++ ",\n$next_indent".join([item.pretty_print(max_line, indent, next_indent) for item in items]) ++ "\n$current_indent]"
66 else pass
68 return inline
70 func add_codepoint(cur:JSON, codepoint:Int32 -> JSON)
71 when cur is String(str)
72 if byte_str := Text.from_utf32([codepoint])
73 return JSON.String(str ++ byte_str)
74 else
75 return JSON.InvalidUnicodeString(str.utf32() ++ [codepoint])
76 is InvalidUnicodeString(utf32)
77 return JSON.InvalidUnicodeString(utf32 ++ [codepoint])
78 else
79 fail("I expected this to be a String or InvalidUnicodeString, not: $cur")
81 func add_text(cur:JSON, text:Text -> JSON)
82 when cur is String(str)
83 return JSON.String(str ++ text)
84 is InvalidUnicodeString(utf32)
85 return JSON.InvalidUnicodeString(utf32 ++ text.utf32())
86 else
87 fail("I expected this to be a String or InvalidUnicodeString, not: $cur")
89 func parse_text(text:Text, remainder:&Text?=none, strict=no -> JSONDecodeResult)
90 if text.starts_with('"')
91 ret := JSON.String("")
92 pos := 2
93 escapes := {"n":"\n", "t":"\t", "r":"\r", '"':'"', "\\":"\\", "/":"/", "b":"\b", "f":"\f"}
94 while pos <= text.length
95 c := text[pos]!
96 if c == '"'
97 if remainder
98 remainder[] = text.from(pos + 1)
99 return Success(ret)
101 if c == "\\"
102 stop if pos + 1 > text.length
104 if esc := escapes[text[pos+1]!]
105 ret = ret.add_text(esc)
106 pos += 2
107 else if m := $Pat'u{4 hex}'.match(text, pos=pos + 1)
108 ret = ret.add_codepoint(Int32.parse(m.captures[1]!, 16)!)
109 pos += 1 + m.text.length
110 else
111 stop
112 else if c.utf32()[1]! <= 31
113 if strict
114 return JSONDecodeResult.invalid(text.from(pos))
115 ret = ret.add_text(c)
116 pos += 1
117 else
118 ret = ret.add_text(c)
119 pos += 1
121 if remainder
122 remainder[] = text
123 return JSONDecodeResult.invalid(text)
125 func parse(text:Text, remainder:&Text?=none, strict=no, max_depth=25 -> JSONDecodeResult)
126 if max_depth <= 0
127 return JSONDecodeResult.Failure("Maximum depth exceeded")
129 if text.starts_with("true", remainder)
130 return Success(JSON.Boolean(yes))
131 else if text.starts_with("false", remainder)
132 return Success(JSON.Boolean(no))
133 else if text.starts_with("null", remainder)
134 return Success(JSON.Null)
136 lower4 := text.to(4).lower()
137 if (
138 not strict or not (
139 lower4.starts_with("+") or
140 lower4.starts_with("nan") or
141 lower4.starts_with("inf") or
142 lower4.starts_with("-inf") or
143 lower4.starts_with("-nan") or
144 lower4.starts_with("0x") or
145 lower4.starts_with("-0x") or
146 lower4.starts_with("0o") or
147 lower4.starts_with("-0o") or
148 lower4.starts_with("0b") or
149 lower4.starts_with("-0b")
152 if n := Num.parse(text, remainder)
153 return Success(JSON.Number(n))
155 if text.starts_with('"')
156 return JSON.parse_text(text, remainder, strict=strict)
157 else if text.starts_with("[")
158 elements : &[JSON]
159 text = $Pat"{whitespace}".trim(text.from(2), right=no)
160 has_trailing_comma := no
161 repeat
162 when JSON.parse(text, &text, strict=strict, max_depth=max_depth-1) is Success(elem)
163 elements.insert(elem)
164 has_trailing_comma = no
165 else stop
167 if delim := $Pat'{0+ ws},{0+ ws}'.match(text)
168 text = text.from(delim.text.length + 1)
169 has_trailing_comma = yes
170 else stop
172 if has_trailing_comma and strict
173 return JSONDecodeResult.invalid(text)
175 if terminator := $Pat'{0+ ws}]'.match(text)
176 if remainder
177 remainder[] = text.from(terminator.text.length + 1)
178 return Success(JSON.Array(elements))
179 else if text.starts_with("{")
180 object : &{Text:JSON}
181 text = $Pat"{whitespace}".trim(text.from(2), right=no)
182 has_trailing_comma := no
183 repeat
184 key_text := text
185 when JSON.parse_text(text, &text, strict=strict) is Success(key_json)
186 key := key_json.String or return JSONDecodeResult.invalid(key_text)
187 if separator := $Pat'{0+ ws}:{0+ ws}'.match(text)
188 text = text.from(separator.text.length + 1)
189 else
190 return JSONDecodeResult.invalid(text)
192 when JSON.parse(text, &text, strict=strict, max_depth=max_depth-1) is Success(value)
193 object[key.text] = value
194 has_trailing_comma = no
195 else
196 return JSONDecodeResult.invalid(text)
197 else stop
199 if delim := $Pat'{0+ ws},{0+ ws}'.match(text)
200 text = text.from(delim.text.length + 1)
201 has_trailing_comma = yes
202 else stop
204 if has_trailing_comma and strict
205 return JSONDecodeResult.invalid(text)
207 if terminator := $Pat'{0+ ws}{1}}'.match(text)
208 if remainder
209 remainder[] = text.from(terminator.text.length + 1)
210 return Success(JSON.Object(object))
212 return JSONDecodeResult.invalid(text)
214 func main(input=(/dev/stdin), pretty_print:Bool = no, strict:Bool = no, max_depth=100)
215 text := $Pat"{whitespace}".trim(input.read() or exit("Invalid file: $input"))
216 while text.length > 0
217 when JSON.parse(text, remainder=&text, strict=strict, max_depth=max_depth) is Success(json)
218 if pretty_print
219 say(json.pretty_print())
220 else
221 say(json.encode())
223 if strict
224 text = $Pat"{whitespace}".trim(text)
225 if text.length > 0
226 exit(code=1)
227 exit(code=0)
229 is Failure(msg)
230 exit("\033[31;1m$msg\033[m", code=1)