From 5d32c5b54f35e55cc257d1926244e25dd5d07586 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:08:31 +1000 Subject: [PATCH 01/13] Add XML node abstractions This commit introduces the core XML node abstractions for tags, declarations, attributes, and text content. It adds reusable interfaces and concrete node classes with serialization, escaping, and child-node support for building basic XML documents. --- include/xtrpg/xml/node/IAttributes.hpp | 85 +++++++++++++++++ include/xtrpg/xml/node/INode.hpp | 47 ++++++++++ include/xtrpg/xml/node/ITagname.hpp | 28 ++++++ include/xtrpg/xml/node/NodeType.hpp | 10 ++ include/xtrpg/xml/node/XmlDeclarationTag.hpp | 31 +++++++ include/xtrpg/xml/node/XmlTagNode.hpp | 92 ++++++++++++++++++ include/xtrpg/xml/node/XmlTextContent.hpp | 98 ++++++++++++++++++++ 7 files changed, 391 insertions(+) create mode 100644 include/xtrpg/xml/node/IAttributes.hpp create mode 100644 include/xtrpg/xml/node/INode.hpp create mode 100644 include/xtrpg/xml/node/ITagname.hpp create mode 100644 include/xtrpg/xml/node/NodeType.hpp create mode 100644 include/xtrpg/xml/node/XmlDeclarationTag.hpp create mode 100644 include/xtrpg/xml/node/XmlTagNode.hpp create mode 100644 include/xtrpg/xml/node/XmlTextContent.hpp diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp new file mode 100644 index 0000000..6fb517d --- /dev/null +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include + +namespace xtrpg::xml::node { + +/** + * Provides storage, lookup, mutation, iteration, and serialization for XML + * attributes. + */ +class IAttributes { +public: + /** + * Virtual destructor. + */ + virtual ~IAttributes() = default; + + /** + * Sets an attribute key/value pair. + */ + void setAttribute(std::string_view key, std::string_view value) { + this->_attributes[std::string(key)] = std::string(value); + } + + /** + * Returns the value associated with the given attribute key. + */ + std::optional getAttribute(const std::string &key) const { + auto it = this->_attributes.find(key); + if (it != this->_attributes.end()) { + return it->second; + } + return std::nullopt; + } + + /** + * Removes a given attribute if it exists, returning the value that was + * removed. + */ + std::optional removeAttribute(const std::string &key) { + auto node = this->_attributes.extract(key); + if (node.empty()) { + return std::nullopt; + } + return std::move(node.mapped()); + } + + /** + * Executes a callback function for each key/value attribute pair. + * + * Callback signature: void(std::string_view key, std::string_view value) + */ + template void forEachAttribute(Func &&callback) const { + for (const auto &[key, value] : this->_attributes) { + callback(std::string_view{key}, std::string_view{value}); + } + } + + /** + * Serializes the node into an XML formatted string. + */ + void serialize(std::ostream &os) const { + for (const auto &[attr, val] : this->_attributes) { + os << " " << attr << "=\""; + // Escape attribute values + for (char c : val) { + if (c == '"') + os << """; + else if (c == '&') + os << "&"; + else + os << c; + } + os << "\""; + } + } + +private: + std::unordered_map _attributes; +}; + +} // namespace xtrpg::xml \ No newline at end of file diff --git a/include/xtrpg/xml/node/INode.hpp b/include/xtrpg/xml/node/INode.hpp new file mode 100644 index 0000000..6016f69 --- /dev/null +++ b/include/xtrpg/xml/node/INode.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "xtrpg/xml/node/NodeType.hpp" +#include + +namespace xtrpg::xml::node { + +/** + * Defines the common interface and node type metadata for XML nodes. + */ +class INode { +public: + /** + * Constructor that takes in a node type. + */ + explicit INode(NodeType type) : _type(type) {} + + /** + * Virtual destructor. + */ + virtual ~INode() = default; + + /** + * Virtual method to serialize the node into the provided output stream. + */ + virtual void serialize(std::ostream &os) const = 0; + + /** + * Returns the type of this node. + */ + NodeType getNodeType() const { return this->_type; }; + + bool isType(NodeType type) const { return this->_type == type; } + +private: + NodeType _type; +}; + +/** + * Stream operator overload for easy serialization + */ +inline std::ostream &operator<<(std::ostream &os, const INode &node) { + node.serialize(os); + return os; +} + +} // namespace xtrpg::xml \ No newline at end of file diff --git a/include/xtrpg/xml/node/ITagname.hpp b/include/xtrpg/xml/node/ITagname.hpp new file mode 100644 index 0000000..97be382 --- /dev/null +++ b/include/xtrpg/xml/node/ITagname.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace xtrpg::xml::node { + +/** + * Stores and exposes the canonical name associated with an XML tag. + */ +class ITagname { +public: + /** + * Constructor that takes in the canonical name for the tag. + */ + explicit ITagname(std::string tagname) : _tagname(std::move(tagname)) {} + + /** + * Virtual destructor. + */ + virtual ~ITagname() = default; + + std::string_view getTagname() const { return this->_tagname; } + +private: + std::string _tagname; +}; + +} // namespace xtrpg::xml \ No newline at end of file diff --git a/include/xtrpg/xml/node/NodeType.hpp b/include/xtrpg/xml/node/NodeType.hpp new file mode 100644 index 0000000..00755c7 --- /dev/null +++ b/include/xtrpg/xml/node/NodeType.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace xtrpg::xml::node { + +/** + * Identifies the supported XML node categories. + */ +enum class NodeType { TAG, DECLARATION, TEXT_CONTENT }; + +} // namespace xtrpg::xml::node \ No newline at end of file diff --git a/include/xtrpg/xml/node/XmlDeclarationTag.hpp b/include/xtrpg/xml/node/XmlDeclarationTag.hpp new file mode 100644 index 0000000..d7eb6c2 --- /dev/null +++ b/include/xtrpg/xml/node/XmlDeclarationTag.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "xtrpg/xml/node/IAttributes.hpp" +#include "xtrpg/xml/node/INode.hpp" +#include "xtrpg/xml/node/ITagname.hpp" +#include "xtrpg/xml/node/NodeType.hpp" + +namespace xtrpg::xml::node { + +/** + * Represents an XML declaration node with a tag name and attributes. + */ +class XmlDeclarationTag : public INode, public ITagname, public IAttributes { +public: + /** + * Inline constructor that accepts a tag name. + */ + explicit XmlDeclarationTag(std::string tagname) + : INode(NodeType::DECLARATION), ITagname(tagname), IAttributes() {} + + /** + * Serializes the node into an XML formatted string. + */ + void serialize(std::ostream &os) const override { + os << "getTagname(); + IAttributes::serialize(os); + os << "?>"; + } +}; + +} // namespace xtrpg::xml::node \ No newline at end of file diff --git a/include/xtrpg/xml/node/XmlTagNode.hpp b/include/xtrpg/xml/node/XmlTagNode.hpp new file mode 100644 index 0000000..0aabb71 --- /dev/null +++ b/include/xtrpg/xml/node/XmlTagNode.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "xtrpg/xml/node/IAttributes.hpp" +#include "xtrpg/xml/node/INode.hpp" +#include "xtrpg/xml/node/ITagname.hpp" +#include "xtrpg/xml/node/NodeType.hpp" +#include "xtrpg/xml/node/XmlTextContent.hpp" + +namespace xtrpg::xml::node { + +/** + * Represents an XML element with a tag name, attributes, and child nodes. + */ +class XmlTagNode : public INode, public ITagname, public IAttributes { +public: + /** + * Inline constructor that accepts a tag name. + */ + explicit XmlTagNode(std::string name) + : INode(NodeType::TAG), ITagname(name), IAttributes() {} + + /** + * Returns a reference to the name of the tag. + */ + const std::string_view name() const { return this->getTagname(); } + + /** + * Appends a given child node. + */ + void append(std::shared_ptr child) { + if (child) { + this->_children.push_back(std::move(child)); + } + } + + /** + * Returns a vector of child nodes. + */ + const std::vector> &children() const { + return this->_children; + } + + /** + * Serializes the node into an XML formatted string. + */ + void serialize(std::ostream &os) const override { + os << "<" << this->getTagname(); + + IAttributes::serialize(os); + + if (this->_children.empty()) { + os << "/>"; + return; + } + + os << ">"; + for (const auto &child : this->_children) { + child->serialize(os); + } + os << "getTagname() << ">"; + } + +private: + std::vector> _children; +}; + +/** + * Stream operator overload for easy serialization + */ +inline XmlTagNode &operator<<(XmlTagNode &node, std::shared_ptr child) { + node.append(std::move(child)); + return node; +} + +/** + * Stream operator overload for easy serialization, appends a new Text Node + * child containing the provided text. + */ +inline XmlTagNode &operator<<(XmlTagNode &node, const std::string &withText) { + auto textNode = std::make_shared(withText); + node.append(textNode); + return node; +} + +} // namespace xtrpg::xml::node \ No newline at end of file diff --git a/include/xtrpg/xml/node/XmlTextContent.hpp b/include/xtrpg/xml/node/XmlTextContent.hpp new file mode 100644 index 0000000..b82a87a --- /dev/null +++ b/include/xtrpg/xml/node/XmlTextContent.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include "xtrpg/xml/node/INode.hpp" +#include "xtrpg/xml/node/NodeType.hpp" +#include + +namespace xtrpg::xml::node { + +/** + * Represents text content within an XML document and escapes it on output. + */ +class XmlTextContent : public INode { +public: + /** + * Default constructor that generate a blank text node. + */ + XmlTextContent() : INode(NodeType::TEXT_CONTENT) {} + + /** + * Constructor that takes in a copy of the text content to store within this + * node. + */ + explicit XmlTextContent(std::string content) + : INode(NodeType::TEXT_CONTENT), _content(std::move(content)) {} + + /** + * Returns a reference to the text content stored on this node. + */ + const std::string &content() const { return this->_content; } + + /** + * Sets the text content stored on this node, overriding any previous content. + */ + void content(std::string withContent) { + this->_content = std::move(withContent); + } + + /** + * Appends the provided string to the end of the stored text content. + */ + void append(const std::string &withText) { this->_content += withText; } + + /** + * Serializes the text content into the provided output stream, automatically + * escaping and special XML entities. + */ + void serialize(std::ostream &os) const override { + for (char c : this->_content) { + switch (c) { + case '<': + os << "<"; + break; + case '>': + os << ">"; + break; + case '&': + os << "&"; + break; + case '"': + os << """; + break; + case '\'': + os << "'"; + break; + default: + os << c; + break; + } + } + } + +private: + std::string _content; +}; + +/** + * Stream insertion overload. (node << text). + */ +inline XmlTextContent &operator<<(XmlTextContent &node, + const std::string &withText) { + node.append(withText); + return node; +} + +/** + * Stream extraction overload: reads line-by-line (standard >> behavior) and + * appends to the content of the node. + */ +inline std::istream &operator>>(std::istream &is, XmlTextContent &node) { + std::string temp; + if (std::getline(is, temp)) { + node.append(temp); + node.append("\n"); + } + return is; +} + +} // namespace xtrpg::xml::node \ No newline at end of file From cfd964e890ae26c50c3a68ff5cb682aa7e9e8eda Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:10:00 +1000 Subject: [PATCH 02/13] Escape XML attribute less-than sign Fix XML attribute serialization by escaping '<' as '<' when writing attribute values. This keeps attribute contents valid XML and prevents malformed output when attribute text contains comparison operators or angle brackets. --- include/xtrpg/xml/node/IAttributes.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp index 6fb517d..1dc5f1e 100644 --- a/include/xtrpg/xml/node/IAttributes.hpp +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -71,6 +71,8 @@ class IAttributes { os << """; else if (c == '&') os << "&"; + else if (c == '<') + os << "<"; else os << c; } From b464aea3742b02362e4fd3bc0eec54d391ca2ad2 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:15:27 +1000 Subject: [PATCH 03/13] Validate XML names in node APIs This change adds a shared XML name validator and enforces it when creating tag names and setting attributes. Invalid names now throw std::invalid_argument instead of being accepted silently, preventing malformed XML content at the API boundary. --- include/xtrpg/xml/node/IAttributes.hpp | 8 +++++- include/xtrpg/xml/node/ITagname.hpp | 10 ++++++- include/xtrpg/xml/node/XmlName.hpp | 38 ++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 include/xtrpg/xml/node/XmlName.hpp diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp index 1dc5f1e..592036c 100644 --- a/include/xtrpg/xml/node/IAttributes.hpp +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -2,9 +2,12 @@ #include #include +#include #include #include +#include "xtrpg/xml/node/XmlName.hpp" + namespace xtrpg::xml::node { /** @@ -22,6 +25,9 @@ class IAttributes { * Sets an attribute key/value pair. */ void setAttribute(std::string_view key, std::string_view value) { + if (!isValidXmlName(key)) { + throw std::invalid_argument("Invalid XML attribute name"); + } this->_attributes[std::string(key)] = std::string(value); } @@ -84,4 +90,4 @@ class IAttributes { std::unordered_map _attributes; }; -} // namespace xtrpg::xml \ No newline at end of file +} // namespace xtrpg::xml::node \ No newline at end of file diff --git a/include/xtrpg/xml/node/ITagname.hpp b/include/xtrpg/xml/node/ITagname.hpp index 97be382..8508ebb 100644 --- a/include/xtrpg/xml/node/ITagname.hpp +++ b/include/xtrpg/xml/node/ITagname.hpp @@ -1,6 +1,10 @@ #pragma once +#include #include +#include + +#include "xtrpg/xml/node/XmlName.hpp" namespace xtrpg::xml::node { @@ -12,7 +16,11 @@ class ITagname { /** * Constructor that takes in the canonical name for the tag. */ - explicit ITagname(std::string tagname) : _tagname(std::move(tagname)) {} + explicit ITagname(std::string tagname) : _tagname(std::move(tagname)) { + if (!isValidXmlName(this->_tagname)) { + throw std::invalid_argument("Invalid XML tag name"); + } + } /** * Virtual destructor. diff --git a/include/xtrpg/xml/node/XmlName.hpp b/include/xtrpg/xml/node/XmlName.hpp new file mode 100644 index 0000000..9f35867 --- /dev/null +++ b/include/xtrpg/xml/node/XmlName.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +namespace xtrpg::xml::node { + +/** + * Returns whether a string is a valid XML name using the ASCII XML Name + * character set. + */ +inline bool isValidXmlName(std::string_view name) { + if (name.empty()) { + return false; + } + + const auto isNameStart = [](unsigned char character) { + return (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z') || character == ':' || + character == '_'; + }; + const auto isNameCharacter = [&](unsigned char character) { + return isNameStart(character) || (character >= '0' && character <= '9') || + character == '.' || character == '-'; + }; + + if (!isNameStart(static_cast(name.front()))) { + return false; + } + + for (const unsigned char character : name.substr(1)) { + if (!isNameCharacter(character)) { + return false; + } + } + return true; +} + +} // namespace xtrpg::xml::node From 0d9c99c60b5bd81727bb698205c839e3bca446a3 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:17:56 +1000 Subject: [PATCH 04/13] Validate XML character data in text and attributes Add XmlCharacter.hpp (isValidXmlCharacterData) to enforce XML 1.0 character rules (reject control chars < 0x20 except tab/newline/carriage return). Use it in IAttributes to validate attribute values and in XmlTextContent (ctor, setter, append) to validate text content. Also add necessary includes and throw std::invalid_argument on invalid input. --- include/xtrpg/xml/node/IAttributes.hpp | 4 ++++ include/xtrpg/xml/node/XmlCharacter.hpp | 21 +++++++++++++++++++++ include/xtrpg/xml/node/XmlTextContent.hpp | 18 ++++++++++++++++-- 3 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 include/xtrpg/xml/node/XmlCharacter.hpp diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp index 592036c..9df8118 100644 --- a/include/xtrpg/xml/node/IAttributes.hpp +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -6,6 +6,7 @@ #include #include +#include "xtrpg/xml/node/XmlCharacter.hpp" #include "xtrpg/xml/node/XmlName.hpp" namespace xtrpg::xml::node { @@ -28,6 +29,9 @@ class IAttributes { if (!isValidXmlName(key)) { throw std::invalid_argument("Invalid XML attribute name"); } + if (!isValidXmlCharacterData(value)) { + throw std::invalid_argument("Invalid character in XML attribute value"); + } this->_attributes[std::string(key)] = std::string(value); } diff --git a/include/xtrpg/xml/node/XmlCharacter.hpp b/include/xtrpg/xml/node/XmlCharacter.hpp new file mode 100644 index 0000000..eeeecc9 --- /dev/null +++ b/include/xtrpg/xml/node/XmlCharacter.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace xtrpg::xml::node { + +/** + * Returns whether a string contains only characters permitted in XML 1.0 + * character data and attribute values. + */ +inline bool isValidXmlCharacterData(std::string_view value) { + for (const unsigned char character : value) { + if (character < 0x20 && character != '\t' && character != '\n' && + character != '\r') { + return false; + } + } + return true; +} + +} // namespace xtrpg::xml::node diff --git a/include/xtrpg/xml/node/XmlTextContent.hpp b/include/xtrpg/xml/node/XmlTextContent.hpp index b82a87a..2f2b3b4 100644 --- a/include/xtrpg/xml/node/XmlTextContent.hpp +++ b/include/xtrpg/xml/node/XmlTextContent.hpp @@ -2,6 +2,8 @@ #include "xtrpg/xml/node/INode.hpp" #include "xtrpg/xml/node/NodeType.hpp" +#include "xtrpg/xml/node/XmlCharacter.hpp" +#include #include namespace xtrpg::xml::node { @@ -21,7 +23,11 @@ class XmlTextContent : public INode { * node. */ explicit XmlTextContent(std::string content) - : INode(NodeType::TEXT_CONTENT), _content(std::move(content)) {} + : INode(NodeType::TEXT_CONTENT), _content(std::move(content)) { + if (!isValidXmlCharacterData(this->_content)) { + throw std::invalid_argument("Invalid character in XML text content"); + } + } /** * Returns a reference to the text content stored on this node. @@ -32,13 +38,21 @@ class XmlTextContent : public INode { * Sets the text content stored on this node, overriding any previous content. */ void content(std::string withContent) { + if (!isValidXmlCharacterData(withContent)) { + throw std::invalid_argument("Invalid character in XML text content"); + } this->_content = std::move(withContent); } /** * Appends the provided string to the end of the stored text content. */ - void append(const std::string &withText) { this->_content += withText; } + void append(const std::string &withText) { + if (!isValidXmlCharacterData(withText)) { + throw std::invalid_argument("Invalid character in XML text content"); + } + this->_content += withText; + } /** * Serializes the text content into the provided output stream, automatically From edf8dd0587d94dd05d32cd7f57c135f645875ff6 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:19:42 +1000 Subject: [PATCH 05/13] Return XML attribute values by value The XML attribute accessor was returning std::optional, which can dangle when the underlying stored value is not guaranteed to outlive the call. This change returns a copied std::string instead, making the API safe and consistent with callers that need an owned value. --- include/xtrpg/xml/node/IAttributes.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp index 9df8118..5b0e194 100644 --- a/include/xtrpg/xml/node/IAttributes.hpp +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -36,9 +36,9 @@ class IAttributes { } /** - * Returns the value associated with the given attribute key. + * Returns a copy of the value associated with the given attribute key. */ - std::optional getAttribute(const std::string &key) const { + std::optional getAttribute(const std::string &key) const { auto it = this->_attributes.find(key); if (it != this->_attributes.end()) { return it->second; From 982c20544a4d1d9c2fa5ec8d37102e23ec98b907 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:24:06 +1000 Subject: [PATCH 06/13] Avoid cycles when appending XmlTagNode children Add cycle detection to XmlTagNode::append: attempting to append a child that would create a cycle now throws std::invalid_argument. Implement containsNode (iterative DFS with a visited set) to detect reachability. Include necessary headers (, ). --- include/xtrpg/xml/node/XmlTagNode.hpp | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/include/xtrpg/xml/node/XmlTagNode.hpp b/include/xtrpg/xml/node/XmlTagNode.hpp index 0aabb71..7a0a46e 100644 --- a/include/xtrpg/xml/node/XmlTagNode.hpp +++ b/include/xtrpg/xml/node/XmlTagNode.hpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include #include "xtrpg/xml/node/IAttributes.hpp" @@ -33,9 +35,14 @@ class XmlTagNode : public INode, public ITagname, public IAttributes { /** * Appends a given child node. + * + * @throws std::invalid_argument if the child already contains this node. */ void append(std::shared_ptr child) { if (child) { + if (containsNode(child.get(), this)) { + throw std::invalid_argument("Cannot create a cycle in XML nodes"); + } this->_children.push_back(std::move(child)); } } @@ -68,6 +75,35 @@ class XmlTagNode : public INode, public ITagname, public IAttributes { } private: + /** + * Checks whether target is reachable through an XML tag node subtree. + */ + static bool containsNode(const INode *root, const INode *target) { + std::vector pending{root}; + std::unordered_set visited; + + while (!pending.empty()) { + const INode *current = pending.back(); + pending.pop_back(); + + if (!current || !visited.insert(current).second) { + continue; + } + if (current == target) { + return true; + } + + const auto *tag = dynamic_cast(current); + if (!tag) { + continue; + } + for (const auto &child : tag->_children) { + pending.push_back(child.get()); + } + } + return false; + } + std::vector> _children; }; From 66b2adfce691c9ab0c746c937cb753b4bca6eea7 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:26:14 +1000 Subject: [PATCH 07/13] Validate XML declaration tags This change enforces XML declaration correctness by requiring the tag name to be `xml`, validating required `version` values (`1.0` or `1.1`), and rejecting unsupported attributes, invalid encoding names, and invalid standalone values during serialization. It also serializes declarations in the standard `` form with optional `encoding` and `standalone` attributes. --- include/xtrpg/xml/node/XmlDeclarationTag.hpp | 68 ++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/include/xtrpg/xml/node/XmlDeclarationTag.hpp b/include/xtrpg/xml/node/XmlDeclarationTag.hpp index d7eb6c2..ec9268d 100644 --- a/include/xtrpg/xml/node/XmlDeclarationTag.hpp +++ b/include/xtrpg/xml/node/XmlDeclarationTag.hpp @@ -4,6 +4,8 @@ #include "xtrpg/xml/node/INode.hpp" #include "xtrpg/xml/node/ITagname.hpp" #include "xtrpg/xml/node/NodeType.hpp" +#include +#include namespace xtrpg::xml::node { @@ -13,19 +15,77 @@ namespace xtrpg::xml::node { class XmlDeclarationTag : public INode, public ITagname, public IAttributes { public: /** - * Inline constructor that accepts a tag name. + * Constructs an XML declaration. */ explicit XmlDeclarationTag(std::string tagname) - : INode(NodeType::DECLARATION), ITagname(tagname), IAttributes() {} + : INode(NodeType::DECLARATION), ITagname(std::move(tagname)), + IAttributes() { + if (this->getTagname() != "xml") { + throw std::invalid_argument("XML declaration name must be 'xml'"); + } + } /** * Serializes the node into an XML formatted string. */ void serialize(std::ostream &os) const override { - os << "getTagname(); - IAttributes::serialize(os); + const auto version = this->getAttribute("version"); + if (!version || (*version != "1.0" && *version != "1.1")) { + throw std::invalid_argument( + "XML declaration requires version 1.0 or 1.1"); + } + + bool hasInvalidAttribute = false; + this->forEachAttribute([&](std::string_view key, std::string_view) { + if (key != "version" && key != "encoding" && key != "standalone") { + hasInvalidAttribute = true; + } + }); + if (hasInvalidAttribute) { + throw std::invalid_argument("Invalid XML declaration attribute"); + } + + if (const auto encoding = this->getAttribute("encoding")) { + if (!isValidEncodingName(*encoding)) { + throw std::invalid_argument("Invalid XML declaration encoding"); + } + } + if (const auto standalone = this->getAttribute("standalone")) { + if (*standalone != "yes" && *standalone != "no") { + throw std::invalid_argument( + "XML declaration standalone must be 'yes' or 'no'"); + } + } + + os << "getAttribute("encoding")) { + os << " encoding=\"" << *encoding << "\""; + } + if (const auto standalone = this->getAttribute("standalone")) { + os << " standalone=\"" << *standalone << "\""; + } os << "?>"; } + +private: + static bool isValidEncodingName(std::string_view encoding) { + if (encoding.empty()) { + return false; + } + for (const unsigned char character : encoding) { + const bool isLetter = (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z'); + const bool isAllowed = isLetter || (character >= '0' && character <= '9') || + character == '.' || character == '_' || + character == '-'; + if (!isAllowed) { + return false; + } + } + const unsigned char first = static_cast(encoding.front()); + return (first >= 'A' && first <= 'Z') || + (first >= 'a' && first <= 'z'); + } }; } // namespace xtrpg::xml::node \ No newline at end of file From da02ebd9602514e70c89fa49b214d8281514f359 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:27:40 +1000 Subject: [PATCH 08/13] Add missing XML node includes These headers were using std::string_view and related standard library types without including the required headers. This patch adds the missing includes to the XML node interfaces and content classes so they compile cleanly and remain self-contained. --- include/xtrpg/xml/node/IAttributes.hpp | 2 ++ include/xtrpg/xml/node/ITagname.hpp | 1 + include/xtrpg/xml/node/XmlDeclarationTag.hpp | 1 + include/xtrpg/xml/node/XmlTagNode.hpp | 2 ++ include/xtrpg/xml/node/XmlTextContent.hpp | 2 ++ 5 files changed, 8 insertions(+) diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp index 5b0e194..1928105 100644 --- a/include/xtrpg/xml/node/IAttributes.hpp +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -4,7 +4,9 @@ #include #include #include +#include #include +#include #include "xtrpg/xml/node/XmlCharacter.hpp" #include "xtrpg/xml/node/XmlName.hpp" diff --git a/include/xtrpg/xml/node/ITagname.hpp b/include/xtrpg/xml/node/ITagname.hpp index 8508ebb..bce4fe5 100644 --- a/include/xtrpg/xml/node/ITagname.hpp +++ b/include/xtrpg/xml/node/ITagname.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "xtrpg/xml/node/XmlName.hpp" diff --git a/include/xtrpg/xml/node/XmlDeclarationTag.hpp b/include/xtrpg/xml/node/XmlDeclarationTag.hpp index ec9268d..f2a42ee 100644 --- a/include/xtrpg/xml/node/XmlDeclarationTag.hpp +++ b/include/xtrpg/xml/node/XmlDeclarationTag.hpp @@ -4,6 +4,7 @@ #include "xtrpg/xml/node/INode.hpp" #include "xtrpg/xml/node/ITagname.hpp" #include "xtrpg/xml/node/NodeType.hpp" +#include #include #include diff --git a/include/xtrpg/xml/node/XmlTagNode.hpp b/include/xtrpg/xml/node/XmlTagNode.hpp index 7a0a46e..a3a43ab 100644 --- a/include/xtrpg/xml/node/XmlTagNode.hpp +++ b/include/xtrpg/xml/node/XmlTagNode.hpp @@ -5,8 +5,10 @@ #include #include #include +#include #include #include +#include #include #include "xtrpg/xml/node/IAttributes.hpp" diff --git a/include/xtrpg/xml/node/XmlTextContent.hpp b/include/xtrpg/xml/node/XmlTextContent.hpp index 2f2b3b4..f9dcc34 100644 --- a/include/xtrpg/xml/node/XmlTextContent.hpp +++ b/include/xtrpg/xml/node/XmlTextContent.hpp @@ -3,8 +3,10 @@ #include "xtrpg/xml/node/INode.hpp" #include "xtrpg/xml/node/NodeType.hpp" #include "xtrpg/xml/node/XmlCharacter.hpp" +#include #include #include +#include namespace xtrpg::xml::node { From fc45ea9f948293a122736f1b58ad93849dd48862 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:29:32 +1000 Subject: [PATCH 09/13] Sort XML attributes for stable serialization Serialize XML attributes in a deterministic key order instead of the underlying hash-map order so output is stable and predictable. The change also keeps attribute escaping intact while writing values. Minor formatting/include-order cleanups were applied to the XML declaration and tag headers. --- include/xtrpg/xml/node/IAttributes.hpp | 14 +++++++++++++- include/xtrpg/xml/node/XmlDeclarationTag.hpp | 11 +++++------ include/xtrpg/xml/node/XmlTagNode.hpp | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp index 1928105..b6de86a 100644 --- a/include/xtrpg/xml/node/IAttributes.hpp +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include "xtrpg/xml/node/XmlCharacter.hpp" #include "xtrpg/xml/node/XmlName.hpp" @@ -72,10 +74,20 @@ class IAttributes { } /** - * Serializes the node into an XML formatted string. + * Serializes attributes into an XML formatted string in key order. */ void serialize(std::ostream &os) const { + std::vector> attributes; + attributes.reserve(this->_attributes.size()); for (const auto &[attr, val] : this->_attributes) { + attributes.emplace_back(attr, val); + } + std::sort(attributes.begin(), attributes.end(), + [](const auto &left, const auto &right) { + return left.first < right.first; + }); + + for (const auto &[attr, val] : attributes) { os << " " << attr << "=\""; // Escape attribute values for (char c : val) { diff --git a/include/xtrpg/xml/node/XmlDeclarationTag.hpp b/include/xtrpg/xml/node/XmlDeclarationTag.hpp index f2a42ee..ca8f4d0 100644 --- a/include/xtrpg/xml/node/XmlDeclarationTag.hpp +++ b/include/xtrpg/xml/node/XmlDeclarationTag.hpp @@ -4,8 +4,8 @@ #include "xtrpg/xml/node/INode.hpp" #include "xtrpg/xml/node/ITagname.hpp" #include "xtrpg/xml/node/NodeType.hpp" -#include #include +#include #include namespace xtrpg::xml::node { @@ -76,16 +76,15 @@ class XmlDeclarationTag : public INode, public ITagname, public IAttributes { for (const unsigned char character : encoding) { const bool isLetter = (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z'); - const bool isAllowed = isLetter || (character >= '0' && character <= '9') || - character == '.' || character == '_' || - character == '-'; + const bool isAllowed = + isLetter || (character >= '0' && character <= '9') || + character == '.' || character == '_' || character == '-'; if (!isAllowed) { return false; } } const unsigned char first = static_cast(encoding.front()); - return (first >= 'A' && first <= 'Z') || - (first >= 'a' && first <= 'z'); + return (first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z'); } }; diff --git a/include/xtrpg/xml/node/XmlTagNode.hpp b/include/xtrpg/xml/node/XmlTagNode.hpp index a3a43ab..6d50a96 100644 --- a/include/xtrpg/xml/node/XmlTagNode.hpp +++ b/include/xtrpg/xml/node/XmlTagNode.hpp @@ -3,8 +3,8 @@ #include #include #include -#include #include +#include #include #include #include From a4ff0d9d1ff6b7ee624eceb444dfc7cf818054a4 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:32:37 +1000 Subject: [PATCH 10/13] Use ordered map for XML attributes Replace the unordered attribute container with std::map and remove the extra sort step during XML serialization. This keeps attribute output deterministic and in key order while simplifying the implementation and reducing unnecessary work. --- include/xtrpg/xml/node/IAttributes.hpp | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp index b6de86a..facec8e 100644 --- a/include/xtrpg/xml/node/IAttributes.hpp +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -1,14 +1,12 @@ #pragma once -#include #include +#include #include #include #include #include -#include #include -#include #include "xtrpg/xml/node/XmlCharacter.hpp" #include "xtrpg/xml/node/XmlName.hpp" @@ -73,21 +71,9 @@ class IAttributes { } } - /** - * Serializes attributes into an XML formatted string in key order. - */ + /** Serializes attributes into an XML formatted string in key order. */ void serialize(std::ostream &os) const { - std::vector> attributes; - attributes.reserve(this->_attributes.size()); for (const auto &[attr, val] : this->_attributes) { - attributes.emplace_back(attr, val); - } - std::sort(attributes.begin(), attributes.end(), - [](const auto &left, const auto &right) { - return left.first < right.first; - }); - - for (const auto &[attr, val] : attributes) { os << " " << attr << "=\""; // Escape attribute values for (char c : val) { @@ -105,7 +91,7 @@ class IAttributes { } private: - std::unordered_map _attributes; + std::map _attributes; }; } // namespace xtrpg::xml::node \ No newline at end of file From 8bdf80d3cf80f0c93b58f00b152c18750f5f68fd Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:34:16 +1000 Subject: [PATCH 11/13] Don't escape quotes in XML text content Remove handling that converted '"' and '\'' to " and ' in XmlTextContent output. Quotes are valid in XML character data and should only be escaped in attribute values; this avoids unnecessary escaping or double-encoding in text nodes. Change made in include/xtrpg/xml/node/XmlTextContent.hpp. --- include/xtrpg/xml/node/XmlTextContent.hpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/include/xtrpg/xml/node/XmlTextContent.hpp b/include/xtrpg/xml/node/XmlTextContent.hpp index f9dcc34..6b0d088 100644 --- a/include/xtrpg/xml/node/XmlTextContent.hpp +++ b/include/xtrpg/xml/node/XmlTextContent.hpp @@ -72,12 +72,6 @@ class XmlTextContent : public INode { case '&': os << "&"; break; - case '"': - os << """; - break; - case '\'': - os << "'"; - break; default: os << c; break; From e9f37c592114372a152107de32e1bc20c359613b Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:38:12 +1000 Subject: [PATCH 12/13] Fix XmlTextContent line extraction The stream extraction overload for XmlTextContent was appending a newline after each read line. This patch removes that delimiter so a single line is appended exactly as read, preserving the original text content without introducing extra line breaks. --- include/xtrpg/xml/node/XmlTextContent.hpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/include/xtrpg/xml/node/XmlTextContent.hpp b/include/xtrpg/xml/node/XmlTextContent.hpp index 6b0d088..d5dc6dc 100644 --- a/include/xtrpg/xml/node/XmlTextContent.hpp +++ b/include/xtrpg/xml/node/XmlTextContent.hpp @@ -93,14 +93,13 @@ inline XmlTextContent &operator<<(XmlTextContent &node, } /** - * Stream extraction overload: reads line-by-line (standard >> behavior) and - * appends to the content of the node. + * Stream extraction overload: reads one line and appends the characters read + * to the content of the node without adding a delimiter. */ inline std::istream &operator>>(std::istream &is, XmlTextContent &node) { std::string temp; if (std::getline(is, temp)) { node.append(temp); - node.append("\n"); } return is; } From cbb64051fc85be9143fcaac7c500d75b0bd16b11 Mon Sep 17 00:00:00 2001 From: Xeno Snow Fox Date: Sat, 29 Aug 2026 00:39:32 +1000 Subject: [PATCH 13/13] Escape XML control chars in attributes This change updates XML attribute escaping to encode tab, newline, and carriage return characters as XML numeric references (` `, ` `, ` `). This ensures attribute values remain valid and safe when serialized into XML. --- include/xtrpg/xml/node/IAttributes.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/xtrpg/xml/node/IAttributes.hpp b/include/xtrpg/xml/node/IAttributes.hpp index facec8e..907e1e9 100644 --- a/include/xtrpg/xml/node/IAttributes.hpp +++ b/include/xtrpg/xml/node/IAttributes.hpp @@ -83,6 +83,12 @@ class IAttributes { os << "&"; else if (c == '<') os << "<"; + else if (c == '\t') + os << " "; + else if (c == '\n') + os << " "; + else if (c == '\r') + os << " "; else os << c; }