Repository: godot
Issue: `XMLParser` handles white space outside tags incorrectly/inconsistently.
Labels: bug,topic:core

### Godot version

v3.3.stable.official

### System information

Linux

### Issue description

`XMLParser` treats "trailing" whitespace differently depending on whether it's two or three+ characters long.

This occurs here: https://github.com/godotengine/godot/blob/49309ac5fb2d8b33cf80c1e4b6eb0d0130f7a24a/core/io/xml_parser.cpp#L45-L46


### Origin

This behaviour appears to originate in the original `irrXML` implementation. (See, e.g. <https://sourceforge.net/p/g3d/code/HEAD/tree/G3D10/external/assimp.lib/contrib/irrXML/CXMLReaderImpl.h#l257>.)

The *why* doesn't appear to be documented but I'm guessing it was maybe intended for CR+LF end-of-line handling.

### Impact

However, as a result, it handles single-character EOL situation incorrectly--or at least differently depending on e.g. whether subsequent line indent is one tab character or multiple space characters.

Based on this comment https://github.com/godotengine/godot/issues/23895#issuecomment-442432191 this behaviour (as well as being confusing when first encountered) is probably not compliant with the specification.

### Background

I encountered this issue when my code worked with one file but didn't work with another and it wasn't obvious what the difference was between the two. Initially I suspected that it may be line-ending related but that wasn't the case so I was stumped until I looked at the parsing implementation in Godot and discovered the "special case" handling.

### Potential fix
While it would break compatibility, going forward it would seem best if this special case handling was removed or at least changed to only affect end-of-line related characters. (Depending on what the XML specification says.)

### Other impact
It seems this special case handling also means it's impossible to "round trip" certain XML files by reading the file in and then immediately writing it out again because the initial reading is "lossy" in an undetectable way.

### (Semi-) Related issues

 * https://github.com/godotengine/godot/issues/23895
 * https://github.com/godotengine/godot/issues/31682
 * https://github.com/godotengine/godot/issues/48779 & https://github.com/godotengine/godot/pull/48806
 * https://github.com/godotengine/godot/issues/28768

### Steps to reproduce

1. Run reproduction script.
2. Observe output:
``` 
----------------------------------------
buffer content: "<xml><element /></xml>"
node type: 1
           XMLParser.NODE_ELEMENT
           'xml'

node type: 1
           XMLParser.NODE_ELEMENT
           'element'

node type: 2
           XMLParser.NODE_ELEMENT_END
           'xml'

----------------------------------------
buffer content: "<xml> <element /></xml>"
node type: 1
           XMLParser.NODE_ELEMENT
           'xml'

node type: 1
           XMLParser.NODE_ELEMENT
           'element'

node type: 2
           XMLParser.NODE_ELEMENT_END
           'xml'

----------------------------------------
buffer content: "<xml>  <element /></xml>"
node type: 1
           XMLParser.NODE_ELEMENT
           'xml'

node type: 1
           XMLParser.NODE_ELEMENT
           'element'

node type: 2
           XMLParser.NODE_ELEMENT_END
           'xml'

----------------------------------------
buffer content: "<xml>   <element /></xml>"
node type: 1
           XMLParser.NODE_ELEMENT
           'xml'

node type: 3
           XMLParser.NODE_TEXT
           0x20
           0x20
           0x20

node type: 1
           XMLParser.NODE_ELEMENT
           'element'

node type: 2
           XMLParser.NODE_ELEMENT_END
           'xml'
```
Note that the last example suddenly shows an additional `NODE_TEXT` node which was missing from the previous two examples which also included whitespace characters (but fewer than 3).

### Minimal reproduction project

```gdscript
extends Control

# Note: Oddly, Godot treats "trailing" whitespace differently
#       depending on whether it's two or three+ characters long.
#
#       See: <https://github.com/godotengine/godot/blame/49309ac5fb2d8b33cf80c1e4b6eb0d0130f7a24a/core/io/xml_parser.cpp#L45-L46>
#
#       This behaviour appears to originate in the original
#       `irrXML` implementation.
#
#       See, e.g.: <https://sourceforge.net/p/g3d/code/HEAD/tree/G3D10/external/assimp.lib/contrib/irrXML/CXMLReaderImpl.h#l257>
#
#       The *why* doesn't appear to be documented but I'm
#       guessing it was maybe intended for CR+LF end-of-line
#       handling.
#
#       However, as a result, it handles single-character EOL
#       situation incorrectly--or at least differently depending
#       on e.g. whether subsequent line indent is one tab character
#       or multiple space characters.

var test_xml_buffer_1 = "<xml>\r<element /></xml>"
var test_xml_buffer_2 = "<xml>\r\t<element /></xml>"

var test_xml_buffer_3 = "<xml>\r <element /></xml>"
var test_xml_buffer_4 = "<xml>\r  <element /></xml>"

# While I encountered this issue with content that included non-printing end-of-line
# characters (shown above) this set of test data uses only spaces because that
# makes it easier to see the differences between each example:
var test_xml_buffer_5 = "<xml><element /></xml>"
var test_xml_buffer_6 = "<xml> <element /></xml>"
var test_xml_buffer_7 = "<xml>  <element /></xml>"
var test_xml_buffer_8 = "<xml>   <element /></xml>"


func parse_xml(test_xml_buffer) -> void:

    print("-".repeat(40))
    print("buffer content: \"%s\"" % test_xml_buffer)

    var xml_parser: XMLParser = XMLParser.new()

    var err = xml_parser.open_buffer(test_xml_buffer.to_utf8())
    if err != OK:
        push_error("error: %s" % err)
        return


    while true:

        err = xml_parser.read()
        if err != OK:
            push_error("error: %s" % err)
            return

        var node_type = xml_parser.get_node_type()
        var node_type_str = ""
        var node_name = ""
        var node_data = ""

        prints("node type:", node_type)

        node_type_str = [
            "XMLParser.NODE_NONE",
            "XMLParser.NODE_ELEMENT",
            "XMLParser.NODE_ELEMENT_END",
            "XMLParser.NODE_TEXT",
            # ...
            ][node_type]

        prints("          ", node_type_str)

        match node_type:

            XMLParser.NODE_ELEMENT, XMLParser.NODE_ELEMENT_END:
                node_name = xml_parser.get_node_name()
                prints("          ", "'%s'" % node_name)

            XMLParser.NODE_TEXT:
                node_data = xml_parser.get_node_data()

                for i in range(node_data.length()):
                    prints("          ", "0x%02x" % ord(node_data.substr(i,1)))

            _:
                push_error("Unhandled node type.")

        print()


func _ready() -> void:

    parse_xml(test_xml_buffer_5)
    parse_xml(test_xml_buffer_6)
    parse_xml(test_xml_buffer_7)
    parse_xml(test_xml_buffer_8)

```