X-Git-Url: http://nitlanguage.org diff --git a/lib/json/static.nit b/lib/json/static.nit index 3ed9929..15a4355 100644 --- a/lib/json/static.nit +++ b/lib/json/static.nit @@ -34,43 +34,95 @@ interface Jsonable # SEE: `append_json` fun to_json: String is abstract + # Use `append_json` to implement `to_json`. + # + # Therefore, one that redefine `append_json` may use the following + # redefinition to link `to_json` and `append_json`: + # + # ~~~nitish + # redef fun to_json do return to_json_by_append + # ~~~ + # + # Note: This is not the default implementation of `to_json` in order to + # avoid cyclic references between `append_json` and `to_json` when none are + # implemented. + protected fun to_json_by_append: String do + var buffer = new FlatBuffer + append_json(buffer) + return buffer.to_s + end + # Append the JSON representation of `self` to the specified buffer. # # SEE: `to_json` - fun append_json(buffer: Buffer) do - buffer.append(to_json) + fun append_json(buffer: Buffer) do buffer.append(to_json) + + # Pretty print JSON string. + # + # ~~~ + # var obj = new JsonObject + # obj["foo"] = 1 + # obj["bar"] = true + # var arr = new JsonArray + # arr.add 2 + # arr.add false + # arr.add "baz" + # obj["baz"] = arr + # var res = obj.to_pretty_json + # var exp = """{ + # \t"foo": 1, + # \t"bar": true, + # \t"baz": [2, false, "baz"] + # }\n""" + # assert res == exp + # ~~~ + fun to_pretty_json: String do + var res = new FlatBuffer + pretty_json_visit(res, 0) + res.add '\n' + return res.to_s end + + private fun pretty_json_visit(buffer: FlatBuffer, indent: Int) is abstract end redef class Text super Jsonable + # Removes JSON-escaping if necessary in a JSON string + # + # assert "\\\"string\\uD83D\\uDE02\\\"".unescape_json == "\"string😂\"" + fun unescape_json: Text do + if not json_need_escape then return self + return self.json_to_nit_string + end + + # Does `self` need treatment from JSON to Nit ? + # + # i.e. is there at least one `\` character in it ? + # + # assert not "string".json_need_escape + # assert "\\\"string\\\"".json_need_escape + protected fun json_need_escape: Bool do return has('\\') + redef fun append_json(buffer) do buffer.add '\"' - for i in [0..self.length[ do + for i in [0 .. self.length[ do var char = self[i] if char == '\\' then buffer.append "\\\\" else if char == '\"' then buffer.append "\\\"" - else if char == '\/' then - buffer.append "\\/" - else if char < 16.ascii then + else if char < ' ' then if char == '\n' then buffer.append "\\n" else if char == '\r' then buffer.append "\\r" else if char == '\t' then buffer.append "\\t" - else if char == 0x0C.ascii then - buffer.append "\\f" - else if char == 0x08.ascii then - buffer.append "\\b" else - buffer.append "\\u000{char.ascii.to_hex}" + buffer.append char.escape_to_utf16 end - else if char < ' ' then - buffer.append "\\u00{char.ascii.to_hex}" else buffer.add char end @@ -78,14 +130,65 @@ redef class Text buffer.add '\"' end + # Escapes `self` from a JSON string to a Nit string + # + # assert "\\\"string\\\"".json_to_nit_string == "\"string\"" + # assert "\\nEscape\\t\\n".json_to_nit_string == "\nEscape\t\n" + # assert "\\u0041zu\\uD800\\uDFD3".json_to_nit_string == "Azu𐏓" + protected fun json_to_nit_string: String do + var res = new FlatBuffer.with_capacity(bytelen) + var i = 0 + while i < self.length do + var char = self[i] + if char == '\\' then + i += 1 + char = self[i] + if char == 'b' then + char = 0x08.code_point + else if char == 'f' then + char = 0x0C.code_point + else if char == 'n' then + char = '\n' + else if char == 'r' then + char = '\r' + else if char == 't' then + char = '\t' + else if char == 'u' then + var code = substring(i + 1, 4) + var hx = code.to_hex + if hx >= 0xD800 and hx <= 0xDFFF then + var lostr = substring(i + 7, 4) + if lostr.length < 4 then + hx = 0xFFFD + else + hx <<= 16 + hx += lostr.to_hex + hx = hx.from_utf16_surr + end + i += 6 + end + i += 4 + char = hx.code_point + end + # `"`, `/` or `\` => Keep `char` as-is. + end + res.add char + i += 1 + end + return res.to_s + end + + # Encode `self` in JSON. # - # assert "\t\"http://example.com\"\r\n\0\\".to_json == - # "\"\\t\\\"http:\\/\\/example.com\\\"\\r\\n\\u0000\\\\\"" + # ~~~ + # assert "\t\"http://example.com\"\r\n\0\\".to_json == + # "\"\\t\\\"http://example.com\\\"\\r\\n\\u0000\\\\\"" + # ~~~ redef fun to_json do - var buffer = new FlatBuffer - append_json(buffer) - return buffer.write_to_string + var b = new FlatBuffer.with_capacity(bytelen) + append_json(b) + return b.to_s end # Parse `self` as JSON. @@ -133,6 +236,16 @@ redef class Text end end +redef class FlatText + redef fun json_need_escape do + var its = items + for i in [first_byte .. last_byte] do + if its[i] == 0x5Cu8 then return true + end + return false + end +end + redef class Buffer # Append the JSON representation of `jsonable` to `self`. @@ -211,10 +324,29 @@ interface JsonMapRead[K: String, V: nullable Jsonable] # obj = new JsonObject # obj["baz"] = null # assert obj.to_json == "\{\"baz\":null\}" - redef fun to_json do - var buffer = new FlatBuffer - append_json(buffer) - return buffer.write_to_string + redef fun to_json do return to_json_by_append + + redef fun pretty_json_visit(buffer, indent) do + buffer.append "\{\n" + indent += 1 + var i = 0 + for k, v in self do + buffer.append "\t" * indent + buffer.append "\"{k}\": " + if v isa JsonObject or v isa JsonArray then + v.pretty_json_visit(buffer, indent) + else + buffer.append v.to_json + end + if i < length - 1 then + buffer.append "," + end + buffer.append "\n" + i += 1 + end + indent -= 1 + buffer.append "\t" * indent + buffer.append "\}" end private fun append_json_entry(iterator: MapIterator[String, nullable Jsonable], @@ -259,10 +391,21 @@ class JsonSequenceRead[E: nullable Jsonable] # assert arr.to_json =="[\"foo\"]" # arr.pop # assert arr.to_json =="[]" - redef fun to_json do - var buffer = new FlatBuffer - append_json(buffer) - return buffer.write_to_string + redef fun to_json do return to_json_by_append + + redef fun pretty_json_visit(buffer, indent) do + buffer.append "\[" + var i = 0 + for v in self do + if v isa JsonObject or v isa JsonArray then + v.pretty_json_visit(buffer, indent) + else + buffer.append v.to_json + end + if i < length - 1 then buffer.append ", " + i += 1 + end + buffer.append "\]" end private fun append_json_entry(iterator: Iterator[nullable Jsonable], @@ -283,13 +426,15 @@ redef class JsonParseError # Get the JSON representation of `self`. # - # var err = new JsonParseError("foo", new Position(1, 2, 3, 4, 5, 6)) - # assert err.to_json == "\{\"error\":\"JsonParseError\"," + - # "\"position\":\{" + - # "\"pos_start\":1,\"pos_end\":2," + - # "\"line_start\":3,\"line_end\":4," + - # "\"col_start\":5,\"col_end\":6" + - # "\},\"message\":\"foo\"\}" + # ~~~ + # var err = new JsonParseError("foo", new Position(1, 2, 3, 4, 5, 6)) + # assert err.to_json == "\{\"error\":\"JsonParseError\"," + + # "\"position\":\{" + + # "\"pos_start\":1,\"pos_end\":2," + + # "\"line_start\":3,\"line_end\":4," + + # "\"col_start\":5,\"col_end\":6" + + # "\},\"message\":\"foo\"\}" + # ~~~ redef fun to_json do return "\{\"error\":\"JsonParseError\"," + "\"position\":{position.to_json}," + @@ -302,12 +447,14 @@ redef class Position # Get the JSON representation of `self`. # - # var pos = new Position(1, 2, 3, 4, 5, 6) - # assert pos.to_json == "\{" + - # "\"pos_start\":1,\"pos_end\":2," + - # "\"line_start\":3,\"line_end\":4," + - # "\"col_start\":5,\"col_end\":6" + - # "\}" + # ~~~ + # var pos = new Position(1, 2, 3, 4, 5, 6) + # assert pos.to_json == "\{" + + # "\"pos_start\":1,\"pos_end\":2," + + # "\"line_start\":3,\"line_end\":4," + + # "\"col_start\":5,\"col_end\":6" + + # "\}" + # ~~~ redef fun to_json do return "\{\"pos_start\":{pos_start},\"pos_end\":{pos_end}," + "\"line_start\":{line_start},\"line_end\":{line_end}," + @@ -319,6 +466,7 @@ end # Redef parser redef class Nvalue + # The represented value. private fun to_nit_object: nullable Jsonable is abstract end @@ -348,41 +496,8 @@ redef class Nvalue_null end redef class Nstring - private fun to_nit_string: String do - var res = new FlatBuffer - var i = 1 - while i < text.length - 1 do - var char = text[i] - if char == '\\' then - i += 1 - char = text[i] - if char == 'b' then - char = 0x08.ascii - else if char == 'f' then - char = 0x0C.ascii - else if char == 'n' then - char = '\n' - else if char == 'r' then - char = '\r' - else if char == 't' then - char = '\t' - else if char == 'u' then - var code = text.substring(i + 1, 4).to_hex - # TODO UTF-16 escaping is not supported yet. - if code >= 128 then - char = '?' - else - char = code.ascii - end - i += 4 - end - # `"`, `/` or `\` => Keep `char` as-is. - end - res.add char - i += 1 - end - return res.write_to_string - end + # The represented string. + private fun to_nit_string: String do return text.substring(1, text.length - 2).unescape_json.to_s end redef class Nvalue_object @@ -398,6 +513,7 @@ redef class Nvalue_object end redef class Nmembers + # All the key-value pairs. private fun pairs: Array[Npair] is abstract end @@ -415,7 +531,10 @@ redef class Nmembers_head end redef class Npair + # The represented key. private fun name: String do return n_string.to_nit_string + + # The represented value. private fun value: nullable Jsonable do return n_value.to_nit_object end @@ -433,6 +552,7 @@ redef class Nvalue_array end redef class Nelements + # All the items. private fun items: Array[Nvalue] is abstract end