diff --git a/include/xtrpg/xml/node/XmlDeclarationTag.hpp b/include/xtrpg/xml/node/DeclarationNode.hpp similarity index 53% rename from include/xtrpg/xml/node/XmlDeclarationTag.hpp rename to include/xtrpg/xml/node/DeclarationNode.hpp index ca8f4d0..23038d9 100644 --- a/include/xtrpg/xml/node/XmlDeclarationTag.hpp +++ b/include/xtrpg/xml/node/DeclarationNode.hpp @@ -13,12 +13,12 @@ namespace xtrpg::xml::node { /** * Represents an XML declaration node with a tag name and attributes. */ -class XmlDeclarationTag : public INode, public ITagname, public IAttributes { +class DeclarationNode : public INode, public ITagname, public IAttributes { public: /** * Constructs an XML declaration. */ - explicit XmlDeclarationTag(std::string tagname) + explicit DeclarationNode(std::string tagname) : INode(NodeType::DECLARATION), ITagname(std::move(tagname)), IAttributes() { if (this->getTagname() != "xml") { @@ -27,35 +27,69 @@ class XmlDeclarationTag : public INode, public ITagname, public IAttributes { } /** - * Serializes the node into an XML formatted string. + * Explicitly defaulted copy constructor. */ - void serialize(std::ostream &os) const override { - 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"); - } + DeclarationNode(const DeclarationNode &) = default; - 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"); - } + /** + * Explicitly defaulted move constructor. + */ + DeclarationNode(DeclarationNode &&) = default; - if (const auto encoding = this->getAttribute("encoding")) { - if (!isValidEncodingName(*encoding)) { + /** + * Explicitly defaulted copy assignment operator. + */ + DeclarationNode &operator=(const DeclarationNode &) = default; + + /** + * Explicitly defaulted move assignment operator. + */ + DeclarationNode &operator=(DeclarationNode &&) = default; + + /** + * Sets an attribute key/value pair with XML declaration-specific validation. + * Only allows 'version', 'encoding', and 'standalone' attributes. + * Version must be '1.0' or '1.1', encoding must be a valid encoding name, + * and standalone must be 'yes' or 'no'. + * + * @throws std::invalid_argument if the attribute is invalid for XML + * declarations + */ + void setAttribute(std::string_view key, std::string_view value) override { + // Validate XML declaration-specific attributes + if (key == "version") { + if (value != "1.0" && value != "1.1") { + throw std::invalid_argument( + "XML declaration version must be '1.0' or '1.1'"); + } + } else if (key == "encoding") { + if (!isValidEncodingName(value)) { throw std::invalid_argument("Invalid XML declaration encoding"); } - } - if (const auto standalone = this->getAttribute("standalone")) { - if (*standalone != "yes" && *standalone != "no") { + } else if (key == "standalone") { + if (value != "yes" && value != "no") { throw std::invalid_argument( "XML declaration standalone must be 'yes' or 'no'"); } + } else { + throw std::invalid_argument( + "XML declaration does not support attribute: " + std::string(key)); + } + + // Call parent implementation + IAttributes::setAttribute(key, value); + } + + /** + * Serializes the node into an XML formatted string. + * Note: All attribute validation occurs in setAttribute(), so this method + * assumes a valid state. + */ + void serialize(std::ostream &os) const override { + const auto version = this->getAttribute("version"); + if (!version) { + throw std::invalid_argument( + "XML declaration requires a 'version' attribute"); } os << " void forEachAttribute(Func &&callback) const { for (const auto &[key, value] : this->_attributes) { - callback(std::string_view{key}, std::string_view{value}); + if (!callback(std::string_view{key}, std::string_view{value})) { + break; + } } } diff --git a/include/xtrpg/xml/node/INode.hpp b/include/xtrpg/xml/node/INode.hpp index 6016f69..4166d98 100644 --- a/include/xtrpg/xml/node/INode.hpp +++ b/include/xtrpg/xml/node/INode.hpp @@ -5,10 +5,15 @@ namespace xtrpg::xml::node { +// Forward declaration +class NodeContainer; + /** * Defines the common interface and node type metadata for XML nodes. */ class INode { + friend class NodeContainer; + public: /** * Constructor that takes in a node type. @@ -30,10 +35,27 @@ class INode { */ NodeType getNodeType() const { return this->_type; }; + /** + * Checks if this node is of the specified type. + */ bool isType(NodeType type) const { return this->_type == type; } + /** + * Returns the parent NodeContainer of this node, or nullptr if this is a + * root node. + */ + class NodeContainer *getParent() const { return this->_parent; } + +protected: + /** + * Sets the parent NodeContainer for this node. + * Called by NodeContainer when appending children. + */ + void setParent(class NodeContainer *parent) { this->_parent = parent; } + private: NodeType _type; + class NodeContainer *_parent = nullptr; }; /** @@ -44,4 +66,4 @@ inline std::ostream &operator<<(std::ostream &os, const INode &node) { return os; } -} // 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/NodeContainer.hpp b/include/xtrpg/xml/node/NodeContainer.hpp new file mode 100644 index 0000000..599e276 --- /dev/null +++ b/include/xtrpg/xml/node/NodeContainer.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include +#include + +#include "xtrpg/xml/node/INode.hpp" +#include "xtrpg/xml/node/NodeType.hpp" +#include "xtrpg/xml/node/TextNode.hpp" + +namespace xtrpg::xml::node { + +/** + * A node design to be a container of other nodes. + */ +class NodeContainer : public INode { +public: + NodeContainer() : INode(NodeType::CONTAINER) {} + + explicit NodeContainer(NodeType type) : INode(type) {} + + ~NodeContainer() = default; + + /** + * Appends a given child node. + * + * @throws std::invalid_argument if appending would create a cycle. + */ + void append(std::shared_ptr child) { + if (child) { + // Check if this node is already an ancestor of child by walking up + // the parent chain. This is O(depth) instead of O(n). + for (NodeContainer *ancestor = child->getParent(); ancestor != nullptr; + ancestor = ancestor->getParent()) { + if (ancestor == this) { + throw std::invalid_argument("Cannot create a cycle in XML nodes"); + } + } + // Set this node as the child's parent and append + child->setParent(this); + this->_children.push_back(std::move(child)); + } + } + + /** + * Returns a vector of child nodes. + */ + const std::vector> &children() const { + return this->_children; + } + + /** + * Checks whether this node has any child nodes. + * + * @return true if this node contains at least one child, false otherwise. + */ + [[nodiscard]] constexpr bool hasChildren() const noexcept { + return !this->_children.empty(); + } + + /** + * Serializes all child nodes into the provided output stream. + */ + void serialize(std::ostream &os) const override { + for (const auto &child : this->_children) { + child->serialize(os); + } + } + +private: + std::vector> _children; +}; + +/** + * Stream operator overload for easy serialization + */ +inline NodeContainer &operator<<(NodeContainer &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 NodeContainer &operator<<(NodeContainer &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/NodeType.hpp b/include/xtrpg/xml/node/NodeType.hpp index 00755c7..8c139ca 100644 --- a/include/xtrpg/xml/node/NodeType.hpp +++ b/include/xtrpg/xml/node/NodeType.hpp @@ -5,6 +5,6 @@ namespace xtrpg::xml::node { /** * Identifies the supported XML node categories. */ -enum class NodeType { TAG, DECLARATION, TEXT_CONTENT }; +enum class NodeType { TAG, DECLARATION, TEXT_CONTENT, CONTAINER }; } // namespace xtrpg::xml::node \ No newline at end of file diff --git a/include/xtrpg/xml/node/TagNode.hpp b/include/xtrpg/xml/node/TagNode.hpp new file mode 100644 index 0000000..01a9bcf --- /dev/null +++ b/include/xtrpg/xml/node/TagNode.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include +#include +#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/NodeContainer.hpp" +#include "xtrpg/xml/node/NodeType.hpp" +#include "xtrpg/xml/node/TextNode.hpp" + +namespace xtrpg::xml::node { + +/** + * Represents an XML element with a tag name, attributes, and child nodes. + */ +class TagNode : public INode, + public ITagname, + public IAttributes, + public NodeContainer { +public: + /** + * Inline constructor that accepts a tag name. + */ + explicit TagNode(std::string name) + : INode(NodeType::TAG), ITagname(name), IAttributes(), NodeContainer() {} + + /** + * Explicitly defaulted copy constructor. + */ + TagNode(const TagNode &) = default; + + /** + * Explicitly defaulted move constructor. + */ + TagNode(TagNode &&) = default; + + /** + * Explicitly defaulted copy assignment operator. + */ + TagNode &operator=(const TagNode &) = default; + + /** + * Explicitly defaulted move assignment operator. + */ + TagNode &operator=(TagNode &&) = default; + + /** + * Returns a reference to the name of the tag. + */ + const std::string_view name() const { return this->getTagname(); } + + /** + * Serializes the node into an XML formatted string. + */ + void serialize(std::ostream &os) const override { + os << "<" << this->getTagname(); + + IAttributes::serialize(os); + + if (!this->hasChildren()) { + os << "/>"; + return; + } + + os << ">"; + NodeContainer::serialize(os); + os << "getTagname() << ">"; + } + +private: +}; +/** + * Stream operator overload for easy serialization + */ +inline TagNode &operator<<(TagNode &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 TagNode &operator<<(TagNode &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/TextNode.hpp similarity index 76% rename from include/xtrpg/xml/node/XmlTextContent.hpp rename to include/xtrpg/xml/node/TextNode.hpp index d5dc6dc..413fe9f 100644 --- a/include/xtrpg/xml/node/XmlTextContent.hpp +++ b/include/xtrpg/xml/node/TextNode.hpp @@ -13,24 +13,44 @@ namespace xtrpg::xml::node { /** * Represents text content within an XML document and escapes it on output. */ -class XmlTextContent : public INode { +class TextNode : public INode { public: /** * Default constructor that generate a blank text node. */ - XmlTextContent() : INode(NodeType::TEXT_CONTENT) {} + TextNode() : INode(NodeType::TEXT_CONTENT) {} /** * Constructor that takes in a copy of the text content to store within this * node. */ - explicit XmlTextContent(std::string content) + explicit TextNode(std::string content) : INode(NodeType::TEXT_CONTENT), _content(std::move(content)) { if (!isValidXmlCharacterData(this->_content)) { throw std::invalid_argument("Invalid character in XML text content"); } } + /** + * Explicitly defaulted copy constructor. + */ + TextNode(const TextNode &) = default; + + /** + * Explicitly defaulted move constructor. + */ + TextNode(TextNode &&) = default; + + /** + * Explicitly defaulted copy assignment operator. + */ + TextNode &operator=(const TextNode &) = default; + + /** + * Explicitly defaulted move assignment operator. + */ + TextNode &operator=(TextNode &&) = default; + /** * Returns a reference to the text content stored on this node. */ @@ -39,7 +59,7 @@ class XmlTextContent : public INode { /** * Sets the text content stored on this node, overriding any previous content. */ - void content(std::string withContent) { + void setContent(std::string withContent) { if (!isValidXmlCharacterData(withContent)) { throw std::invalid_argument("Invalid character in XML text content"); } @@ -86,8 +106,7 @@ class XmlTextContent : public INode { /** * Stream insertion overload. (node << text). */ -inline XmlTextContent &operator<<(XmlTextContent &node, - const std::string &withText) { +inline TextNode &operator<<(TextNode &node, const std::string &withText) { node.append(withText); return node; } @@ -96,7 +115,7 @@ inline XmlTextContent &operator<<(XmlTextContent &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) { +inline std::istream &operator>>(std::istream &is, TextNode &node) { std::string temp; if (std::getline(is, temp)) { node.append(temp); diff --git a/include/xtrpg/xml/node/XmlTagNode.hpp b/include/xtrpg/xml/node/XmlTagNode.hpp deleted file mode 100644 index 6d50a96..0000000 --- a/include/xtrpg/xml/node/XmlTagNode.hpp +++ /dev/null @@ -1,130 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#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. - * - * @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)); - } - } - - /** - * 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: - /** - * 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; -}; - -/** - * 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