# ############################################################################ # # Copyright © 2015-2021 inkle Ltd. # Copyright © 2019-2022 Frédéric Maquin # All Rights Reserved # # This file is part of inkgd. # inkgd is licensed under the terms of the MIT license. # ############################################################################ # extends InkBase class_name InkSimpleJSON # ############################################################################ # # (String) -> Dictionary static func text_to_dictionary(text: String) -> Dictionary: return Reader.new(text).to_dictionary() # (String) -> Array static func text_to_array(text: String) -> Array: return Reader.new(text).to_array() class Reader extends InkBase: # (String) -> Reader func _init(text: String): _text = text _offset = 0 skip_whitespace() _root_object = read_object() # () -> Dictionary func to_dictionary() -> Dictionary: return _root_object # () -> Array func to_array() -> Array: return _root_object # (String) -> bool func is_number_char(c: String) -> bool: if c.length() > 1: return false return c.is_valid_int() || c == "." || c == "-" || c == "+" || c == 'E' || c == 'e' # (String) -> bool func is_first_number_char(c: String) -> bool: if c.length() > 1: return false return c.is_valid_int() || c == "-" || c == "+" # () -> Variant func read_object(): var current_char = _text[_offset] if current_char == "{": return read_dictionary() elif current_char == "[": return read_array() elif current_char == "\"": return read_string() elif is_first_number_char(current_char): return read_number() elif try_read("true"): return true elif try_read("false"): return false elif try_read("null"): return null InkUtils.throw_exception("Unhandled object type in JSON: %s" % _text.substr(_offset, 30)) return JsonError.new() # () -> Dictionary? func read_dictionary(): var dict = {} # Dictionary if !expect("{"): return null skip_whitespace() if try_read("}"): return dict var first_time = true while first_time || try_read(","): first_time = false skip_whitespace() var key = read_string() if !expect(key != null, "dictionary key"): return null skip_whitespace() if !expect(":"): return null skip_whitespace() var val = read_object() if !expect(val != null, "dictionary value"): return null dict[key] = val skip_whitespace() if !expect("}"): return null return dict # () -> Array? func read_array(): var list = [] if !expect("["): return null skip_whitespace() if try_read("]"): return list var first_time = true while first_time || try_read(","): first_time = false skip_whitespace() var val = read_object() list.append(val) skip_whitespace() if !expect("]"): return null return list # () -> String? func read_string(): if !expect("\""): return null var sb = "" while(_offset < _text.length()): var c = _text[_offset] if c == "\\": _offset += 1 if _offset >= _text.length(): InkUtils.throw_exception("Unexpected EOF while reading string") return null c = _text[_offset] match c: "\"", "\\", "/": sb += c "n": sb += "\n" "t": sb += "\t" "r", "b", "f": pass "u": if _offset + 4 >= _text.length(): InkUtils.throw_exception("Unexpected EOF while reading string") return null var digits = _text.substr(_offset + 1, 4) var test_json_conv = JSON.new() test_json_conv.parse("\"\\u" + digits + "\"") var json_parse_result = test_json_conv.get_data() if json_parse_result.error != OK: InkUtils.throw_exception("Invalid Unicode escape character at offset %d" % (_offset - 1)) return null sb += json_parse_result.result _offset += 4 break _: InkUtils.throw_exception("Invalid Unicode escape character at offset %d " % (_offset - 1)) return null elif c == "\"": break else: sb += c _offset += 1 if !expect("\""): return null return sb # () -> Variant func read_number(): var start_offset = _offset var is_float = false while(_offset < _text.length()): var c = _text[_offset] if (c == "." || c == "e" || c == "E"): is_float = true if is_number_char(c): _offset += 1 continue else: break _offset += 1 var num_str = _text.substr(start_offset, _offset - start_offset) if is_float: if num_str.is_valid_float(): return float(num_str) else: if num_str.is_valid_int(): return int(num_str) InkUtils.throw_exception("Failed to parse number value: " + num_str) return JsonError.new() # (String) -> bool func try_read(text_to_read: String) -> bool: if _offset + text_to_read.length() > _text.length(): return false var i = 0 while (i < text_to_read.length()): if text_to_read[i] != _text[_offset + i]: return false i += 1 _offset += text_to_read.length() return true # (bool | String, String) -> bool func expect(condition_or_expected_str, message = null) -> bool: var _condition = false if condition_or_expected_str is String: _condition = try_read(condition_or_expected_str) elif condition_or_expected_str is bool: _condition = condition_or_expected_str if !_condition: if message == null: message = "Unexpected token" else: message = "Expected " + message message += str(" at offset ", _offset) InkUtils.throw_exception(message) return false return true func skip_whitespace(): while _offset < _text.length(): var c = _text[_offset] if c == " " || c == "\t" || c == "\n" || c == "\r": _offset += 1 else: break var _text = null # String var _offset: int = 0 # int var _root_object # Variant # ######################################################################## # # GDScript extra methods # ######################################################################## # func is_ink_class(type: String) -> bool: return type == "InkSimpleJSON.Reader" || super.is_ink_class(type) func get_ink_class() -> String: return "InkSimpleJSON.Reader" class Writer extends InkBase: # ######################################################################## # # Imports # ######################################################################## # var InkStringWriter := load("res://addons/inkgd/runtime/extra/string_writer.gd") as GDScript var InkStateElement := load("res://addons/inkgd/runtime/extra/state_element.gd") as GDScript # (String) -> Writer func _init(): self._writer = InkStringWriter.new() # (Callable) -> void func write_object(inner: Callable) -> void: write_object_start() inner.call(self) write_object_end() func write_object_start() -> void: start_new_object(true) self._state_stack.push_front(InkStateElement.new(InkStateElement.State.OBJECT)) self._writer.write("{") func write_object_end() -> void: assert_that(self.state == InkStateElement.State.OBJECT) self._writer.write("}") self._state_stack.pop_front() # These two methods don't need to be implemented in GDScript. # # public void WriteProperty(string name, Action inner) # public void WriteProperty(int id, Action inner) # Also include: # void WriteProperty(T name, Action inner) # (String, Variant) -> void func write_property(name: String, content) -> void: if (content is String || content is int || content is bool): write_property_start(name) write(content) write_property_end() elif content is Callable: write_property_start(name) content.call(self) write_property_end() else: push_error("Wrong type for 'content': %s" % str(content)) # These two methods don't need to be implemented in GDScript. # # public void WritePropertyStart(string name) # public void WritePropertyStart(int id) # () -> void func write_property_end() -> void: assert_that(self.state == InkStateElement.State.PROPERTY) assert_that(self.child_count == 1) self._state_stack.pop_front() # (String) -> void func write_property_name_start() -> void: assert_that(self.state == InkStateElement.State.OBJECT) if self.child_count > 0: self._writer.write(',') self._writer.write('"') increment_child_count() self._state_stack.push_front(InkStateElement.new(InkStateElement.State.PROPERTY)) self._state_stack.push_front(InkStateElement.new(InkStateElement.State.PROPERTY_NAME)) # () -> void func write_property_name_end() -> void: assert_that(self.state == InkStateElement.State.PROPERTY_NAME) self._writer.write('":') self._state_stack.pop_front() # (String) -> void func write_property_name_inner(string: String) -> void: assert_that(self.state == InkStateElement.State.PROPERTY_NAME) self._writer.write(string) # (Variant) -> void func write_property_start(name) -> void: assert_that(self.state == InkStateElement.State.OBJECT) if self.child_count > 0: self._writer.write(',') self._writer.write('"') self._writer.write(str(name)) self._writer.write('":') increment_child_count() _state_stack.push_front(InkStateElement.new(InkStateElement.State.PROPERTY)) # () -> void func write_array_start() -> void: start_new_object(true) _state_stack.push_front(InkStateElement.new(InkStateElement.State.ARRAY)) _writer.write("[") # () -> void func write_array_end() -> void: assert_that(self.state == InkStateElement.State.ARRAY) _writer.write("]") _state_stack.pop_front() # This method didn't exist as-is in the original implementation. # (Variant) -> void func write(content) -> void: if content is int: write_int(content) elif content is float: write_float(content) elif content is String: write_string(content) elif content is bool: write_bool(content) else: push_error("Wrong type for 'content': %s" % str(content)) # (int) -> void func write_int(i: int) -> void: start_new_object(false) _writer.write(str(i)) # (float) -> void func write_float(f: float) -> void: start_new_object(false) var float_str = str(f) # We could probably use 3.402823e+38, but keeping # ±3.4e+38 for compatibility with the reference implementation. if float_str == "inf": _writer.write("3.4e+38") elif float_str == "-inf": _writer.write("-3.4e+38") elif float_str == "nan": _writer.write("0.0") else: _writer.write(float_str) # The exponent part is defensive as Godot doesn't seem to convert # floats to string in such a way. if !("." in float_str) && !("e" in float_str) && !("E" in float_str): _writer.write(".0") # (String, bool) -> void func write_string(string: String, escape: bool = true): start_new_object(false) _writer.write('"') if escape: write_escaped_string(string) else: _writer.write(string) _writer.write('"') # (bool) -> void func write_bool(b: bool) -> void: start_new_object(false) _writer.write("true" if b else "false") # () -> void func write_null() -> void: start_new_object(false) _writer.write("null") # () -> void func write_string_start() -> void: start_new_object(true) _state_stack.push_front(InkStateElement.new(InkStateElement.State.STRING)) _writer.write('"') # () -> void func write_string_end() -> void: assert_that(state == InkStateElement.State.STRING) _writer.write('"') _state_stack.pop_front() # (string, bool) -> void func write_string_inner(string: String, escape: bool = true) -> void: assert_that(self.state == InkStateElement.State.STRING) if escape: write_escaped_string(string) else: _writer.write(string) # (String) -> void func write_escaped_string(string: String) -> void: for c in string: if c < ' ': match c: "\n": _writer.write("\\n") "\t": _writer.write("\\t") else: match c: '\\', '"': _writer.write("\\") _writer.write(c) _: _writer.write(c) # (bool) -> void func start_new_object(container: bool) -> void: if container: assert_that( self.state == InkStateElement.State.NONE || self.state == InkStateElement.State.PROPERTY || self.state == InkStateElement.State.ARRAY ) else: assert_that( self.state == InkStateElement.State.PROPERTY || self.state == InkStateElement.State.ARRAY ) if self.state == InkStateElement.State.ARRAY && self.child_count > 0: _writer.write(",") if self.state == InkStateElement.State.PROPERTY: assert_that(self.child_count == 0) if ( self.state == InkStateElement.State.ARRAY || self.state == InkStateElement.State.PROPERTY ): increment_child_count() var state: int: # StateElement.State get: if _state_stack.size() > 0: return _state_stack.front().type else: return InkStateElement.State.NONE var child_count: int: # int get: if _state_stack.size() > 0: return _state_stack.front().child_count else: return 0 # () -> void func increment_child_count() -> void: assert_that(_state_stack.size() > 0) var curr_el = _state_stack.pop_front() curr_el.child_count += 1 _state_stack.push_front(curr_el) # (bool) -> void func assert_that(condition: bool) -> void: if OS.is_debug_build(): return if !condition: push_error("Assert failed while writing JSON") assert(condition) # () -> String func _to_string() -> String: return _writer._to_string() var _state_stack: Array = [] # Array var _writer: InkStringWriter # ######################################################################## # # GDScript extra methods # ######################################################################## # func is_ink_class(type: String) -> bool: return type == "InkSimpleJSON.Writer" || super.is_ink_class(type) func get_ink_class() -> String: return "InkSimpleJSON.Writer" class JsonError: func init(): pass