diff --git a/include/myxml/cdata.hpp b/include/myxml/cdata.hpp new file mode 100644 index 0000000..a8368f3 --- /dev/null +++ b/include/myxml/cdata.hpp @@ -0,0 +1,19 @@ +#pragma once +#include "myxml/node.hpp" + +namespace myxml +{ + class CData : public Node + { + private: + std::string inner; + + public: + explicit CData(std::string); + + virtual ~CData() = default; + virtual std::string ExportRaw() const override; + virtual std::string ExportFormatted(int indentLevel = 0, int indentSize = 4) const override; + virtual void SetEntityEncoding(bool) override; + }; +} \ No newline at end of file diff --git a/include/myxml/element.hpp b/include/myxml/element.hpp index 71aa127..b97641c 100644 --- a/include/myxml/element.hpp +++ b/include/myxml/element.hpp @@ -19,17 +19,16 @@ namespace myxml }; private: - // std::shared_ptr firstChild; - // std::shared_ptr lastChild; std::string name; std::map> attributes; - // std::map, std::less<>> nameToElemBuffer; - /* Set nitializer as private to avoid using Element without share_ptr*/ - Element(std::string_view name); + /* Set initializer as private to avoid using Element without share_ptr*/ + explicit Element(std::string_view name); Element() = default; public: + virtual ~Element() = default; + /* Builder */ // Wraps creating shared_ptr static std::shared_ptr New(std::string_view name); @@ -45,12 +44,6 @@ namespace myxml void SetAttribute(std::string key, std::string value); void ExtendAttributes(std::map); - /* Implement Node */ - virtual NodeType Type() override; - virtual bool IsType(NodeType) override; - virtual std::optional> AsElement() override; - virtual std::optional> AsText() override; - /* Implement Exportable */ virtual std::string ExportRaw() const override; virtual std::string ExportFormatted(int indentLevel = 0, int indentSize = 4) const override; diff --git a/include/myxml/node.hpp b/include/myxml/node.hpp index af03a28..2d6a0f7 100644 --- a/include/myxml/node.hpp +++ b/include/myxml/node.hpp @@ -14,15 +14,8 @@ namespace myxml // defined below class CompositeNode; - enum class NodeType - { - Text, - Element, - Declaration, - }; - // Element, Text are Node. - class Node : public Exportable + class Node : public std::enable_shared_from_this, public Exportable { public: virtual ~Node() = default; @@ -31,14 +24,14 @@ namespace myxml std::shared_ptr prev; std::shared_ptr next; - virtual NodeType Type() = 0; - virtual bool IsType(NodeType) = 0; - virtual std::optional> AsElement() = 0; - virtual std::optional> AsText() = 0; + template + std::enable_if_t, + std::optional>> + As(); }; // Element are Composite Node. - class CompositeNode : public Node, public std::enable_shared_from_this + class CompositeNode : public Node { private: std::shared_ptr firstChild; @@ -60,4 +53,13 @@ namespace myxml std::shared_ptr InsertAtEnd(const std::shared_ptr &); void Unlink(const std::shared_ptr &); }; + + template + std::enable_if_t, + std::optional>> + Node::As() + { + auto derivedPtr = std::dynamic_pointer_cast(this->shared_from_this()); + return derivedPtr ? std::optional(derivedPtr) : std::nullopt; + } } diff --git a/include/myxml/parser.hpp b/include/myxml/parser.hpp index ff015c4..a0a8ee4 100644 --- a/include/myxml/parser.hpp +++ b/include/myxml/parser.hpp @@ -1,14 +1,10 @@ #pragma once #include "myxml/element.hpp" #include "myxml/document.hpp" +#include "myxml/cdata.hpp" namespace myxml { - // No effect currently. Just use it to mark what is a tag. - struct Tag - { - }; - struct ElementTag { enum class ClosingType @@ -59,7 +55,14 @@ namespace myxml * @throws `SyntaxError` if the following chars do not confront to `key="value"` format */ std::optional> parseAttribute(); + /** + * @throws `SyntaxError` if faild to find `<` + */ std::shared_ptr parseText(); + /** + * @returns `std::nullopt` if not start with `> parseCData(); /** * @throws `UnexpectedEndOfInput` * @throws `SyntaxError` diff --git a/include/myxml/text.hpp b/include/myxml/text.hpp index 8effa4e..190b3eb 100644 --- a/include/myxml/text.hpp +++ b/include/myxml/text.hpp @@ -4,7 +4,7 @@ namespace myxml { - class Text : public Node, public std::enable_shared_from_this + class Text : public Node { private: std::string inner; @@ -13,18 +13,14 @@ namespace myxml public: explicit Text(std::string_view str); + virtual ~Text() = default; + // may used in Export bool IsAllSpace() const; - /* implement Node */ - virtual NodeType Type() override; - virtual bool IsType(NodeType) override; - virtual std::optional> AsElement() override; - virtual std::optional> AsText() override; - /* Implment Exportable*/ virtual std::string ExportRaw() const override; virtual std::string ExportFormatted(int indentLevel = 0, int indentSize = 4) const override; virtual void SetEntityEncoding(bool) override; }; -} +} \ No newline at end of file diff --git a/src/cdata.cpp b/src/cdata.cpp new file mode 100644 index 0000000..c3f390f --- /dev/null +++ b/src/cdata.cpp @@ -0,0 +1,24 @@ +#include +#include "myxml/cdata.hpp" + +namespace myxml +{ + CData::CData(std::string str) + : inner(str) + { + } + + std::string CData::ExportRaw() const + { + return fmt::format("\n", this->inner); + } + + std::string CData::ExportFormatted(int indentLevel, int indentSize) const + { + return std::string(indentLevel * indentSize, ' ') + this->ExportRaw(); + } + + void CData::SetEntityEncoding(bool) + { // do nothing + } +} diff --git a/src/element.cpp b/src/element.cpp index e515d10..61a82f9 100644 --- a/src/element.cpp +++ b/src/element.cpp @@ -53,26 +53,6 @@ namespace myxml this->attributes.insert(attris.begin(), attris.end()); } - NodeType Element::Type() - { - return NodeType::Element; - } - - bool Element::IsType(NodeType type) - { - return type == NodeType::Element; - } - - std::optional> Element::AsElement() - { - return std::dynamic_pointer_cast(this->shared_from_this()); - } - - std::optional> Element::AsText() - { - return std::nullopt; - } - std::string Element::ExportRaw() const { diff --git a/src/node.cpp b/src/node.cpp index b756839..c0c6ae5 100644 --- a/src/node.cpp +++ b/src/node.cpp @@ -1,9 +1,11 @@ +#include #include "myxml/node.hpp" #include "myxml/element.hpp" namespace myxml { + std::shared_ptr CompositeNode::LastChild() { return this->lastChild; @@ -40,7 +42,7 @@ namespace myxml } for (auto child = this->firstChild; child != nullptr; child = child->next) { - if (auto elem = child->AsElement(); elem && (*elem)->GetName() == name) + if (auto elem = child->As(); elem && (*elem)->GetName() == name) { this->nameToElemBuffer.emplace(name, *elem); return *elem; @@ -55,7 +57,7 @@ namespace myxml { elem->parent->Unlink(elem); } - elem->parent = this->shared_from_this(); + elem->parent = *this->shared_from_this()->As(); if (this->firstChild == nullptr) { this->firstChild = elem; @@ -76,7 +78,7 @@ namespace myxml { elem->parent->Unlink(elem); } - elem->parent = this->shared_from_this(); + elem->parent = *this->shared_from_this()->As(); if (this->firstChild == nullptr) { this->firstChild = elem; diff --git a/src/parser.cpp b/src/parser.cpp index 4620695..bb88cea 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -139,7 +139,7 @@ namespace myxml { len++; } - if (this->buffer[begin + len] != '<') + if (begin + len >= this->buffer.length()) { // if jump out of while loop due to length limit throw SyntaxError(fmt::format("expected '<' after text")); } @@ -147,6 +147,28 @@ namespace myxml return std::shared_ptr(new Text(this->buffer.substr(begin, len))); } + std::optional> Parser::parseCData() + { + if (this->peekNextNChars(9) != "nextNChars(9); + std::size_t begin = this->offset; + std::size_t len = 0; + while (begin + len + 2 < this->buffer.length() && + std::string_view(this->buffer.data() + begin + len, 3) != "]]>") + { + len++; + } + if (begin + len + 2 >= this->buffer.length()) + { + throw SyntaxError(fmt::format("expected \"]]>\" after CDATA")); + } + this->offset += len + 2; + return std::shared_ptr(new CData(this->buffer.substr(begin, len))); + } + std::optional Parser::ParseTag() { if (this->nextChar() != '<') @@ -194,6 +216,11 @@ namespace myxml { case '<': { + if (auto cdata = this->parseCData(); cdata) + { + elem->InsertAtEnd(*cdata); + continue; + } auto tag = this->ParseTag(); // impossible to be std::nullopt assert(tag); switch (tag->type) @@ -218,7 +245,7 @@ namespace myxml case ElementTag::ClosingType::Closing: if (tag->name != elem->GetName()) { - throw SyntaxError(fmt::format("")); + throw SyntaxError(fmt::format("elem name in closing tag is mismatched with the header")); } if (!header.attris.empty()) { diff --git a/src/text.cpp b/src/text.cpp index 610ecfe..4b29145 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -4,26 +4,6 @@ namespace myxml { - NodeType Text::Type() - { - return NodeType::Text; - } - - bool Text::IsType(NodeType type) - { - return type == NodeType::Text; - } - - std::optional> Text::AsElement() - { - return std::nullopt; - } - - std::optional> Text::AsText() - { - Text *test = dynamic_cast(this); - return std::dynamic_pointer_cast(this->shared_from_this()); - } Text::Text(std::string_view input) : encodeOnExport(true) diff --git a/tests/document_test.cpp b/tests/document_test.cpp index 561a5f3..82cdc07 100644 --- a/tests/document_test.cpp +++ b/tests/document_test.cpp @@ -11,7 +11,7 @@ TEST_CASE("Simple document", "[document]") auto doc = myxml::Document::Parse(input); REQUIRE(doc->GetRoot()->GetName() == "root"); REQUIRE(doc->GetRoot()->Elem("child")->GetName() == "child"); - REQUIRE(doc->GetRoot()->Elem("child")->FirstChild()->AsText().value()->ExportRaw() == "Value"); + REQUIRE(doc->GetRoot()->Elem("child")->FirstChild()->As().value()->ExportRaw() == "Value"); } SECTION("With decl") diff --git a/tests/element_test.cpp b/tests/element_test.cpp index d1c4a75..2d1f2a0 100644 --- a/tests/element_test.cpp +++ b/tests/element_test.cpp @@ -15,8 +15,8 @@ TEST_CASE("Element Functionality", "[element]") SECTION("Basic Insertion") { root->InsertAtFront(child); - REQUIRE(root->FirstChild()->AsElement().value()->GetName() == "child"); - REQUIRE(root->LastChild()->AsElement().value()->GetName() == "child"); + REQUIRE(root->FirstChild()->As().value()->GetName() == "child"); + REQUIRE(root->LastChild()->As().value()->GetName() == "child"); } SECTION("Get child by name after insert it") @@ -33,7 +33,7 @@ TEST_CASE("Element Functionality", "[element]") root->InsertAtEnd(child); root->InsertAtEnd(sibiling); REQUIRE(root->Elem("child")->GetName() == "child"); - REQUIRE(root->Elem("child")->next->AsElement().value()->GetName() == "sibiling"); - REQUIRE(root->Elem("sibiling")->prev->AsElement().value()->GetName() == "child"); + REQUIRE(root->Elem("child")->next->As().value()->GetName() == "sibiling"); + REQUIRE(root->Elem("sibiling")->prev->As().value()->GetName() == "child"); } } \ No newline at end of file diff --git a/tests/parser_test.cpp b/tests/parser_test.cpp index a73eef8..9d287be 100644 --- a/tests/parser_test.cpp +++ b/tests/parser_test.cpp @@ -37,11 +37,11 @@ TEST_CASE("Parsing simple xml elements", "[parser]") )"; auto elem = myxml::Element::Parse(withText); REQUIRE(elem->GetName() == "root"); - REQUIRE(elem->FirstChild()->AsText().value()->ExportRaw() == "\n "); - auto child = elem->FirstChild()->next->AsElement().value(); + REQUIRE(elem->FirstChild()->As().value()->ExportRaw() == "\n "); + auto child = elem->FirstChild()->next->As().value(); REQUIRE(child->GetName() == "child"); - REQUIRE(child->FirstChild()->AsText().value()->ExportRaw() == "Hello, world!"); - REQUIRE(child->next->AsText().value()->ExportRaw() == "\n"); + REQUIRE(child->FirstChild()->As().value()->ExportRaw() == "Hello, world!"); + REQUIRE(child->next->As().value()->ExportRaw() == "\n"); } SECTION("Nested") @@ -53,18 +53,18 @@ TEST_CASE("Parsing simple xml elements", "[parser]") )"; auto elem = myxml::Element::Parse(nested); REQUIRE(elem->GetName() == "root"); - REQUIRE(elem->FirstChild()->AsText().value()->ExportRaw() == "\n "); + REQUIRE(elem->FirstChild()->As().value()->ExportRaw() == "\n "); - auto parent = elem->FirstChild()->next->AsElement().value(); + auto parent = elem->FirstChild()->next->As().value(); REQUIRE(parent->GetName() == "parent"); - REQUIRE(parent->FirstChild()->AsText().value()->ExportRaw() == "\n "); + REQUIRE(parent->FirstChild()->As().value()->ExportRaw() == "\n "); - auto child = parent->FirstChild()->next->AsElement().value(); + auto child = parent->FirstChild()->next->As().value(); REQUIRE(child->GetName() == "child"); REQUIRE(child->FirstChild() == nullptr); // 因为 是空的,没有文本子节点 - REQUIRE(parent->FirstChild()->next->next->AsText().value()->ExportRaw() == "\n "); - REQUIRE(elem->FirstChild()->next->next->AsText().value()->ExportRaw() == "\n"); + REQUIRE(parent->FirstChild()->next->next->As().value()->ExportRaw() == "\n "); + REQUIRE(elem->FirstChild()->next->next->As().value()->ExportRaw() == "\n"); } SECTION("Mutli-Level") @@ -76,25 +76,25 @@ TEST_CASE("Parsing simple xml elements", "[parser]") )"; auto elem = myxml::Element::Parse(multiLevel); REQUIRE(elem->GetName() == "root"); - REQUIRE(elem->FirstChild()->AsText().value()->ExportRaw() == "\n "); + REQUIRE(elem->FirstChild()->As().value()->ExportRaw() == "\n "); // 验证第一个 节点 - auto item1 = elem->FirstChild()->next->AsElement().value(); + auto item1 = elem->FirstChild()->next->As().value(); REQUIRE(item1->GetName() == "item"); - REQUIRE(item1->FirstChild()->AsText().value()->ExportRaw() == "First"); + REQUIRE(item1->FirstChild()->As().value()->ExportRaw() == "First"); // 验证第二个 节点 - auto item2 = item1->next->next->AsElement().value(); + auto item2 = item1->next->next->As().value(); REQUIRE(item2->GetName() == "item"); - REQUIRE(item2->FirstChild()->AsText().value()->ExportRaw() == "Second"); + REQUIRE(item2->FirstChild()->As().value()->ExportRaw() == "Second"); // 验证第三个 节点 - auto item3 = item2->next->next->AsElement().value(); + auto item3 = item2->next->next->As().value(); REQUIRE(item3->GetName() == "item"); - REQUIRE(item3->FirstChild()->AsText().value()->ExportRaw() == "Third"); + REQUIRE(item3->FirstChild()->As().value()->ExportRaw() == "Third"); // 验证 root 节点的最后文本 - REQUIRE(item3->next->AsText().value()->ExportRaw() == "\n"); + REQUIRE(item3->next->As().value()->ExportRaw() == "\n"); } SECTION("Closed Element") @@ -104,15 +104,15 @@ TEST_CASE("Parsing simple xml elements", "[parser]") )"; auto elem = myxml::Element::Parse(closed); REQUIRE(elem->GetName() == "root"); - REQUIRE(elem->FirstChild()->AsText().value()->ExportRaw() == "\n "); + REQUIRE(elem->FirstChild()->As().value()->ExportRaw() == "\n "); // 验证 节点 - auto emptyElement = elem->FirstChild()->next->AsElement().value(); + auto emptyElement = elem->FirstChild()->next->As().value(); REQUIRE(emptyElement->GetName() == "empty"); REQUIRE(emptyElement->FirstChild() == nullptr); // 自闭合标签没有子节点 // 验证 root 节点的最后文本 - REQUIRE(emptyElement->next->AsText().value()->ExportRaw() == "\n"); + REQUIRE(emptyElement->next->As().value()->ExportRaw() == "\n"); } SECTION("Mixed") @@ -123,15 +123,15 @@ TEST_CASE("Parsing simple xml elements", "[parser]") )"; auto elem = myxml::Element::Parse(mixed); REQUIRE(elem->GetName() == "root"); - REQUIRE(elem->FirstChild()->AsText().value()->ExportRaw() == "\n hello\n "); + REQUIRE(elem->FirstChild()->As().value()->ExportRaw() == "\n hello\n "); // 验证 节点 - auto child = elem->FirstChild()->next->AsElement().value(); + auto child = elem->FirstChild()->next->As().value(); REQUIRE(child->GetName() == "child"); REQUIRE(child->FirstChild() == nullptr); // 是空的,没有文本子节点 // 验证 root 节点的最后文本 - REQUIRE(child->next->AsText().value()->ExportRaw() == "\n"); + REQUIRE(child->next->As().value()->ExportRaw() == "\n"); } SECTION("With attributes") @@ -179,11 +179,11 @@ TEST_CASE("Parsing simple xml elements", "[parser]") REQUIRE(elem->GetAttribute("attr") == "value"); // 检查根元素的第一个子元素 - auto child = elem->FirstChild()->AsElement().value(); + auto child = elem->FirstChild()->As().value(); REQUIRE(child->GetName() == "child"); // 检查子元素的文本内容 - REQUIRE(child->FirstChild()->AsText().value()->ExportRaw() == "Text"); + REQUIRE(child->FirstChild()->As().value()->ExportRaw() == "Text"); } SECTION("Decoding entity") @@ -195,9 +195,17 @@ TEST_CASE("Parsing simple xml elements", "[parser]") elem->SetEntityEncoding(false); REQUIRE(elem->GetName() == "root"); - REQUIRE(elem->FirstChild()->AsText().value()->ExportRaw() == "\n <>\n"); + REQUIRE(elem->FirstChild()->As().value()->ExportRaw() == "\n <>\n"); elem->SetEntityEncoding(true); - REQUIRE(elem->FirstChild()->AsText().value()->ExportRaw() == "\n <>\n"); + REQUIRE(elem->FirstChild()->As().value()->ExportRaw() == "\n <>\n"); + } + + SECTION("CDATA") + { + std::string cdata = ""; + auto elem = myxml::Element::Parse(cdata); + + REQUIRE(elem->FirstChild()->As().value()->ExportRaw() == "\n"); } } \ No newline at end of file