1# json - reading and writing JSON.2#3# Unmarshal turns the text into the values of the language, an object into a4# map and an array into a list, and Marshal writes them back:5#6# json = import("encoding/json")7#8# # A raw string, or the braces would be read as interpolation and the9# # quotes would need escaping.10# v = json.Unmarshal(`{"name": "tau", "tags": [1, 2]}`)11# println(v["name"], v["tags"][0])12# println(json.MarshalIndent(v, " "))13#14# What it reads is JSON and nothing more: no trailing commas, no comments, no15# single quotes. A text it will not take comes back as an error saying at16# which line and column it gave up. An escape of the form \uXXXX is kept as17# it was written rather than turned into the character it names.1819spaces = "\t\n\v\f\r "2021# --- tiny char helpers (avoid external deps) ---22containsChar = fn(set, ch) {23 for i = 0; i < len(set); ++i {24 if slice(set, i, i + 1) == ch {25 return true26 }27 }28 false29}3031isHex = fn(c) { (c >= "0" && c <= "9") || (c >= "a" && c <= "f") || (c >= "A" && c <= "F") }3233# --- Parser constructor ---34Parser = fn(src) {35 p = new()36 p.s = src37 p.i = 038 p.n = len(src)39 p.line = 140 p.col = 14142 p.inBounds = fn() { p.i >= 0 && p.i < p.n }43 p.peek = fn() { if p.inBounds() { p.s[p.i] } else { "" } }4445 p.advance = fn(n) {46 for k = 0; k < n && p.inBounds(); ++k {47 if p.peek() == "\n" {48 p.line += 149 p.col = 150 } else {51 p.col += 152 }53 p.i += 154 }55 }5657 p.errorf = fn(msg) { error("json parse error at {p.line}:{p.col} -> {msg}") }5859 p.skipSpace = fn() {60 for p.inBounds() && containsChar(spaces, p.peek()) { p.advance(1) }61 }6263 p.expectChar = fn(ch) {64 p.skipSpace()65 if !p.inBounds() || p.peek() != ch {66 return p.errorf("expected '" + ch + "'")67 }68 p.advance(1)69 true70 }7172 p.parseString = fn() {73 p.skipSpace()74 if !p.inBounds() || p.peek() != "\"" {75 return p.errorf(`expected '"' to start string`)76 }77 p.advance(1)7879 buf = ""80 for p.inBounds() {81 c = p.peek()82 p.advance(1)8384 if c == "\"" {85 return buf86 }8788 if c == "\\" {89 if !p.inBounds() {90 return p.errorf("unterminated escape")91 }9293 esc = p.peek()94 p.advance(1)9596 if esc == "\"" {97 buf += "\""98 } else if esc == "\\" {99 buf += "\\"100 } else if esc == "/" {101 buf += "/"102 } else if esc == "b" {103 buf += "\b"104 } else if esc == "f" {105 buf += "\f"106 } else if esc == "n" {107 buf += "\n"108 } else if esc == "r" {109 buf += "\r"110 } else if esc == "t" {111 buf += "\t"112 } else if esc == "u" {113 # keep \uXXXX verbatim114 if !(p.i + 3 < p.n) {115 return p.errorf("invalid \\u escape")116 }117118 h0 = p.s[p.i]; h1 = p.s[p.i + 1]119 h2 = p.s[p.i + 2]; h3 = p.s[p.i + 3]120121 if !isHex(h0) || !isHex(h1) || !isHex(h2) || !isHex(h3) {122 return p.errorf("invalid hex in \\u escape")123 }124 buf += "\\u" + h0 + h1 + h2 + h3125 p.advance(4)126 } else {127 return p.errorf("unknown escape sequence '\\" + esc + "'")128 }129 } else {130 buf += c131 }132 }133134 p.errorf("unterminated string")135 }136137 p.parseNumber = fn() {138 p.skipSpace()139 start = p.i140141 if p.inBounds() && p.peek() == "-" {142 p.advance(1)143 }144 if !p.inBounds() || p.peek() < "0" || p.peek() > "9" {145 return p.errorf("expected digit")146 }147 for p.inBounds() && p.peek() >= "0" && p.peek() <= "9" {148 p.advance(1)149 }150151 isFloat = false152153 if p.inBounds() && p.peek() == "." {154 isFloat = true155 p.advance(1)156 if !p.inBounds() || p.peek() < "0" || p.peek() > "9" {157 return p.errorf("expected digit after '.'")158 }159 for p.inBounds() && p.peek() >= "0" && p.peek() <= "9" {160 p.advance(1)161 }162 }163164 if p.inBounds() && (p.peek() == "e" || p.peek() == "E") {165 isFloat = true166 p.advance(1)167 if p.inBounds() && (p.peek() == "+" || p.peek() == "-") {168 p.advance(1)169 }170 if !p.inBounds() || p.peek() < "0" || p.peek() > "9" {171 return p.errorf("expected digit in the exponent")172 }173 for p.inBounds() && p.peek() >= "0" && p.peek() <= "9" {174 p.advance(1)175 }176 }177178 text = slice(p.s, start, p.i)179 return if isFloat { float(text) } else { int(text) }180 }181182 p.matchWord = fn(w) {183 for k = 0; k < len(w); ++k {184 if !(p.i + k < p.n) || p.s[p.i + k] != w[k] {185 return false186 }187 }188 p.i = p.i + len(w)189 p.col = p.col + len(w) # approximate (ignores newlines; literals don't have)190 true191 }192193 p.parseLiteral = fn() {194 p.skipSpace()195 if p.inBounds() && p.peek() == "t" && p.matchWord("true") { return true }196 if p.inBounds() && p.peek() == "f" && p.matchWord("false") { return false }197 if p.inBounds() && p.peek() == "n" && p.matchWord("null") { return null }198 p.errorf("expected literal true/false/null")199 }200201 p.parseArray = fn() {202 if failed(_ = p.expectChar("[")) { return _ }203204 arr = []205206 p.skipSpace()207 if p.inBounds() && p.peek() == "]" { p.advance(1); return arr }208209 for {210 if failed(v = p.parseValue()) { return v }211 arr = append(arr, v)212213 p.skipSpace()214 if !p.inBounds() { return p.errorf("unterminated array") }215216 if p.peek() == "," {217 p.advance(1)218 p.skipSpace()219 if !p.inBounds() { return p.errorf("value expected after ','") }220 continue221 }222223 if p.peek() == "]" { p.advance(1); return arr }224225 return p.errorf("expected ',' or ']'")226 }227 }228229 p.parseObject = fn() {230 if failed(_ = p.expectChar(`{`)) { return _ }231232 obj = {} # Tau map233234 p.skipSpace()235 if p.inBounds() && p.peek() == `}` { p.advance(1); return obj }236237 for {238 if failed(k = p.parseString()) { return k }239 if failed(_ = p.expectChar(":")) { return _ }240 if failed(v = p.parseValue()) { return v }241 obj[k] = v242243 p.skipSpace()244 if !p.inBounds() { return p.errorf("unterminated object") }245246 if p.peek() == "," {247 p.advance(1)248 p.skipSpace()249 if p.inBounds() && p.peek() == `}` {250 return p.errorf("trailing comma in object")251 }252 continue253 }254255 if p.peek() == `}` {256 p.advance(1)257 return obj258 }259260 return p.errorf("expected ',' or '}}'")261 }262 }263264 p.parseValue = fn() {265 p.skipSpace()266 if !p.inBounds() {267 return p.errorf("unexpected end while expecting value")268 }269270 c = p.peek()271 if c == `{` {272 return p.parseObject()273 }274 if c == `[` {275 return p.parseArray()276 }277 if c == `"` {278 return p.parseString()279 }280 if c == "-" || (c >= "0" && c <= "9") {281 return p.parseNumber()282 }283 p.parseLiteral()284 }285286 p.parse = fn() {287 val = p.parseValue()288 if failed(val) {289 return val290 }291 p.skipSpace()292 if p.i != p.n {293 return p.errorf("extra data after valid JSON value")294 }295 val296 }297298 p299}300301# Public API302Unmarshal = fn(s) {303 # should never fail, but be consistent304 if failed(p = Parser(s)) {305 return p306 }307 p.parse()308}309310escapeJSONString = fn(s) {311 buf = "\""312 for i = 0; i < len(s); ++i {313 ch = s[i]314315 # fast path316 if ch != "\"" && ch != "\\" && ch != "\b" && ch != "\f" && ch != "\n" && ch != "\r" && ch != "\t" && bytes(ch)[0] >= 32 {317 buf += ch318 continue319 }320321 # escapes / control chars322 if ch == "\"" {323 buf += "\\\""324 } else if ch == "\\" {325 buf += "\\\\"326 } else if ch == "\b" {327 buf += "\\b"328 } else if ch == "\f" {329 buf += "\\f"330 } else if ch == "\n" {331 buf += "\\n"332 } else if ch == "\r" {333 buf += "\\r"334 } else if ch == "\t" {335 buf += "\\t"336 } else {337 # control char < 0x20 -> \u00XX338 n = bytes(ch)[0]339 digits = "0123456789abcdef"340 hi = n / 16341 lo = n - hi * 16342 buf += "\\u00" + digits[hi] + digits[lo]343 }344 }345 buf + "\""346}347348encodeListJSON = fn(xs) {349 buf = "["350 for i = 0; i < len(xs); ++i {351 if i > 0 { buf += "," }352 if failed(v = encodeJSON(xs[i])) {353 return v354 }355 buf += v356 }357 buf + "]"358}359360encodeMapJSON = fn(m) {361 # JSON requires string keys362 ks = keys(m)363 buf = `{`364 for i = 0; i < len(ks); ++i {365 k = ks[i]366 if type(k) != "string" {367 return error("json encode error: object key is not a string")368 }369 if i > 0 { buf += "," }370 buf += escapeJSONString(k) + ":"371 if failed(v = encodeJSON(m[k])) {372 return v373 }374 buf += v375 }376 buf + `}`377}378379encodeJSON = fn(x) {380 t = type(x)381382 return if t == "null" {383 "null"384 } else if t == "string" {385 escapeJSONString(x)386 } else if t == "int" || t == "float" || t == "bool" {387 string(x)388 } else if t == "list" {389 encodeListJSON(x)390 } else if t == "map" || t == "object" {391 encodeMapJSON(x)392 } else {393 error("json encode error: unsupported type " + t)394 }395}396397# Marshal returns the JSON form of a value: null, booleans, numbers, strings,398# lists, maps and objects, which are written as objects the way a struct is399# in Go.400Marshal = fn(v) { encodeJSON(v) }401402# Valid reports whether s holds one well formed JSON value and nothing else.403Valid = fn(s) { !failed(Unmarshal(s)) }404405# MarshalIndent is Marshal with the output spread over several lines, indent406# being what one level is written with. With no indent a tab is used.407MarshalIndent = fn(v, indent) {408 if failed(s = Marshal(v)) {409 return s410 }411 return Indent(s, indent)412}413414# Indent rewrites JSON with one value per line. It walks the text rather than415# the value, so what is inside strings is left alone. The braces are written416# as raw strings: in an ordinary one a brace opens an interpolation.417Indent = fn(src, indent) {418 if indent == null {419 indent = "\t"420 }421422 out = ""423 depth = 0424 inString = false425426 newline = fn(d) {427 s = "\n"428 for i = 0; i < d; ++i {429 s = s + indent430 }431 return s432 }433434 for i = 0; i < len(src); ++i {435 c = src[i]436437 if inString {438 out = out + c439 # A quote closes the string unless it is escaped, and a backslash440 # takes the next character with it whatever it is.441 if c == "\\" && i + 1 < len(src) {442 ++i443 out = out + src[i]444 } else if c == "\"" {445 inString = false446 }447 continue448 }449450 if c == "\"" {451 inString = true452 out = out + c453 } else if c == `{` || c == "[" {454 ++depth455 out = out + c + newline(depth)456 } else if c == `}` || c == "]" {457 --depth458 out = out + newline(depth) + c459 } else if c == "," {460 out = out + c + newline(depth)461 } else if c == ":" {462 out = out + c + " "463 } else {464 out = out + c465 }466 }467 return out468}