From 58fabf3a8fda822d4b01eefd7bc9fe113ff6c3a9 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 16 Jun 2025 14:46:19 +0100 Subject: [PATCH] XmlElement: Add new API to allow iterating over attributes --- .../containers/juce_NamedValueSet.cpp | 10 +- modules/juce_core/xml/juce_XmlDocument.cpp | 2 +- modules/juce_core/xml/juce_XmlElement.cpp | 50 +++--- modules/juce_core/xml/juce_XmlElement.h | 167 +++++++++++++++--- 4 files changed, 171 insertions(+), 58 deletions(-) diff --git a/modules/juce_core/containers/juce_NamedValueSet.cpp b/modules/juce_core/containers/juce_NamedValueSet.cpp index 83ea755271..2bd9d0bb06 100644 --- a/modules/juce_core/containers/juce_NamedValueSet.cpp +++ b/modules/juce_core/containers/juce_NamedValueSet.cpp @@ -277,20 +277,20 @@ void NamedValueSet::setFromXmlAttributes (const XmlElement& xml) { values.clearQuick(); - for (auto* att = xml.attributes.get(); att != nullptr; att = att->nextListItem) + for (const auto& [name, value] : xml.getAttributeIterator()) { - if (att->name.toString().startsWith ("base64:")) + if (name.toString().startsWith ("base64:")) { MemoryBlock mb; - if (mb.fromBase64Encoding (att->value)) + if (mb.fromBase64Encoding (value)) { - values.add ({ att->name.toString().substring (7), var (mb) }); + values.add ({ name.toString().substring (7), var (mb) }); continue; } } - values.add ({ att->name, var (att->value) }); + values.add ({ name, var (value) }); } } diff --git a/modules/juce_core/xml/juce_XmlDocument.cpp b/modules/juce_core/xml/juce_XmlDocument.cpp index 7487cd4658..9f6ce5deb7 100644 --- a/modules/juce_core/xml/juce_XmlDocument.cpp +++ b/modules/juce_core/xml/juce_XmlDocument.cpp @@ -474,7 +474,7 @@ XmlElement* XmlDocument::readNextElement (const bool alsoParseSubElements) if (nextChar == '"' || nextChar == '\'') { auto* newAtt = new XmlElement::XmlAttributeNode (attNameStart, attNameEnd); - readQuotedString (newAtt->value); + readQuotedString (newAtt->attribute.value); attributeAppender.append (newAtt); continue; } diff --git a/modules/juce_core/xml/juce_XmlElement.cpp b/modules/juce_core/xml/juce_XmlElement.cpp index e213e4d479..779b7e3059 100644 --- a/modules/juce_core/xml/juce_XmlElement.cpp +++ b/modules/juce_core/xml/juce_XmlElement.cpp @@ -34,7 +34,6 @@ namespace juce { - static bool isValidXmlNameStartCharacter (juce_wchar character) noexcept { return character == ':' @@ -67,21 +66,19 @@ static bool isValidXmlNameBodyCharacter (juce_wchar character) noexcept } XmlElement::XmlAttributeNode::XmlAttributeNode (const XmlAttributeNode& other) noexcept - : name (other.name), - value (other.value) + : attribute (other.attribute) { } XmlElement::XmlAttributeNode::XmlAttributeNode (const Identifier& n, const String& v) noexcept - : name (n), value (v) + : attribute { n, v } { - jassert (isValidXmlName (name)); + jassert (isValidXmlName (attribute.name)); } XmlElement::XmlAttributeNode::XmlAttributeNode (String::CharPointerType nameStart, String::CharPointerType nameEnd) - : name (nameStart, nameEnd) + : XmlAttributeNode ({ nameStart, nameEnd }, {}) { - jassert (isValidXmlName (name)); } //============================================================================== @@ -281,7 +278,7 @@ void XmlElement::writeElementAsText (OutputStream& outputStream, auto attIndent = (size_t) (indentationLevel + tagName.length() + 1); int lineLen = 0; - for (auto* att = attributes.get(); att != nullptr; att = att->nextListItem) + for (const auto& [name, value] : getAttributeIterator()) { if (lineLen > lineWrapLength && indentationLevel >= 0) { @@ -292,9 +289,9 @@ void XmlElement::writeElementAsText (OutputStream& outputStream, auto startPos = outputStream.getPosition(); outputStream.writeByte (' '); - outputStream << att->name; + outputStream << name; outputStream.write ("=\"", 2); - XmlOutputFunctions::escapeIllegalXmlChars (outputStream, att->value, true); + XmlOutputFunctions::escapeIllegalXmlChars (outputStream, value, true); outputStream.writeByte ('"'); lineLen += (int) (outputStream.getPosition() - startPos); } @@ -536,7 +533,7 @@ static const String& getEmptyStringRef() noexcept const String& XmlElement::getAttributeName (const int index) const noexcept { if (auto* att = attributes[index].get()) - return att->name.toString(); + return att->attribute.name.toString(); return getEmptyStringRef(); } @@ -544,16 +541,16 @@ const String& XmlElement::getAttributeName (const int index) const noexcept const String& XmlElement::getAttributeValue (const int index) const noexcept { if (auto* att = attributes[index].get()) - return att->value; + return att->attribute.value; return getEmptyStringRef(); } -XmlElement::XmlAttributeNode* XmlElement::getAttribute (StringRef attributeName) const noexcept +const XmlAttribute* XmlElement::getAttribute (StringRef attributeName) const noexcept { - for (auto* att = attributes.get(); att != nullptr; att = att->nextListItem) - if (att->name == attributeName) - return att; + for (const auto& att : getAttributeIterator()) + if (att.name == attributeName) + return &att; return nullptr; } @@ -617,12 +614,16 @@ bool XmlElement::compareAttribute (StringRef attributeName, const bool ignoreCase) const noexcept { if (auto* att = getAttribute (attributeName)) - return ignoreCase ? att->value.equalsIgnoreCase (stringToCompareAgainst) - : att->value == stringToCompareAgainst; + return att->equals (attributeName, stringToCompareAgainst, ignoreCase); return false; } +bool XmlElement::compareAttribute (const XmlAttribute& other, const bool ignoreCase) const noexcept +{ + return compareAttribute (other.name, other.value, ignoreCase); +} + //============================================================================== void XmlElement::setAttribute (const Identifier& attributeName, const String& value) { @@ -634,9 +635,9 @@ void XmlElement::setAttribute (const Identifier& attributeName, const String& va { for (auto* att = attributes.get(); ; att = att->nextListItem) { - if (att->name == attributeName) + if (att->attribute.name == attributeName) { - att->value = value; + att->attribute.value = value; break; } @@ -663,7 +664,7 @@ void XmlElement::removeAttribute (const Identifier& attributeName) noexcept { for (auto* att = &attributes; att->get() != nullptr; att = &(att->get()->nextListItem)) { - if (att->get()->name == attributeName) + if (att->get()->attribute.name == attributeName) { delete att->removeNext(); break; @@ -794,7 +795,7 @@ bool XmlElement::isEquivalentTo (const XmlElement* const other, for (auto* att = attributes.get(); att != nullptr; att = att->nextListItem) { - if (! other->compareAttribute (att->name, att->value)) + if (! other->compareAttribute (att->attribute)) return false; ++totalAtts; @@ -818,11 +819,8 @@ bool XmlElement::isEquivalentTo (const XmlElement* const other, return false; } - if (thisAtt->name != otherAtt->name - || thisAtt->value != otherAtt->value) - { + if (thisAtt->attribute != otherAtt->attribute) return false; - } thisAtt = thisAtt->nextListItem; otherAtt = otherAtt->nextListItem; diff --git a/modules/juce_core/xml/juce_XmlElement.h b/modules/juce_core/xml/juce_XmlElement.h index c320756f0e..1805b47cba 100644 --- a/modules/juce_core/xml/juce_XmlElement.h +++ b/modules/juce_core/xml/juce_XmlElement.h @@ -35,6 +35,61 @@ namespace juce { +/** A name-value pair representing an attribute of an XML tag. + + @see XmlElement + + @tags{Core} +*/ +struct XmlAttribute +{ + /** The name of the attribute. */ + Identifier name; + + /** The value of the attribute. */ + String value; + + /** Returns true if the name and value of this attribute compare equal to the passed-in strings. + + The 'ignoreCase' option only affects the value strings. + */ + bool equals (StringRef otherName, StringRef otherValue, bool ignoreCase) const + { + if (name != otherName) + return false; + + return ignoreCase ? value.equalsIgnoreCase (otherValue) + : value == otherValue; + } + + /** Returns true if this attribute compares equal to the passed-in attribute. + + The 'ignoreCase' option only affects the value strings. + */ + bool equals (const XmlAttribute& other, bool ignoreCase) const + { + return equals (other.name, other.value, ignoreCase); + } + + /** Returns true if both attributes are equal. + + This comparison is case-sensitive. + */ + bool operator== (const XmlAttribute& other) const + { + return equals (other, false); + } + + /** Returns true if the attributes have different values. + + This comparison is case-sensitive. + */ + bool operator!= (const XmlAttribute& other) const + { + return ! operator== (other); + } +}; + //============================================================================== /** Used to build a tree of elements representing an XML document. @@ -263,6 +318,15 @@ public: StringRef stringToCompareAgainst, bool ignoreCase = false) const noexcept; + /** Compares the value of a named attribute with a value passed-in. + + @param attribute the name-value pair to search for in the current element + @param ignoreCase whether the value comparison should be case-insensitive + @returns true if the value of the attribute is the same as the string passed-in; + false if it's different (or if no such attribute exists) + */ + bool compareAttribute (const XmlAttribute& attribute, bool ignoreCase = false) const noexcept; + /** Returns the value of a named attribute as an integer. This will try to find the attribute and convert it to an integer (using @@ -657,14 +721,30 @@ private: //============================================================================== struct GetNextElement { - XmlElement* getNext (const XmlElement& e) const { return e.getNextElement(); } + using Value = XmlElement*; + using Element = XmlElement*; + + Element getNext (Element e) const { return e->getNextElement(); } + + static const Value& deref (const Element& e) + { + return e; + } }; struct GetNextElementWithTagName { + using Value = XmlElement*; + using Element = XmlElement*; + GetNextElementWithTagName() = default; explicit GetNextElementWithTagName (String n) : name (std::move (n)) {} - XmlElement* getNext (const XmlElement& e) const { return e.getNextElementWithTagName (name); } + Element getNext (Element e) const { return e->getNextElementWithTagName (name); } + + static const Value& deref (const Element& e) + { + return e; + } String name; }; @@ -675,15 +755,17 @@ private: { public: using difference_type = ptrdiff_t; - using value_type = XmlElement*; + using value_type = typename Traits::Value; using pointer = const value_type*; - using reference = value_type; + using reference = const value_type&; using iterator_category = std::input_iterator_tag; + using Element = typename Traits::Element; + Iterator() = default; template - Iterator (XmlElement* e, Args&&... args) + Iterator (Element e, Args&&... args) : Traits (std::forward (args)...), element (e) {} Iterator begin() const { return *this; } @@ -692,12 +774,12 @@ private: bool operator== (const Iterator& other) const { return element == other.element; } bool operator!= (const Iterator& other) const { return ! operator== (other); } - reference operator*() const { return element; } - pointer operator->() const { return &element; } + reference operator*() const { return Traits::deref (element); } + pointer operator->() const { return std::addressof (Traits::deref (element)); } Iterator& operator++() { - element = Traits::getNext (*element); + element = Traits::getNext (element); return *this; } @@ -709,9 +791,40 @@ private: } private: - value_type element = nullptr; + Element element{}; }; + struct XmlAttributeNode + { + XmlAttributeNode (const XmlAttributeNode&) noexcept; + XmlAttributeNode (const Identifier&, const String&) noexcept; + XmlAttributeNode (String::CharPointerType, String::CharPointerType); + + XmlAttributeNode& operator= (const XmlAttributeNode&) = delete; + XmlAttributeNode& operator= (XmlAttributeNode&&) = delete; + + LinkedListPointer nextListItem; + XmlAttribute attribute; + }; + + struct AttributeIteratorTraits + { + using Value = XmlAttribute; + using Element = const XmlAttributeNode*; + + static Element getNext (Element node) + { + return node->nextListItem.get(); + } + + static const Value& deref (const Element& node) + { + return node->attribute; + } + }; + + using AttributeIterator = Iterator; + public: //============================================================================== /** Allows iterating the children of an XmlElement using range-for syntax. @@ -744,6 +857,23 @@ public: return Iterator { getChildByName (name), name }; } + /** Allows iterating all attributes of an XmlElement using range-for syntax. + + @code + void doSomethingWithXmlAttributes (const XmlElement& myParentXml) + { + for (const auto& attribute : myParentXml.getAttributeIterator()) + { + // Name and value are available as attribute.name and attribute.value + } + } + @endcode + */ + AttributeIterator getAttributeIterator() const + { + return AttributeIterator { attributes.get() }; + } + #ifndef DOXYGEN [[deprecated]] void macroBasedForLoop() const noexcept {} @@ -771,25 +901,10 @@ public: private: //============================================================================== - struct XmlAttributeNode - { - XmlAttributeNode (const XmlAttributeNode&) noexcept; - XmlAttributeNode (const Identifier&, const String&) noexcept; - XmlAttributeNode (String::CharPointerType, String::CharPointerType); - - LinkedListPointer nextListItem; - Identifier name; - String value; - - private: - XmlAttributeNode& operator= (const XmlAttributeNode&) = delete; - }; - friend class XmlDocument; friend class LinkedListPointer; friend class LinkedListPointer; friend class LinkedListPointer::Appender; - friend class NamedValueSet; LinkedListPointer nextListItem, firstChildElement; LinkedListPointer attributes; @@ -800,9 +915,9 @@ private: void writeElementAsText (OutputStream&, int, int, const char*) const; void getChildElementsAsArray (XmlElement**) const noexcept; void reorderChildElements (XmlElement**, int) noexcept; - XmlAttributeNode* getAttribute (StringRef) const noexcept; + const XmlAttribute* getAttribute (StringRef) const noexcept; - // Sigh.. L"" or _T ("") string literals are problematic in general, and really inappropriate + // L"" or _T ("") string literals are problematic in general, and really inappropriate // for XML tags. Use a UTF-8 encoded literal instead, or if you're really determined to use // UTF-16, cast it to a String and use the other constructor. XmlElement (const wchar_t*) = delete;