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
103 changes: 103 additions & 0 deletions include/xtrpg/xml/node/IAttributes.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#pragma once

#include <iostream>
#include <map>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>

#include "xtrpg/xml/node/XmlCharacter.hpp"
#include "xtrpg/xml/node/XmlName.hpp"

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) {
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);
}

/**
* Returns a copy of the value associated with the given attribute key.
*/
std::optional<std::string> 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<std::string> 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 <typename Func> void forEachAttribute(Func &&callback) const {
for (const auto &[key, value] : this->_attributes) {
callback(std::string_view{key}, std::string_view{value});
}
}

/** Serializes attributes into an XML formatted string in key order. */
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 << "&quot;";
else if (c == '&')
os << "&amp;";
else if (c == '<')
os << "&lt;";
else if (c == '\t')
os << "&#x9;";
else if (c == '\n')
os << "&#xA;";
else if (c == '\r')
os << "&#xD;";
else
os << c;
}
os << "\"";
}
}

private:
std::map<std::string, std::string> _attributes;
};

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

#include "xtrpg/xml/node/NodeType.hpp"
#include <iostream>

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
37 changes: 37 additions & 0 deletions include/xtrpg/xml/node/ITagname.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#pragma once

#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>

#include "xtrpg/xml/node/XmlName.hpp"

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)) {
if (!isValidXmlName(this->_tagname)) {
throw std::invalid_argument("Invalid XML tag name");
}
}

/**
* Virtual destructor.
*/
virtual ~ITagname() = default;

std::string_view getTagname() const { return this->_tagname; }

private:
std::string _tagname;
};

} // namespace xtrpg::xml
10 changes: 10 additions & 0 deletions include/xtrpg/xml/node/NodeType.hpp
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions include/xtrpg/xml/node/XmlCharacter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <string_view>

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
91 changes: 91 additions & 0 deletions include/xtrpg/xml/node/XmlDeclarationTag.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#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"
#include <stdexcept>
#include <string_view>
#include <utility>

namespace xtrpg::xml::node {

/**
* Represents an XML declaration node with a tag name and attributes.
*/
class XmlDeclarationTag : public INode, public ITagname, public IAttributes {
public:
/**
* Constructs an XML declaration.
*/
explicit XmlDeclarationTag(std::string tagname)
: 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 {
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 << "<?xml version=\"" << *version << "\"";
if (const auto encoding = this->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<unsigned char>(encoding.front());
return (first >= 'A' && first <= 'Z') || (first >= 'a' && first <= 'z');
}
};

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

#include <string_view>

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<unsigned char>(name.front()))) {
return false;
}

for (const unsigned char character : name.substr(1)) {
if (!isNameCharacter(character)) {
return false;
}
}
return true;
}

} // namespace xtrpg::xml::node
Loading
Loading