Figments-of-the-Night/addons/inkgd/runtime/simple_json.gd
Gerard Gascón b99855351d init
2025-04-24 17:23:34 +02:00

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