/************************************************************************* ** XMLNode.cpp ** ** ** ** This file is part of dvisvgm -- a fast DVI to SVG converter ** ** Copyright (C) 2005-2024 Martin Gieseking ** ** ** ** This program is free software; you can redistribute it and/or ** ** modify it under the terms of the GNU General Public License as ** ** published by the Free Software Foundation; either version 3 of ** ** the License, or (at your option) any later version. ** ** ** ** This program is distributed in the hope that it will be useful, but ** ** WITHOUT ANY WARRANTY; without even the implied warranty of ** ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** ** GNU General Public License for more details. ** ** ** ** You should have received a copy of the GNU General Public License ** ** along with this program; if not, see . ** *************************************************************************/ #include #include #include #include #include #include "FileSystem.hpp" #include "utility.hpp" #include "XMLNode.hpp" #include "XMLString.hpp" using namespace std; bool XMLNode::KEEP_ENCODED_FILES=false; bool XMLElement::WRITE_NEWLINES=true; /** Inserts a sibling node after this one. * @param[in] node node to insert * @return raw pointer to inserted node */ XMLNode* XMLNode::insertNext (unique_ptr node) { if (_next) { _next->_prev = node.get(); node->_next = std::move(_next); } node->_prev = this; node->_parent = _parent; _next = std::move(node); return _next.get(); } /** Removes the following sibling node of this one. * @return pointer to the removed node */ unique_ptr XMLNode::removeNext () { unique_ptr oldnext = std::move(_next); if (oldnext) { oldnext->_parent = oldnext->_prev = nullptr; if ((_next = std::move(oldnext->_next))) { _next->_prev = this; oldnext->_next.reset(); } } return oldnext; } XMLElement* XMLNode::nextElement () const { for (XMLNode *node = next(); node; node = node->next()) if (node->toElement()) return node->toElement(); return nullptr; } ///////////////////////////////////////////////////////////////////// XMLElement::XMLElement (string name) : _name(std::move(name)) { } XMLElement::XMLElement (const XMLElement &node) : XMLNode(node), _name(node._name), _attributes(node._attributes) { for (XMLNode *child=node._firstChild.get(); child; child = child->next()) insertLast(child->clone()); } XMLElement::XMLElement (XMLElement &&node) noexcept : XMLNode(std::move(node)), _name(std::move(node._name)), _attributes(std::move(node._attributes)), _firstChild(std::move(node._firstChild)), _lastChild(node._lastChild) { } XMLElement::~XMLElement () { // explicitly remove child nodes by iteration to prevent deep recursion unique_ptr child = std::move(_firstChild); while (child && child->next()) child->removeNext(); } /** Removes all attributes and children. */ void XMLElement::clear () { _attributes.clear(); _firstChild.reset(); _lastChild = nullptr; } /** Returns true if element has no child nodes or, alternatively, only whitespace children. * @param[in] ignoreWhitespace if true and if there are only whitespace children, the functions returns true */ bool XMLElement::empty (bool ignoreWhitespace) const { if (!_firstChild || !ignoreWhitespace) return _firstChild == nullptr; for (const XMLNode *node : *this) { if (!node->toWSNode()) return false; } return true; } void XMLElement::addAttribute (const string &name, const string &value) { if (Attribute *attr = getAttribute(name)) attr->value = value; else _attributes.emplace_back(name, value); } void XMLElement::addAttribute (const string &name, double value) { addAttribute(name, XMLString(value)); } void XMLElement::removeAttribute (const std::string &name) { _attributes.erase(find_if(_attributes.begin(), _attributes.end(), [&](const Attribute &attr) { return attr.name == name; })); } /** Inserts a new last child node and returns a raw pointer to it. */ XMLNode* XMLElement::insertLast (unique_ptr child) { if (!child) return nullptr; child->parent(this); if (!empty()) _lastChild = _lastChild->insertNext(std::move(child)); else { _firstChild = std::move(child); _lastChild = _firstChild.get(); } return _lastChild; } /** Inserts a new first child node and returns a raw pointer to it. */ XMLNode* XMLElement::insertFirst (unique_ptr child) { if (!child) return nullptr; child->parent(this); if (empty()) { _firstChild = std::move(child); _lastChild = _firstChild.get(); } else { child->insertNext(std::move(_firstChild)); _firstChild = std::move(child); } return _firstChild.get(); } /** Appends a child node to this element. The element also takes the ownership of the child. * @param[in] child node to be appended * @return raw pointer to the appended child node */ XMLNode* XMLElement::append (unique_ptr child) { if (!child) return nullptr; XMLText *textNode1 = child->toText(); if (!textNode1 || empty()) insertLast(std::move(child)); else { if (XMLText *textNode2 = _lastChild->toText()) textNode2->append(util::static_unique_ptr_cast(std::move(child))); // merge two consecutive text nodes else insertLast(std::move(child)); } return _lastChild; } /** Appends a string to this element. If the last child is a text node, the string is * appended there, otherwise a new text node is created. * @param[in] str string to be appended * @return raw pointer to the text node the string was appended to */ XMLNode* XMLElement::append (const string &str) { XMLText *last; if (!empty() && (last = _lastChild->toText())) last->append(str); else insertLast(util::make_unique(str)); return _lastChild; } /** Prepends a child node to this element. The element also takes the ownership of the child. * @param[in] child node to be prepended * @return raw pointer to the prepended child node */ XMLNode* XMLElement::prepend (unique_ptr child) { if (!child) return nullptr; XMLText *textNode1 = child->toText(); if (textNode1 && !empty()) { if (XMLText *textNode2 = _firstChild->toText()) { textNode2->prepend(util::static_unique_ptr_cast(std::move(child))); // merge two consecutive text nodes return textNode2; } } insertFirst(std::move(child)); return _firstChild.get(); } /** Inserts a new child node before a given child node already present. The latter * will be the following sibling of the node to be inserted. If there's no such * node present, nothing is inserted. * @param[in] child node to be inserted * @param[in] sibling following sibling of 'child' * @return raw pointer to inserted node */ XMLNode* XMLElement::insertBefore (unique_ptr child, XMLNode *sibling) { if (!child || (sibling && sibling->parent() != this)) return nullptr; if (!sibling) return insertLast(std::move(child)); if (sibling == _firstChild.get()) return insertFirst(std::move(child)); return sibling->prev()->insertNext(std::move(child)); } /** Inserts a new child node after a given child node already present. The latter * will be the preceding sibling of the node to be inserted. If there's no such * node present, nothing is inserted. * @param[in] child node to be inserted * @param[in] sibling preceding sibling of 'child' * @return raw pointer to inserted node */ XMLNode* XMLElement::insertAfter (unique_ptr child, XMLNode *sibling) { if (!child || (sibling && sibling->parent() != this)) return nullptr; if (!sibling) return insertFirst(std::move(child)); if (sibling == _lastChild) return insertLast(std::move(child)); return sibling->insertNext(std::move(child)); } /** Moves a sequence of child nodes to a new element of a given name and inserts * this (wrapper) element at the former position of the first node of the sequence. * Example: wrap 3 child nodes of element a with b: * text1text2 => text1text2 * @param[in] first first node to wrap * @param[in] last last node to wrap (or nullptr if all following siblings of 'first' are to be wrapped) * @param[in] name name of the wrapper element to be created * @return raw pointer to the new wrapper element */ XMLElement* XMLElement::wrap (XMLNode *first, XMLNode *last, const string &name) { if (!first || !first->parent() || (last && first->parent() != last->parent())) return nullptr; XMLElement *parent = first->parent()->toElement(); XMLNode *prev = first->prev(); auto wrapper = util::make_unique(name); if (last) last = last->next(); XMLNode *child = first; while (child && child != last) { XMLNode *next = child->next(); wrapper->insertLast(detach(child)); child = next; } XMLElement *ret = wrapper.get(); if (prev) parent->insertAfter(std::move(wrapper), prev); else parent->insertFirst(std::move(wrapper)); return ret; } /** Moves all child nodes C1,...,Cn of a given element E to its parent and * removes E afterwards, so that C1 is located at the former position of E * followed by C2,...,Cn. * Example: unwrap a child element b of a: * text1text2 => text1text2 * @param[in] child child element to unwrap * @return raw pointer to the first node C1 of the unwrapped sequence or nullptr if element was empty */ XMLNode* XMLElement::unwrap (XMLElement *element) { if (!element || !element->parent()) return nullptr; XMLElement *parent = element->parent()->toElement(); XMLNode *prev = element->prev(); auto detachedElement = util::static_unique_ptr_cast(detach(element)); if (detachedElement->empty()) return nullptr; XMLNode *firstChild = detachedElement->firstChild(); while (auto child = detach(detachedElement->firstChild())) prev = parent->insertAfter(std::move(child), prev); return firstChild; } /** Isolates a node and its descendants from a subtree. * @param[in] node raw pointer to node to be detached * @return unique pointer to the detached node. */ unique_ptr XMLElement::detach (XMLNode *node) { unique_ptr uniqueNode; if (node && node->parent()) { XMLElement *parent = node->parent()->toElement(); if (node == parent->_lastChild) parent->_lastChild = node->prev(); if (node != parent->firstChild()) uniqueNode = node->prev()->removeNext(); else { uniqueNode = std::move(parent->_firstChild); if ((parent->_firstChild = std::move(uniqueNode->_next))) parent->_firstChild->prev(nullptr); } node->parent(nullptr); } return uniqueNode; } /** Gets all descendant elements with a given name and given attribute. * @param[in] name name of elements to find * @param[in] attrName name of attribute to find * @param[out] descendants all elements found * @return true if at least one element was found */ bool XMLElement::getDescendants (const char *name, const char *attrName, vector &descendants) const { for (XMLNode *child = _firstChild.get(); child; child = child->next()) { if (XMLElement *elem = child->toElement()) { if (!name || !name[0] || (name[0] != '!' && elem->name() == name) || (name[0] == '!' && elem->name() != name+1)) { if (!attrName || elem->hasAttribute(attrName)) descendants.push_back(elem); } elem->getDescendants(name, attrName, descendants); } } return !descendants.empty(); } /** Returns the first descendant element that matches the given properties in depth first order. * @param[in] name element name; if 0, all elements are taken into account * @param[in] attrName if not 0, only elements with an attribute of this name are considered * @param[in] attrValue if not 0, only elements with attribute attrName="attrValue" are considered * @return pointer to the found element or 0 */ XMLElement* XMLElement::getFirstDescendant (const char *name, const char *attrName, const char *attrValue) const { for (XMLNode *child = _firstChild.get(); child; child = child->next()) { if (XMLElement *elem = child->toElement()) { if (!name || elem->name() == name) { const char *value; if (!attrName || (((value = elem->getAttributeValue(attrName)) != nullptr) && (!attrValue || string(value) == attrValue))) return elem; } if (XMLElement *descendant = elem->getFirstDescendant(name, attrName, attrValue)) return descendant; } } return nullptr; } ostream& XMLElement::write (ostream &os) const { os << '<' << _name; for (const auto &attrib : _attributes) { os << ' '; if (attrib.name.front() != '@') os << attrib.name << "='" << attrib.value << '\''; else { bool keep = (attrib.name.size() > 1 && attrib.name[1] == '@'); os << attrib.name.substr(keep ? 2 : 1) << "='"; auto pos = attrib.value.find("base64,"); if (pos == string::npos) os << attrib.value; else { os << attrib.value.substr(0, pos+7); string fname = attrib.value.substr(pos+7); ifstream ifs(fname, ios::binary); if (ifs) { os << '\n'; util::base64_copy(ifs, os, 200); ifs.close(); if (!KEEP_ENCODED_FILES && !keep) FileSystem::remove(fname); } } os << "'"; } } if (empty()) os << "/>"; else { os << '>'; // Insert newlines around children except text nodes. According to the // SVG specification, pure whitespace nodes are ignored by the SVG renderer. if (WRITE_NEWLINES && name() != "text" && !_firstChild->toText()) os << '\n'; for (XMLNode *child = _firstChild.get(); child; child = child->next()) { child->write(os); if (!child->toText()) { if (WRITE_NEWLINES && name() != "text" && (!child->next() || !child->next()->toText())) os << '\n'; } } os << "'; } return os; } /** Returns true if this element has an attribute of given name. */ bool XMLElement::hasAttribute (const string &name) const { return getAttribute(name) != nullptr; } /** Returns the value of an attribute. * @param[in] name name of attribute * @return attribute value or 0 if attribute doesn't exist */ const char* XMLElement::getAttributeValue (const std::string& name) const { if (const Attribute *attr = getAttribute(name)) return attr->value.c_str(); return nullptr; } XMLElement::Attribute* XMLElement::getAttribute (const string &name) { auto it = find_if(_attributes.begin(), _attributes.end(), [&](const Attribute &attr) { return attr.name == name; }); return it != _attributes.end() ? &(*it) : nullptr; } const XMLElement::Attribute* XMLElement::getAttribute (const string &name) const { auto it = find_if(_attributes.begin(), _attributes.end(), [&](const Attribute &attr) { return attr.name == name; }); return it != _attributes.end() ? &(*it) : nullptr; } /** Checks whether an SVG attribute A of an element E implicitly propagates its properties * to all child elements of E that don't specify A. For now we only consider a subset of * the inheritable properties. * @return true if the attribute is inheritable */ bool XMLElement::Attribute::inheritable () const { // subset of inheritable properties listed on https://www.w3.org/TR/SVG11/propidx.html // clip-path is not inheritable but can be moved to the parent element as long as // no child gets an different clip-path attribute // https://www.w3.org/TR/SVG11/styling.html#Inheritance static const char *names[] = { "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "direction", "fill", "fill-opacity", "fill-rule", "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "glyph-orientation-horizontal", "glyph-orientation-vertical", "letter-spacing", "paint-order", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "transform", "visibility", "word-spacing", "writing-mode" }; return binary_search(std::begin(names), std::end(names), name, [](const string &name1, const string &name2) { return name1 < name2; }); } ////////////////////// void XMLText::append (unique_ptr node) { if (!node) return; if (node->toText()) append(util::static_unique_ptr_cast(std::move(node))); else { // append text representation of the node ostringstream oss; node->write(oss); append(XMLString(oss.str())); } } void XMLText::append (unique_ptr node) { if (node) _text += node->_text; } void XMLText::append (const string &str) { _text += str; } void XMLText::prepend (unique_ptr node) { if (XMLText *textNode = node->toText()) _text = textNode->_text + _text; } const XMLText* XMLText::toWSNode () const { return _text.find_first_not_of(" \t\n\r") == string::npos ? this : nullptr; } ///////////////////////////////////////////////////////////////////// ostream& XMLCData::write (ostream &os) const { if (!_data.empty()) os << ""; return os; } void XMLCData::append (string &&str) { if (_data.empty()) _data = std::move(str); else _data += str; }