diff --git a/.gitignore b/.gitignore
index 905d2bf174..ca30ee1b9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,4 +130,9 @@ vls.log
wasm.v
TAGS
tags
-vlib/builtin/js/*.js
+
+# ignore large GTK *.gir files
+Gtk-4.0.gir
+*.gir
+
+vlib/builtin/js/*.js
\ No newline at end of file
diff --git a/vlib/encoding/xml/README.md b/vlib/encoding/xml/README.md
new file mode 100644
index 0000000000..161baf1af9
--- /dev/null
+++ b/vlib/encoding/xml/README.md
@@ -0,0 +1,44 @@
+## Description
+
+`xml` is a module to parse XML documents into a tree structure. It also supports
+validation of XML documents against a DTD.
+
+Note that this is not a streaming XML parser. It reads the entire document into
+memory and then parses it. This is not a problem for small documents, but it
+might be a problem for extremely large documents (several hundred megabytes or more).
+
+## Usage
+
+### Parsing XML Files
+
+There are three different ways to parse an XML Document:
+
+1. Pass the entire XML document as a string to `XMLDocument.from_string`.
+2. Specify a file path to `XMLDocument.from_file`.
+3. Use a source that implements `io.Reader` and pass it to `XMLDocument.from_reader`.
+
+```v
+import encoding.xml
+
+//...
+doc := xml.XMLDocument.from_file('test/sample.xml')!
+```
+
+### Validating XML Documents
+
+Simply call `validate` on the parsed XML document.
+
+### Querying
+
+Check the `get_element...` methods defined on the XMLDocument struct.
+
+### Escaping and Un-escaping XML Entities
+
+When the `validate` method is called, the XML document is parsed and all text
+nodes are un-escaped. This means that the text nodes will contain the actual
+text and not the escaped version of the text.
+
+When the XML document is serialized (using `str` or `pretty_str`), all text nodes are escaped.
+
+The escaping and un-escaping can also be done manually using the `escape_text` and
+`unescape_text` methods.
diff --git a/vlib/encoding/xml/encoding.v b/vlib/encoding/xml/encoding.v
new file mode 100644
index 0000000000..2ef924af8b
--- /dev/null
+++ b/vlib/encoding/xml/encoding.v
@@ -0,0 +1,148 @@
+module xml
+
+import strings
+
+// pretty_str returns a pretty-printed version of the XML node. It requires the current indentation
+// the node is at, the depth of the node in the tree, and a map of reverse entities to use when
+// escaping text.
+pub fn (node XMLNode) pretty_str(original_indent string, depth int, reverse_entities map[string]string) string {
+ // Create the proper indentation first
+ mut indent_builder := strings.new_builder(original_indent.len * depth)
+ for _ in 0 .. depth {
+ indent_builder.write_string(original_indent)
+ }
+ indent := indent_builder.str()
+
+ // Now we can stringify the node
+ mut builder := strings.new_builder(1024)
+ builder.write_string(indent)
+ builder.write_u8(`<`)
+ builder.write_string(node.name)
+
+ for key, value in node.attributes {
+ builder.write_u8(` `)
+ builder.write_string(key)
+ builder.write_string('="')
+ builder.write_string(value)
+ builder.write_u8(`"`)
+ }
+ builder.write_string('>\n')
+ for child in node.children {
+ match child {
+ string {
+ builder.write_string(indent)
+ builder.write_string(original_indent)
+ builder.write_string(escape_text(child, reverse_entities: reverse_entities))
+ }
+ XMLNode {
+ builder.write_string(child.pretty_str(original_indent, depth + 1, reverse_entities))
+ }
+ XMLComment {
+ builder.write_string(indent)
+ builder.write_string(original_indent)
+ builder.write_string('')
+ }
+ XMLCData {
+ builder.write_string(indent)
+ builder.write_string(original_indent)
+ builder.write_string('')
+ }
+ }
+ builder.write_u8(`\n`)
+ }
+ builder.write_string(indent)
+ builder.write_string('')
+ builder.write_string(node.name)
+ builder.write_u8(`>`)
+ return builder.str()
+}
+
+fn (list []DTDListItem) pretty_str(indent string) string {
+ if list.len == 0 {
+ return ''
+ }
+
+ mut builder := strings.new_builder(1024)
+ builder.write_u8(`[`)
+ builder.write_u8(`\n`)
+
+ for item in list {
+ match item {
+ DTDEntity {
+ builder.write_string('${indent}')
+ }
+ DTDElement {
+ builder.write_string('${indent}')
+ }
+ }
+ builder.write_u8(`\n`)
+ }
+ builder.write_u8(`]`)
+ return builder.str()
+}
+
+fn (doctype DocumentType) pretty_str(indent string) string {
+ match doctype.dtd {
+ string {
+ content := doctype.dtd
+ return if content.len > 0 {
+ ''
+ } else {
+ ''
+ }
+ }
+ DocumentTypeDefinition {
+ if doctype.dtd.list.len == 0 {
+ return ''
+ }
+
+ mut builder := strings.new_builder(1024)
+ builder.write_string('')
+ builder.write_u8(`\n`)
+ return builder.str()
+ }
+ }
+}
+
+// pretty_str returns a pretty-printed version of the XML document. It requires the string used to
+// indent each level of the document.
+pub fn (doc XMLDocument) pretty_str(indent string) string {
+ mut document_builder := strings.new_builder(1024)
+
+ prolog := ''
+ comments := if doc.comments.len > 0 {
+ mut comments_buffer := strings.new_builder(512)
+ for comment in doc.comments {
+ comments_buffer.write_string('')
+ comments_buffer.write_u8(`\n`)
+ }
+ comments_buffer.str()
+ } else {
+ ''
+ }
+
+ document_builder.write_string(prolog)
+ document_builder.write_u8(`\n`)
+ document_builder.write_string(doc.doctype.pretty_str(indent))
+ document_builder.write_u8(`\n`)
+ document_builder.write_string(comments)
+ document_builder.write_string(doc.root.pretty_str(indent, 0, doc.parsed_reverse_entities))
+
+ return document_builder.str()
+}
+
+// str returns a string representation of the XML document. It uses a 2-space indentation
+// to pretty-print the document.
+pub fn (doc XMLDocument) str() string {
+ return doc.pretty_str(' ')
+}
diff --git a/vlib/encoding/xml/entity.v b/vlib/encoding/xml/entity.v
new file mode 100644
index 0000000000..708fc06903
--- /dev/null
+++ b/vlib/encoding/xml/entity.v
@@ -0,0 +1,79 @@
+module xml
+
+import strings
+
+pub const default_entities = {
+ 'lt': '<'
+ 'gt': '>'
+ 'amp': '&'
+ 'apos': "'"
+ 'quot': '"'
+}
+
+pub const default_entities_reverse = {
+ '<': 'lt'
+ '>': 'gt'
+ '&': 'amp'
+ "'": 'apos'
+ '"': 'quot'
+}
+
+[params]
+pub struct EscapeConfig {
+ reverse_entities map[string]string = xml.default_entities_reverse
+}
+
+// escape_text replaces all entities in the given string with their respective
+// XML entity strings. See default_entities, which can be overridden.
+pub fn escape_text(content string, config EscapeConfig) string {
+ mut flattened_entities := []string{cap: 2 * config.reverse_entities.len}
+
+ for target, replacement in config.reverse_entities {
+ flattened_entities << target
+ flattened_entities << '&' + replacement + ';'
+ }
+
+ return content.replace_each(flattened_entities)
+}
+
+[params]
+pub struct UnescapeConfig {
+ entities map[string]string = xml.default_entities
+}
+
+// unescape_text replaces all entities in the given string with their respective
+// original characters or strings. See default_entities_reverse, which can be overridden.
+pub fn unescape_text(content string, config UnescapeConfig) !string {
+ mut buffer := strings.new_builder(content.len)
+ mut index := 0
+ runes := content.runes()
+ for index < runes.len {
+ match runes[index] {
+ `&` {
+ mut offset := 1
+ mut entity_buf := strings.new_builder(8)
+ for index + offset < runes.len && runes[index + offset] != `;` {
+ entity_buf.write_rune(runes[index + offset])
+ offset++
+ }
+ // Did we reach the end of the string?
+ if index + offset == runes.len {
+ return error('Unexpected end of string while parsing entity.')
+ }
+ // Did we find a valid entity?
+ entity := entity_buf.str()
+ if entity in config.entities {
+ buffer.write_string(config.entities[entity])
+ index += offset
+ } else {
+ return error('Unknown entity: ' + entity)
+ }
+ }
+ else {
+ buffer.write_rune(runes[index])
+ }
+ }
+ index++
+ }
+ return buffer.str()
+}
diff --git a/vlib/encoding/xml/entity_test.v b/vlib/encoding/xml/entity_test.v
new file mode 100644
index 0000000000..6e53b3c032
--- /dev/null
+++ b/vlib/encoding/xml/entity_test.v
@@ -0,0 +1,35 @@
+module main
+
+import encoding.xml
+
+fn test_escape() {
+ assert xml.escape_text('Normal string') == 'Normal string'
+ assert xml.escape_text('12 < 34') == '12 < 34'
+ assert xml.escape_text('12 > 34') == '12 > 34'
+ assert xml.escape_text('12 & 34') == '12 & 34'
+ assert xml.escape_text('He said, "Very well, let us proceed."') == 'He said, "Very well, let us proceed."'
+ assert xml.escape_text("He said, 'Very well, let us proceed.'") == 'He said, 'Very well, let us proceed.''
+
+ assert xml.escape_text('Do not escape ©.') == 'Do not escape ©.'
+
+ mut reverse_entities := xml.default_entities_reverse.clone()
+ reverse_entities['©'] = 'copy'
+ assert xml.escape_text('Do escape ©.', reverse_entities: reverse_entities) == 'Do escape ©.'
+}
+
+fn test_unescape() ! {
+ assert xml.unescape_text('Normal string')! == 'Normal string'
+ assert xml.unescape_text('12 < 34')! == '12 < 34'
+ assert xml.unescape_text('12 > 34')! == '12 > 34'
+ assert xml.unescape_text('12 & 34')! == '12 & 34'
+ assert xml.unescape_text('He said, "Very well, let us proceed."')! == 'He said, "Very well, let us proceed."'
+ assert xml.unescape_text('He said, 'Very well, let us proceed.'')! == "He said, 'Very well, let us proceed.'"
+
+ xml.unescape_text('12 &invalid; 34') or { assert err.msg() == 'Unknown entity: invalid' }
+
+ xml.unescape_text('Do not unescape ©') or { assert err.msg() == 'Unknown entity: copy' }
+
+ mut entities := xml.default_entities.clone()
+ entities['copy'] = '©'
+ assert xml.unescape_text('Do unescape ©.', entities: entities)! == 'Do unescape ©.'
+}
diff --git a/vlib/encoding/xml/parser.v b/vlib/encoding/xml/parser.v
new file mode 100644
index 0000000000..b4a8d55057
--- /dev/null
+++ b/vlib/encoding/xml/parser.v
@@ -0,0 +1,604 @@
+module xml
+
+import io
+import os
+import strings
+
+const (
+ default_prolog_attributes = {
+ 'version': '1.0'
+ 'encoding': 'UTF-8'
+ }
+ default_string_builder_cap = 32
+
+ element_len = '` {
+ break
+ }
+ return error('XML Comment not closed. Expected ">".')
+ } else {
+ comment_buffer.write_u8(ch)
+ comment_buffer.write_u8(after_ch)
+ }
+ }
+ else {
+ comment_buffer.write_u8(ch)
+ }
+ }
+ }
+
+ comment_contents := comment_buffer.str()
+ return XMLComment{comment_contents}
+}
+
+enum CDATAParserState {
+ normal
+ single
+ double
+}
+
+fn parse_cdata(mut reader io.Reader) !XMLCData {
+ mut contents_buf := strings.new_builder(xml.default_string_builder_cap)
+
+ mut state := CDATAParserState.normal
+ mut local_buf := [u8(0)]
+
+ for {
+ ch := next_char(mut reader, mut local_buf)!
+ contents_buf.write_u8(ch)
+ match ch {
+ `]` {
+ match state {
+ .double {
+ // Another ] after the ]] for some reason. Keep the state
+ }
+ .single {
+ state = .double
+ }
+ .normal {
+ state = .single
+ }
+ }
+ }
+ `>` {
+ match state {
+ .double {
+ break
+ }
+ else {
+ state = .normal
+ }
+ }
+ }
+ else {
+ state = .normal
+ }
+ }
+ }
+
+ contents := contents_buf.str().trim_space()
+ if !contents.ends_with(']]>') {
+ return error('CDATA section not closed.')
+ }
+ return XMLCData{contents[1..contents.len - 3]}
+}
+
+fn parse_entity(contents string) !(DTDEntity, string) {
+ // We find the nearest '>' to the start of the ENTITY
+ entity_end := contents.index('>') or { return error('Entity declaration not closed.') }
+ entity_contents := contents[xml.entity_len..entity_end]
+
+ name := entity_contents.trim_left(' \t\n').all_before(' ')
+ if name.len == 0 {
+ return error('Entity is missing name.')
+ }
+ value := entity_contents.all_after_first(name).trim_space().trim('"\'')
+ if value.len == 0 {
+ return error('Entity is missing value.')
+ }
+
+ // TODO: Add support for SYSTEM and PUBLIC entities
+
+ return DTDEntity{name, value}, contents[entity_end + 1..]
+}
+
+fn parse_element(contents string) !(DTDElement, string) {
+ // We find the nearest '>' to the start of the ELEMENT
+ element_end := contents.index('>') or { return error('Element declaration not closed.') }
+ element_contents := contents[xml.element_len..element_end].trim_left(' \t\n')
+
+ mut name_span := TextSpan{}
+
+ for ch in element_contents {
+ match ch {
+ ` `, `\t`, `\n` {
+ break
+ }
+ // Valid characters in an entity name are:
+ // 1. Lowercase alphabet - a-z
+ // 2. Uppercase alphabet - A-Z
+ // 3. Numbers - 0-9
+ // 4. Underscore - _
+ // 5. Colon - :
+ // 6. Period - .
+ `a`...`z`, `A`...`Z`, `0`...`9`, `_`, `:`, `.` {
+ name_span.end++
+ }
+ else {
+ return error('Invalid character in element name: "${ch}"')
+ }
+ }
+ }
+
+ name := element_contents[name_span.start..name_span.end].trim_left(' \t\n')
+ if name.len == 0 {
+ return error('Element is missing name.')
+ }
+ definition_string := element_contents.all_after_first(name).trim_space().trim('"\'')
+
+ definition := if definition_string.starts_with('(') {
+ // We have a list of possible children
+
+ // Ensure that both ( and ) are present
+ if !definition_string.ends_with(')') {
+ return error('Element declaration not closed.')
+ }
+
+ definition_string.trim('()').split(',')
+ } else {
+ // Invalid definition
+ return error('Invalid element definition: ${definition_string}')
+ }
+
+ // TODO: Add support for SYSTEM and PUBLIC entities
+
+ return DTDElement{name, definition}, contents[element_end + 1..]
+}
+
+fn parse_doctype(mut reader io.Reader) !DocumentType {
+ // We may have more < in the doctype so keep count
+ mut depth := 1
+ mut doctype_buffer := strings.new_builder(xml.default_string_builder_cap)
+ mut local_buf := [u8(0)]
+ for {
+ ch := next_char(mut reader, mut local_buf)!
+ doctype_buffer.write_u8(ch)
+ match ch {
+ `<` {
+ depth++
+ }
+ `>` {
+ depth--
+ if depth == 0 {
+ break
+ }
+ }
+ else {}
+ }
+ }
+
+ doctype_contents := doctype_buffer.str().trim_space()
+
+ name := doctype_contents.all_before('[').trim_space()
+
+ mut list_contents := doctype_contents.all_after('[').all_before(']').trim_space()
+ mut items := []DTDListItem{}
+
+ for list_contents.len > 0 {
+ if list_contents.starts_with('` {
+ if found_question_mark {
+ break
+ }
+ return error('Invalid prolog: Found ">" before "?".')
+ }
+ else {
+ if found_question_mark {
+ found_question_mark = false
+ prolog_buffer.write_u8(`?`)
+ }
+ prolog_buffer.write_u8(ch)
+ }
+ }
+ }
+
+ prolog_attributes := prolog_buffer.str().trim_space()
+
+ attributes := if prolog_attributes.len == 0 {
+ xml.default_prolog_attributes
+ } else {
+ parse_attributes(prolog_attributes)!
+ }
+
+ version := attributes['version'] or { return error('XML declaration missing version.') }
+ encoding := attributes['encoding'] or { 'UTF-8' }
+
+ mut comments := []XMLComment{}
+ mut doctype := DocumentType{
+ name: ''
+ dtd: ''
+ }
+ mut found_doctype := false
+ for {
+ ch = next_char(mut reader, mut local_buf)!
+ match ch {
+ ` `, `\t`, `\n` {
+ continue
+ }
+ `<` {
+ // We have a comment, DOCTYPE, or root node
+ ch = next_char(mut reader, mut local_buf)!
+ match ch {
+ `!` {
+ // A comment or DOCTYPE
+ match next_char(mut reader, mut local_buf)! {
+ `-` {
+ // A comment
+ if next_char(mut reader, mut local_buf)! != `-` {
+ return error('Invalid comment.')
+ }
+ comments << parse_comment(mut reader)!
+ }
+ `D` {
+ if found_doctype {
+ return error('Duplicate DOCTYPE declaration.')
+ }
+ // OCTYPE
+ mut doc_buf := []u8{len: 6}
+ if reader.read(mut doc_buf)! != 6 {
+ return error('Invalid DOCTYPE.')
+ }
+ if doc_buf != xml.doctype_chars {
+ return error('Invalid DOCTYPE.')
+ }
+ found_doctype = true
+ doctype = parse_doctype(mut reader)!
+ }
+ else {
+ return error('Unsupported control sequence found in prolog.')
+ }
+ }
+ }
+ else {
+ // We have found the start of the root node
+ break
+ }
+ }
+ }
+ else {}
+ }
+ }
+
+ return Prolog{
+ version: version
+ encoding: encoding
+ doctype: doctype
+ comments: comments
+ }, ch
+}
+
+fn parse_children(name string, attributes map[string]string, mut reader io.Reader) !XMLNode {
+ mut inner_contents := strings.new_builder(xml.default_string_builder_cap)
+
+ mut children := []XMLNodeContents{}
+ mut local_buf := [u8(0)]
+
+ for {
+ ch := next_char(mut reader, mut local_buf)!
+ match ch {
+ `<` {
+ second_char := next_char(mut reader, mut local_buf)!
+ match second_char {
+ `!` {
+ // Comment, CDATA
+ mut next_two := [u8(0), 0]
+ if reader.read(mut next_two)! != 2 {
+ return error('Invalid XML. Incomplete comment or CDATA declaration.')
+ }
+ if next_two == xml.double_dash {
+ // Comment
+ comment := parse_comment(mut reader)!
+ children << comment
+ } else if next_two == xml.c_tag {
+ // DATA
+ mut cdata_buf := []u8{len: 4}
+ if reader.read(mut cdata_buf)! != 4 {
+ return error('Invalid XML. Incomplete CDATA declaration.')
+ }
+ if cdata_buf != xml.data_chars {
+ return error('Invalid XML. Expected "CDATA" after "`
+
+ if node_end_buffer != ending_chars {
+ return error('XML node <${name}> not closed.')
+ }
+
+ collected_contents := inner_contents.str().trim_space()
+ if collected_contents.len > 0 {
+ // We have some inner text
+ children << collected_contents.replace('\r\n', '\n')
+ }
+ return XMLNode{
+ name: name
+ attributes: attributes
+ children: children
+ }
+ }
+ else {
+ // Start of child node
+ child := parse_single_node(second_char, mut reader) or {
+ if err.msg() == 'XML node cannot start with "".' {
+ return error('XML node <${name}> not closed.')
+ } else {
+ return err
+ }
+ }
+ text := inner_contents.str().trim_space()
+ if text.len > 0 {
+ children << text.replace('\r\n', '\n')
+ }
+ children << child
+ }
+ }
+ }
+ else {
+ inner_contents.write_u8(ch)
+ }
+ }
+ }
+ return error('XML node <${name}> not closed.')
+}
+
+fn parse_single_node(first_char u8, mut reader io.Reader) !XMLNode {
+ mut local_buf := [u8(0)]
+ mut ch := next_char(mut reader, mut local_buf)!
+ mut contents := strings.new_builder(xml.default_string_builder_cap)
+ // We're expecting an opening tag
+ if ch == `/` {
+ return error('XML node cannot start with "".')
+ }
+ contents.write_u8(ch)
+
+ for {
+ ch = next_char(mut reader, mut local_buf)!
+ if ch == `>` {
+ break
+ }
+ contents.write_u8(ch)
+ }
+
+ tag_contents := contents.str().trim_space()
+
+ parts := tag_contents.split_any(' \t\n')
+ name := first_char.ascii_str() + parts[0]
+
+ // Check if it is a self-closing tag
+ if tag_contents.ends_with('/') {
+ // We're not looking for children and inner text
+ return XMLNode{
+ name: name
+ attributes: parse_attributes(tag_contents[name.len - 1..tag_contents.len].trim_space())!
+ }
+ }
+
+ attribute_string := tag_contents[name.len - 1..].trim_space()
+ attributes := parse_attributes(attribute_string)!
+
+ return parse_children(name, attributes, mut reader)
+}
+
+// XMLDocument.from_string parses an XML document from a string.
+pub fn XMLDocument.from_string(raw_contents string) !XMLDocument {
+ mut reader := FullBufferReader{
+ contents: raw_contents.bytes()
+ }
+ return XMLDocument.from_reader(mut reader)!
+}
+
+// XMLDocument.from_file parses an XML document from a file. Note that the file is read in its entirety
+// and then parsed. If the file is too large, try using the XMLDocument.from_reader function instead.
+pub fn XMLDocument.from_file(path string) !XMLDocument {
+ mut reader := FullBufferReader{
+ contents: os.read_bytes(path)!
+ }
+ return XMLDocument.from_reader(mut reader)!
+}
+
+// XMLDocument.from_reader parses an XML document from a reader. This is the most generic way to parse
+// an XML document from any arbitrary source that implements that io.Reader interface.
+pub fn XMLDocument.from_reader(mut reader io.Reader) !XMLDocument {
+ prolog, first_char := parse_prolog(mut reader) or {
+ if err is os.Eof || err is io.Eof || err.msg() == 'Unexpected End Of File.' {
+ return error('XML document is empty.')
+ } else {
+ return err
+ }
+ }
+
+ root := parse_single_node(first_char, mut reader)!
+
+ return XMLDocument{
+ version: prolog.version
+ encoding: prolog.encoding
+ comments: prolog.comments
+ doctype: prolog.doctype
+ root: root
+ }
+}
diff --git a/vlib/encoding/xml/query.v b/vlib/encoding/xml/query.v
new file mode 100644
index 0000000000..9d310aff7f
--- /dev/null
+++ b/vlib/encoding/xml/query.v
@@ -0,0 +1,60 @@
+module xml
+
+fn (node XMLNode) get_element_by_id(id string) ?XMLNode {
+ // Is this the node we're looking for?
+ if attribute_id := node.attributes['id'] {
+ if attribute_id == id {
+ return node
+ }
+ }
+
+ if node.children.len == 0 {
+ return none
+ }
+
+ // Recurse into children
+ for child in node.children {
+ match child {
+ XMLNode {
+ if result := child.get_element_by_id(id) {
+ return result
+ }
+ }
+ else {}
+ }
+ }
+
+ return none
+}
+
+fn (node XMLNode) get_elements_by_tag(tag string) []XMLNode {
+ mut result := []XMLNode{}
+
+ if node.name == tag {
+ result << node
+ }
+
+ if node.children.len == 0 {
+ return result
+ }
+
+ // Recurse into children
+ for child in node.children {
+ if child is XMLNode {
+ result << child.get_elements_by_tag(tag)
+ }
+ }
+
+ return result
+}
+
+// get_element_by_id returns the first element with the given id, or none if no
+// such element exists.
+pub fn (doc XMLDocument) get_element_by_id(id string) ?XMLNode {
+ return doc.root.get_element_by_id(id)
+}
+
+// get_elements_by_tag returns all elements with the given tag name.
+pub fn (doc XMLDocument) get_elements_by_tag(tag string) []XMLNode {
+ return doc.root.get_elements_by_tag(tag)
+}
diff --git a/vlib/encoding/xml/reader_util.v b/vlib/encoding/xml/reader_util.v
new file mode 100644
index 0000000000..9ea26be97e
--- /dev/null
+++ b/vlib/encoding/xml/reader_util.v
@@ -0,0 +1,30 @@
+module xml
+
+import io
+
+fn next_char(mut reader io.Reader, mut buf []u8) !u8 {
+ if reader.read(mut buf)! == 0 {
+ return error('Unexpected End Of File.')
+ }
+ return buf[0]
+}
+
+struct FullBufferReader {
+ contents []u8
+mut:
+ position int
+}
+
+[direct_array_access]
+fn (mut fbr FullBufferReader) read(mut buf []u8) !int {
+ if fbr.position >= fbr.contents.len {
+ return io.Eof{}
+ }
+ remaining := fbr.contents.len - fbr.position
+ n := if buf.len < remaining { buf.len } else { remaining }
+ unsafe {
+ vmemcpy(&u8(buf.data), &u8(fbr.contents.data) + fbr.position, n)
+ }
+ fbr.position += n
+ return n
+}
diff --git a/vlib/encoding/xml/test/gtk/gtk_test.v b/vlib/encoding/xml/test/gtk/gtk_test.v
new file mode 100644
index 0000000000..bb8c13f38c
--- /dev/null
+++ b/vlib/encoding/xml/test/gtk/gtk_test.v
@@ -0,0 +1,89 @@
+module main
+
+import encoding.xml
+import os
+
+fn test_large_gtk_file() ! {
+ // Note: If you are contributing to this project, you should download the
+ // GIR file from https://raw.githubusercontent.com/gtk-rs/gir-files/master/Gtk-4.0.gir
+ // and place it in the same directory as this file.
+ path := os.join_path(os.dir(@FILE), 'Gtk-4.0.gir')
+ if !os.exists(path) {
+ println('Skipping test_large_gtk_file because file does not exist.')
+ return
+ }
+
+ actual := xml.XMLDocument.from_file(path) or {
+ return error('Failed to parse large GTK XML file')
+ }
+
+ mut valid := false
+ for elm in actual.get_elements_by_tag('class') {
+ if 'c:type' in elm.attributes && elm.attributes['c:type'] == 'GtkWindow' {
+ assert elm.attributes['parent'] == 'Widget'
+ assert elm.attributes['c:symbol-prefix'] == 'window'
+ valid = true
+ }
+ }
+ assert valid, 'GtkWindow class not found!'
+
+ valid = false
+ for elm in actual.get_elements_by_tag('constructor') {
+ if 'c:identifier' in elm.attributes && elm.attributes['c:identifier'] == 'gtk_window_new' {
+ assert elm == xml.XMLNode{
+ name: 'constructor'
+ attributes: {
+ 'name': 'new'
+ 'c:identifier': 'gtk_window_new'
+ }
+ children: [
+ xml.XMLNodeContents(xml.XMLNode{
+ name: 'doc'
+ attributes: {
+ 'xml:space': 'preserve'
+ }
+ children: [
+ xml.XMLNodeContents('Creates a new `GtkWindow`.
+
+To get an undecorated window (no window borders), use
+[method@Gtk.Window.set_decorated].
+
+All top-level windows created by gtk_window_new() are stored
+in an internal top-level window list. This list can be obtained
+from [func@Gtk.Window.list_toplevels]. Due to GTK keeping a
+reference to the window internally, gtk_window_new() does not
+return a reference to the caller.
+
+To delete a `GtkWindow`, call [method@Gtk.Window.destroy].'),
+ ]
+ }),
+ xml.XMLNodeContents(xml.XMLNode{
+ name: 'return-value'
+ attributes: {
+ 'transfer-ownership': 'none'
+ }
+ children: [
+ xml.XMLNodeContents(xml.XMLNode{
+ name: 'doc'
+ attributes: {
+ 'xml:space': 'preserve'
+ }
+ children: [xml.XMLNodeContents('a new `GtkWindow`.')]
+ }),
+ xml.XMLNodeContents(xml.XMLNode{
+ name: 'type'
+ attributes: {
+ 'name': 'Widget'
+ 'c:type': 'GtkWidget*'
+ }
+ children: []
+ }),
+ ]
+ }),
+ ]
+ }
+ valid = true
+ }
+ }
+ assert valid, 'gtk_window_new constructor not found!'
+}
diff --git a/vlib/encoding/xml/test/local/01_mdn_example/hello_world.xml b/vlib/encoding/xml/test/local/01_mdn_example/hello_world.xml
new file mode 100644
index 0000000000..0ecb2e96d5
--- /dev/null
+++ b/vlib/encoding/xml/test/local/01_mdn_example/hello_world.xml
@@ -0,0 +1,6 @@
+
+