Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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 << "<?xml version=\"" << *version << "\"";
Expand Down
16 changes: 13 additions & 3 deletions include/xtrpg/xml/node/IAttributes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ namespace xtrpg::xml::node {
*/
class IAttributes {
public:
/**
* Default constructor.
*/
IAttributes() {}

/**
* Virtual destructor.
*/
Expand All @@ -27,7 +32,7 @@ class IAttributes {
/**
* Sets an attribute key/value pair.
*/
void setAttribute(std::string_view key, std::string_view value) {
virtual void setAttribute(std::string_view key, std::string_view value) {
if (!isValidXmlName(key)) {
throw std::invalid_argument("Invalid XML attribute name");
}
Expand Down Expand Up @@ -62,12 +67,17 @@ class IAttributes {

/**
* Executes a callback function for each key/value attribute pair.
* The callback can control iteration by returning a boolean value:
* - Return true to continue iterating to the next attribute
* - Return false to break early and stop iteration
*
* Callback signature: void(std::string_view key, std::string_view value)
* Callback signature: bool(std::string_view key, std::string_view value)
*/
template <typename Func> 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;
}
}
}

Expand Down
24 changes: 23 additions & 1 deletion include/xtrpg/xml/node/INode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
};

/**
Expand All @@ -44,4 +66,4 @@ inline std::ostream &operator<<(std::ostream &os, const INode &node) {
return os;
}

} // namespace xtrpg::xml
} // namespace xtrpg::xml::node
92 changes: 92 additions & 0 deletions include/xtrpg/xml/node/NodeContainer.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#pragma once

#include <memory>
#include <vector>

#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<INode> 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<std::shared_ptr<INode>> &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<std::shared_ptr<INode>> _children;
};

/**
* Stream operator overload for easy serialization
*/
inline NodeContainer &operator<<(NodeContainer &node,
std::shared_ptr<INode> 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<TextNode>(withText);
node.append(textNode);
return node;
}
} // namespace xtrpg::xml::node
2 changes: 1 addition & 1 deletion include/xtrpg/xml/node/NodeType.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading