588 lines
14 KiB
GDScript
588 lines
14 KiB
GDScript
# ############################################################################ #
|
|
# Copyright © 2015-2021 inkle Ltd.
|
|
# Copyright © 2019-2022 Frédéric Maquin <fred@ephread.com>
|
|
# 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<String, Variant>
|
|
static func text_to_dictionary(text: String) -> Dictionary:
|
|
return Reader.new(text).to_dictionary()
|
|
|
|
# (String) -> Array<Variant>
|
|
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<String, Variant>
|
|
func to_dictionary() -> Dictionary:
|
|
return _root_object
|
|
|
|
# () -> Array<Variant>
|
|
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<String, Variant>?
|
|
func read_dictionary():
|
|
var dict = {} # Dictionary<String, Variant>
|
|
|
|
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<Variant>?
|
|
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<Writer> inner)
|
|
# public void WriteProperty(int id, Action<Writer> inner)
|
|
|
|
# Also include:
|
|
# void WriteProperty<T>(T name, Action<Writer> 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<StateElement>
|
|
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
|