tochie-facade/scenes/Xml.gd
2023-09-02 18:51:00 +05:00

126 lines
4.7 KiB
GDScript

extends Node
class_name Xml
class XmlNode extends Reference:
func is_element() -> bool: return false
func is_text() -> bool: return false
class XmlElement extends XmlNode:
var name: String
var attributes: Dictionary # String to String
var children: Array # of XmlNode
func is_element() -> bool: return true
func get_named_child_element(p_name: String) -> XmlElement:
for idx in range(self.children.size()):
var child := self.children[idx] as XmlElement
if child != null and child.name == p_name:
return child
return null
func take_named_child_element(p_name: String) -> XmlElement:
for idx in range(self.children.size()):
var child := self.children[idx] as XmlElement
if child != null and child.name == p_name:
self.children.remove(idx)
return child
return null
func as_string(level: int = 0) -> String:
var result = '\t'.repeat(level) + "XmlElement \"%s\"" % self.name
if self.attributes.size() > 0:
result += ", Attributes: " + String(self.attributes)
for child in self.children:
result += '\n' + '\t '.repeat(level) + child.as_string(level + 1)
return result
class XmlText extends XmlNode:
var data: String
func is_text() -> bool: return true
func as_string(_level: int = 0) -> String:
return "XmlText \"%s\"" % self.data
## Wrapper over XMLParser for TCP stream oriented XML data.
# todo: Save namespaces.
# todo: Ability to parse from partially arrived data, with saving for future resume.
class Parser extends Reference:
var _root: XmlElement = null
var _element_stack: Array # of XmlElement
var _pending: PoolByteArray
func take_root() -> XmlElement:
var result := self._root
assert(result != null)
self._root = null
return result
## Returns true if root element is closed, otherwise more tags are expected to come later.
func parse_a_bit(data): # -> bool or Error:
var error: int
var parser := XMLParser.new()
if data is String: data = data.to_utf8()
if data == null: data = PoolByteArray()
var total = self._pending + data
if total.size() == 0:
return false
error = parser.open_buffer(total)
if error != OK:
push_error("Error opening a buffer for XML parsing")
return error
self._pending = PoolByteArray()
while parser.read() == OK:
if parser.get_node_type() == XMLParser.NODE_ELEMENT:
var element := XmlElement.new()
element.name = parser.get_node_name()
var attribute_count := parser.get_attribute_count()
for idx in range(attribute_count):
element.attributes[parser.get_attribute_name(idx)] = parser.get_attribute_value(idx)
if self._root != null:
self._element_stack[self._element_stack.size() - 1].children.push_back(element)
else: self._root = element
# todo: Handle _root empty element.
if not parser.is_empty():
self._element_stack.push_back(element)
elif parser.get_node_type() == XMLParser.NODE_ELEMENT_END:
if self._element_stack.size() == 0:
push_error("Closing element </%s> closes nothing" % [parser.get_node_name()])
return ERR_PARSE_ERROR
var popped := self._element_stack.pop_back() as XmlElement
if popped.name != parser.get_node_name():
push_error("Element <%s> closes sooner than <%s>" % [parser.get_node_name(), popped.name])
return ERR_PARSE_ERROR
if self._element_stack.size() == 0:
# todo: Handle partial data.
if parser.read() == OK:
self._pending = total.subarray(parser.get_node_offset(), -1)
return true
elif parser.get_node_type() == XMLParser.NODE_TEXT:
if self._element_stack.size() == 0:
push_error("Text node should be a child of an element")
return ERR_PARSE_ERROR
var text = XmlText.new()
text.data = parser.get_node_data()
self._element_stack[self._element_stack.size() - 1].children.push_back(text)
elif parser.get_node_type() == XMLParser.NODE_UNKNOWN:
## Needed for things like <?xml ?> version specification.
pass
else:
push_error("Node type unimplemented")
return self._element_stack.size() == 0