summaryrefslogtreecommitdiffstats
path: root/src/xml
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:29:01 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 16:29:01 +0000
commit35a96bde514a8897f6f0fcc41c5833bf63df2e2a (patch)
tree657d15a03cc46bd099fc2c6546a7a4ad43815d9f /src/xml
parentInitial commit. (diff)
downloadinkscape-35a96bde514a8897f6f0fcc41c5833bf63df2e2a.tar.xz
inkscape-35a96bde514a8897f6f0fcc41c5833bf63df2e2a.zip
Adding upstream version 1.0.2.upstream/1.0.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/xml')
-rw-r--r--src/xml/CMakeLists.txt55
-rw-r--r--src/xml/README12
-rw-r--r--src/xml/attribute-record.h59
-rw-r--r--src/xml/comment-node.h57
-rw-r--r--src/xml/composite-node-observer.cpp336
-rw-r--r--src/xml/composite-node-observer.h109
-rw-r--r--src/xml/croco-node-iface.cpp86
-rw-r--r--src/xml/croco-node-iface.h21
-rw-r--r--src/xml/document.h119
-rw-r--r--src/xml/element-node.h52
-rw-r--r--src/xml/event-fns.h37
-rw-r--r--src/xml/event.cpp526
-rw-r--r--src/xml/event.h260
-rw-r--r--src/xml/helper-observer.cpp77
-rw-r--r--src/xml/helper-observer.h60
-rw-r--r--src/xml/invalid-operation-exception.h45
-rw-r--r--src/xml/log-builder.cpp82
-rw-r--r--src/xml/log-builder.h87
-rw-r--r--src/xml/node-event-vector.h68
-rw-r--r--src/xml/node-fns.cpp101
-rw-r--r--src/xml/node-fns.h84
-rw-r--r--src/xml/node-iterators.h63
-rw-r--r--src/xml/node-observer.h179
-rw-r--r--src/xml/node.h543
-rw-r--r--src/xml/pi-node.h53
-rw-r--r--src/xml/quote-test.h87
-rw-r--r--src/xml/quote.cpp88
-rw-r--r--src/xml/quote.h19
-rw-r--r--src/xml/rebase-hrefs.cpp226
-rw-r--r--src/xml/rebase-hrefs.h61
-rw-r--r--src/xml/repr-action-test.h111
-rw-r--r--src/xml/repr-css.cpp519
-rw-r--r--src/xml/repr-io.cpp1065
-rw-r--r--src/xml/repr-sorting.cpp74
-rw-r--r--src/xml/repr-sorting.h51
-rw-r--r--src/xml/repr-util.cpp661
-rw-r--r--src/xml/repr.cpp59
-rw-r--r--src/xml/repr.h246
-rw-r--r--src/xml/simple-document.cpp137
-rw-r--r--src/xml/simple-document.h98
-rw-r--r--src/xml/simple-node.cpp825
-rw-r--r--src/xml/simple-node.h172
-rw-r--r--src/xml/sp-css-attr.h31
-rw-r--r--src/xml/subtree.cpp63
-rw-r--r--src/xml/subtree.h74
-rw-r--r--src/xml/text-node.h64
46 files changed, 7902 insertions, 0 deletions
diff --git a/src/xml/CMakeLists.txt b/src/xml/CMakeLists.txt
new file mode 100644
index 0000000..b5a4f18
--- /dev/null
+++ b/src/xml/CMakeLists.txt
@@ -0,0 +1,55 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+set(xml_SRC
+ composite-node-observer.cpp
+ croco-node-iface.cpp
+ event.cpp
+ log-builder.cpp
+ node-fns.cpp
+ quote.cpp
+ repr.cpp
+ repr-css.cpp
+ repr-io.cpp
+ repr-sorting.cpp
+ repr-util.cpp
+ simple-document.cpp
+ simple-node.cpp
+ subtree.cpp
+ helper-observer.cpp
+ rebase-hrefs.cpp
+
+
+ # -------
+ # Headers
+ attribute-record.h
+ comment-node.h
+ composite-node-observer.h
+ croco-node-iface.h
+ document.h
+ element-node.h
+ event-fns.h
+ event.h
+ helper-observer.h
+ invalid-operation-exception.h
+ log-builder.h
+ node-event-vector.h
+ node-fns.h
+ node-iterators.h
+ node-observer.h
+ node.h
+ pi-node.h
+ quote-test.h
+ quote.h
+ rebase-hrefs.h
+ repr-action-test.h
+ repr-sorting.h
+ repr.h
+ simple-document.h
+ simple-node.h
+ sp-css-attr.h
+ subtree.h
+ text-node.h
+)
+
+# add_inkscape_lib(xml_LIB "${xml_SRC}")
+add_inkscape_source("${xml_SRC}")
diff --git a/src/xml/README b/src/xml/README
new file mode 100644
index 0000000..d46e7e0
--- /dev/null
+++ b/src/xml/README
@@ -0,0 +1,12 @@
+
+
+This directory contains code that handles the XML tree.
+
+Classes to store the parsed XML of an SVG document. Fairly generic,
+and doesn't contain significant SVG-specific functionality. The main
+distinguishing features (from something like libxml++) are
+notifications about XML changes and undo functionality. This subsystem
+is garbage-collected. Because XML nodes were formerly C structures
+called SPRepr, the XML tree is sometimes called the "repr tree", and
+XML nodes "reprs" (short for "representation").
+
diff --git a/src/xml/attribute-record.h b/src/xml/attribute-record.h
new file mode 100644
index 0000000..6808040
--- /dev/null
+++ b/src/xml/attribute-record.h
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2017 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+/** @file
+ * @brief Key-value pair representing an attribute
+ */
+
+#ifndef SEEN_XML_SP_REPR_ATTR_H
+#define SEEN_XML_SP_REPR_ATTR_H
+
+#include <glib.h>
+#include "inkgc/gc-managed.h"
+#include "util/share.h"
+
+#define SP_REPR_ATTRIBUTE_KEY(a) g_quark_to_string((a)->key)
+#define SP_REPR_ATTRIBUTE_VALUE(a) ((a)->value)
+
+namespace Inkscape {
+namespace XML {
+
+/**
+ * @brief Key-value pair representing an attribute
+ *
+ * Internally, the attributes of each node in the XML tree are
+ * represented by this structure.
+ */
+struct AttributeRecord : public Inkscape::GC::Managed<> {
+ AttributeRecord(GQuark k, Inkscape::Util::ptr_shared v)
+ : key(k), value(v) {}
+
+ /** @brief GQuark corresponding to the name of the attribute */
+ GQuark key;
+ /** @brief Shared pointer to the value of the attribute */
+ Inkscape::Util::ptr_shared value;
+
+ // accept default copy constructor and assignment operator
+};
+
+}
+}
+
+#endif /* !SEEN_XML_SP_REPR_ATTR_H */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/xml/comment-node.h b/src/xml/comment-node.h
new file mode 100644
index 0000000..3298152
--- /dev/null
+++ b/src/xml/comment-node.h
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Comment node implementation
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright 2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+
+#ifndef SEEN_INKSCAPE_XML_COMMENT_NODE_H
+#define SEEN_INKSCAPE_XML_COMMENT_NODE_H
+
+#include <glib.h>
+#include "xml/simple-node.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+/**
+ * @brief Comment node, e.g. &lt;!-- Some comment --&gt;
+ */
+struct CommentNode : public SimpleNode {
+ CommentNode(Util::ptr_shared content, Document *doc)
+ : SimpleNode(g_quark_from_static_string("comment"), doc)
+ {
+ setContent(content);
+ }
+
+ CommentNode(CommentNode const &other, Document *doc)
+ : SimpleNode(other, doc) {}
+
+ Inkscape::XML::NodeType type() const override { return Inkscape::XML::COMMENT_NODE; }
+
+protected:
+ SimpleNode *_duplicate(Document* doc) const override { return new CommentNode(*this, doc); }
+};
+
+}
+
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/composite-node-observer.cpp b/src/xml/composite-node-observer.cpp
new file mode 100644
index 0000000..77a1bd2
--- /dev/null
+++ b/src/xml/composite-node-observer.cpp
@@ -0,0 +1,336 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+/*
+ * Inkscape::XML::CompositeNodeObserver - combine multiple observers
+ *
+ * Copyright 2005 MenTaLguY <mental@rydia.net>
+ *
+ * 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 2
+ * of the License, or (at your option) any later version.
+ *
+ * See the file COPYING for details.
+ *
+ */
+
+#include <cstring>
+#include <glib.h>
+
+#include "util/find-if-before.h"
+#include "xml/composite-node-observer.h"
+#include "xml/node-event-vector.h"
+#include "debug/event-tracker.h"
+#include "debug/simple-event.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+void CompositeNodeObserver::notifyChildAdded(Node &node, Node &child, Node *prev)
+{
+ _startIteration();
+ for (auto & iter : _active)
+ {
+ if (!iter.marked) {
+ iter.observer.notifyChildAdded(node, child, prev);
+ }
+ }
+ _finishIteration();
+}
+
+void CompositeNodeObserver::notifyChildRemoved(Node &node, Node &child,
+ Node *prev)
+{
+ _startIteration();
+ for (auto & iter : _active)
+ {
+ if (!iter.marked) {
+ iter.observer.notifyChildRemoved(node, child, prev);
+ }
+ }
+ _finishIteration();
+}
+
+void CompositeNodeObserver::notifyChildOrderChanged(Node &node, Node &child,
+ Node *old_prev,
+ Node *new_prev)
+{
+ _startIteration();
+ for (auto & iter : _active)
+ {
+ if (!iter.marked) {
+ iter.observer.notifyChildOrderChanged(node, child, old_prev, new_prev);
+ }
+ }
+ _finishIteration();
+}
+
+void CompositeNodeObserver::notifyContentChanged(
+ Node &node,
+ Util::ptr_shared old_content, Util::ptr_shared new_content
+) {
+ _startIteration();
+ for (auto & iter : _active)
+ {
+ if (!iter.marked) {
+ iter.observer.notifyContentChanged(node, old_content, new_content);
+ }
+ }
+ _finishIteration();
+}
+
+void CompositeNodeObserver::notifyAttributeChanged(
+ Node &node, GQuark name,
+ Util::ptr_shared old_value, Util::ptr_shared new_value
+) {
+ _startIteration();
+ for (auto & iter : _active)
+ {
+ if (!iter.marked) {
+ iter.observer.notifyAttributeChanged(node, name, old_value, new_value);
+ }
+ }
+ _finishIteration();
+}
+
+void CompositeNodeObserver::notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name)
+{
+ _startIteration();
+ for (auto& iter : _active) {
+ if (!iter.marked) {
+ iter.observer.notifyElementNameChanged(node, old_name, new_name);
+ }
+ }
+ _finishIteration();
+}
+
+void CompositeNodeObserver::add(NodeObserver &observer) {
+ if (_iterating) {
+ _pending.push_back(ObserverRecord(observer));
+ } else {
+ _active.push_back(ObserverRecord(observer));
+ }
+}
+
+namespace {
+
+class VectorNodeObserver : public NodeObserver, public GC::Managed<> {
+public:
+ VectorNodeObserver(NodeEventVector const &v, void *d)
+ : vector(v), data(d) {}
+
+ NodeEventVector const &vector;
+ void * const data;
+
+ void notifyChildAdded(Node &node, Node &child, Node *prev) override {
+ if (vector.child_added) {
+ vector.child_added(&node, &child, prev, data);
+ }
+ }
+
+ void notifyChildRemoved(Node &node, Node &child, Node *prev) override {
+ if (vector.child_removed) {
+ vector.child_removed(&node, &child, prev, data);
+ }
+ }
+
+ void notifyChildOrderChanged(Node &node, Node &child, Node *old_prev, Node *new_prev) override {
+ if (vector.order_changed) {
+ vector.order_changed(&node, &child, old_prev, new_prev, data);
+ }
+ }
+
+ void notifyContentChanged(Node &node, Util::ptr_shared old_content, Util::ptr_shared new_content) override {
+ if (vector.content_changed) {
+ vector.content_changed(&node, old_content, new_content, data);
+ }
+ }
+
+ void notifyAttributeChanged(Node &node, GQuark name, Util::ptr_shared old_value, Util::ptr_shared new_value) override {
+ if (vector.attr_changed) {
+ vector.attr_changed(&node, g_quark_to_string(name), old_value, new_value, false, data);
+ }
+ }
+
+ void notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) override {
+ if (vector.element_name_changed) {
+ vector.element_name_changed(&node, g_quark_to_string(old_name), g_quark_to_string(new_name), data);
+ }
+ }
+};
+
+}
+
+void CompositeNodeObserver::addListener(NodeEventVector const &vector,
+ void *data)
+{
+ Debug::EventTracker<Debug::SimpleEvent<Debug::Event::XML> > tracker("add-listener");
+ add(*(new VectorNodeObserver(vector, data)));
+}
+
+namespace {
+
+typedef CompositeNodeObserver::ObserverRecord ObserverRecord;
+typedef CompositeNodeObserver::ObserverRecordList ObserverRecordList;
+
+template <typename ObserverPredicate>
+struct unmarked_record_satisfying {
+ ObserverPredicate predicate;
+ unmarked_record_satisfying(ObserverPredicate p) : predicate(p) {}
+ bool operator()(ObserverRecord const &record) {
+ return !record.marked && predicate(record.observer);
+ }
+};
+
+template <typename Predicate>
+bool mark_one(ObserverRecordList &observers, unsigned &/*marked_count*/,
+ Predicate p)
+{
+ ObserverRecordList::iterator found=std::find_if(
+ observers.begin(), observers.end(),
+ unmarked_record_satisfying<Predicate>(p)
+ );
+
+ if ( found != observers.end() ) {
+ found->marked = true;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+template <typename Predicate>
+bool remove_one(ObserverRecordList &observers, unsigned &/*marked_count*/,
+ Predicate p)
+{
+ if (observers.empty()) {
+ return false;
+ }
+
+ if (unmarked_record_satisfying<Predicate>(p)(observers.front())) {
+ observers.pop_front();
+ return true;
+ }
+
+ ObserverRecordList::iterator found=Algorithms::find_if_before(
+ observers.begin(), observers.end(),
+ unmarked_record_satisfying<Predicate>(p)
+ );
+
+ if ( found != observers.end() ) {
+ observers.erase_after(found);
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool is_marked(ObserverRecord const &record) { return record.marked; }
+
+void remove_all_marked(ObserverRecordList &observers, unsigned &marked_count)
+{
+ ObserverRecordList::iterator iter;
+
+ g_assert( !observers.empty() || !marked_count );
+
+ while ( marked_count && observers.front().marked ) {
+ observers.pop_front();
+ --marked_count;
+ }
+
+ iter = observers.begin();
+ while (marked_count) {
+ iter = Algorithms::find_if_before(iter, observers.end(), is_marked);
+ observers.erase_after(iter);
+ --marked_count;
+ }
+}
+
+}
+
+void CompositeNodeObserver::_finishIteration() {
+ if (!--_iterating) {
+ remove_all_marked(_active, _active_marked);
+ remove_all_marked(_pending, _pending_marked);
+ _active.insert(_active.end(), _pending.begin(), _pending.end());
+ _pending.clear();
+ }
+}
+
+namespace {
+
+struct eql_observer {
+ NodeObserver const &observer;
+ eql_observer(NodeObserver const &o) : observer(o) {}
+ bool operator()(NodeObserver const &other) {
+ return &observer == &other;
+ }
+};
+
+}
+
+void CompositeNodeObserver::remove(NodeObserver &observer) {
+ eql_observer p(observer);
+ if (_iterating) {
+ mark_one(_active, _active_marked, p) ||
+ mark_one(_pending, _pending_marked, p);
+ } else {
+ remove_one(_active, _active_marked, p) ||
+ remove_one(_pending, _pending_marked, p);
+ }
+}
+
+namespace {
+
+struct vector_data_matches {
+ void * const data;
+ vector_data_matches(void *d) : data(d) {}
+
+ bool operator()(NodeObserver const &observer) {
+ VectorNodeObserver const *vo=dynamic_cast<VectorNodeObserver const *>(&observer);
+ bool OK = false;
+ if (vo) {
+ if (vo && vo->data == data) {
+ OK = true;
+ }
+ }
+ return OK;
+ }
+};
+
+}
+
+void CompositeNodeObserver::removeListenerByData(void *data) {
+ Debug::EventTracker<Debug::SimpleEvent<Debug::Event::XML> > tracker("remove-listener-by-data");
+ vector_data_matches p(data);
+ if (_iterating) {
+ mark_one(_active, _active_marked, p) ||
+ mark_one(_pending, _pending_marked, p);
+ } else {
+ remove_one(_active, _active_marked, p) ||
+ remove_one(_pending, _pending_marked, p);
+ }
+}
+
+}
+
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/composite-node-observer.h b/src/xml/composite-node-observer.h
new file mode 100644
index 0000000..96f1c91
--- /dev/null
+++ b/src/xml/composite-node-observer.h
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Inkscape::XML::CompositeNodeObserver - combine multiple observers
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Author
+ * Copyright 2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_COMPOSITE_NODE_OBSERVER_H
+#define SEEN_INKSCAPE_XML_COMPOSITE_NODE_OBSERVER_H
+
+#include "inkgc/gc-managed.h"
+#include "xml/node-observer.h"
+#include "util/list-container.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+struct NodeEventVector;
+
+/**
+ * @brief An observer that relays notifications to multiple other observers
+ *
+ * This special observer keeps a list of other observer objects and sends
+ * the notifications it receives to all of them. The implementation of the class
+ * allows an observer to remove itself from this object during a method call.
+ * For the documentation of callback methods, see NodeObserver.
+ */
+class CompositeNodeObserver : public NodeObserver, public GC::Managed<> {
+public:
+ struct ObserverRecord : public GC::Managed<> {
+ explicit ObserverRecord(NodeObserver &o) : observer(o), marked(false) {}
+
+ NodeObserver &observer;
+ bool marked; //< if marked for removal
+ };
+ typedef Util::ListContainer<ObserverRecord> ObserverRecordList;
+
+ CompositeNodeObserver()
+ : _iterating(0), _active_marked(0), _pending_marked(0) {}
+
+ /**
+ * @brief Add an observer to the list
+ * @param observer The observer object to add
+ */
+ void add(NodeObserver &observer);
+ /**
+ * @brief Remove an observer from the list
+ * @param observer The observer object to remove
+ */
+ void remove(NodeObserver &observer);
+ /**
+ * @brief Add a set of callbacks with associated data
+ * @deprecated Use add() instead
+ */
+ void addListener(NodeEventVector const &vector, void *data);
+ /**
+ * @brief Remove a set of callbacks by its associated data
+ * @deprecated Use remove() instead
+ */
+ void removeListenerByData(void *data);
+
+ void notifyChildAdded(Node &node, Node &child, Node *prev) override;
+
+ void notifyChildRemoved(Node &node, Node &child, Node *prev) override;
+
+ void notifyChildOrderChanged(Node &node, Node &child,
+ Node *old_prev, Node *new_prev) override;
+
+ void notifyContentChanged(Node &node,
+ Util::ptr_shared old_content,
+ Util::ptr_shared new_content) override;
+
+ void notifyAttributeChanged(Node &node, GQuark name,
+ Util::ptr_shared old_value,
+ Util::ptr_shared new_value) override;
+
+ void notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) override;
+
+private:
+ unsigned _iterating;
+ ObserverRecordList _active;
+ unsigned _active_marked;
+ ObserverRecordList _pending;
+ unsigned _pending_marked;
+
+ void _startIteration() { ++_iterating; }
+ void _finishIteration();
+};
+
+} // namespace XML
+} // namespace Inkscape
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/croco-node-iface.cpp b/src/xml/croco-node-iface.cpp
new file mode 100644
index 0000000..390c7b1
--- /dev/null
+++ b/src/xml/croco-node-iface.cpp
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <cstring>
+#include <string>
+#include <glib.h>
+
+#include "xml/croco-node-iface.h"
+#include "xml/node.h"
+
+static char const *
+local_part(char const *const qname)
+{
+ char const *ret = std::strrchr(qname, ':');
+ if (ret)
+ return ++ret;
+ else
+ return qname;
+}
+
+namespace Inkscape {
+namespace XML {
+
+extern "C" {
+
+static CRXMLNodePtr get_parent(CRXMLNodePtr n) { return static_cast<Node const *>(n)->parent(); }
+static CRXMLNodePtr get_first_child(CRXMLNodePtr n) { return static_cast<Node const *>(n)->firstChild(); }
+static CRXMLNodePtr get_next(CRXMLNodePtr n) { return static_cast<Node const *>(n)->next(); }
+
+static CRXMLNodePtr get_prev(CRXMLNodePtr cn)
+{
+ Node const *n = static_cast<Node const *>(cn);
+ unsigned const n_pos = n->position();
+ if (n_pos) {
+ return n->parent()->nthChild(n_pos - 1);
+ } else {
+ return nullptr;
+ }
+}
+
+static char *get_attr(CRXMLNodePtr n, char const *a)
+{
+ return g_strdup(static_cast<Node const *>(n)->attribute(a));
+}
+
+static char const *get_local_name(CRXMLNodePtr n) { return local_part(static_cast<Node const *>(n)->name()); }
+static gboolean is_element_node(CRXMLNodePtr n) { return static_cast<Node const *>(n)->type() == ELEMENT_NODE; }
+}
+
+/**
+ * Interface for XML nodes used by libcroco.
+ *
+ * This structure defines operations on Inkscape::XML::Node used by the libcroco
+ * CSS parsing library.
+ */
+CRNodeIface const croco_node_iface = {
+ get_parent,
+ get_first_child,
+ get_next,
+ get_prev,
+ get_local_name,
+ get_attr,
+ g_free,
+ is_element_node
+};
+
+}
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/croco-node-iface.h b/src/xml/croco-node-iface.h
new file mode 100644
index 0000000..2965ade
--- /dev/null
+++ b/src/xml/croco-node-iface.h
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef INKSCAPE_SP_REPR_NODE_IFACE_H
+#define INKSCAPE_SP_REPR_NODE_IFACE_H
+
+#include <3rdparty/libcroco/cr-node-iface.h>
+
+namespace Inkscape {
+namespace XML {
+extern CRNodeIface const croco_node_iface;
+}
+}
+
+#endif /* !INKSCAPE_SP_REPR_NODE_IFACE_H */
diff --git a/src/xml/document.h b/src/xml/document.h
new file mode 100644
index 0000000..92878b2
--- /dev/null
+++ b/src/xml/document.h
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Interface for XML documents
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2011 Authors
+ * Copyright 2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_SP_REPR_DOC_H
+#define SEEN_INKSCAPE_XML_SP_REPR_DOC_H
+
+#include "xml/node.h"
+
+namespace Inkscape {
+namespace XML {
+
+/**
+ * @brief Interface for XML documents
+ *
+ * This class represents a complete document tree. You have to go through this class
+ * to create new nodes. It also contains transaction support, which forms the base
+ * of the undo system.
+ *
+ * The document is also a node. It usually contains only two child nodes - a processing
+ * instruction node (PINode) containing the XML prolog, and the root node. You can get
+ * the root node of the document by calling the root() method.
+ *
+ * The name "transaction" can be misleading, because they are not atomic. Their main feature
+ * is that they provide rollback. After starting a transaction,
+ * all changes made to the document are stored in an internal event log. At any time
+ * after starting the transaction, you can call the rollback() method, which restores
+ * the document to the state it was before starting the transaction. Calling the commit()
+ * method causes the internal event log to be discarded, and you can establish a new
+ * "restore point" by calling beginTransaction() again. There can be only one active
+ * transaction at a time for a given document.
+ */
+struct Document : virtual public Node {
+public:
+ /**
+ * @name Document transactions
+ * @{
+ */
+ /**
+ * @brief Checks whether there is an active transaction for this document
+ * @return true if there's an established transaction for this document, false otherwise
+ */
+ virtual bool inTransaction()=0;
+ /**
+ * @brief Begin a transaction and start recording changes
+ *
+ * By calling this method you effectively establish a resotre point.
+ * You can undo all changes made to the document after this call using rollback().
+ */
+ virtual void beginTransaction()=0;
+ /**
+ * @brief Restore the state of the document prior to the transaction
+ *
+ * This method applies the inverses of all recorded changes in reverse order,
+ * restoring the document state from before the transaction. For some implementations,
+ * this function may do nothing.
+ */
+ virtual void rollback()=0;
+ /**
+ * @brief Commit a transaction and discard change data
+ *
+ * This method finishes the active transaction and discards the recorded changes.
+ */
+ virtual void commit()=0;
+ /**
+ * @brief Commit a transaction and store the events for later use
+ *
+ * This method finishes a transaction and returns an event chain
+ * that describes the changes made to the document. This method may return NULL,
+ * which means that the document implementation doesn't support event logging,
+ * or that no changes were made.
+ *
+ * @return Event chain describing the changes, or NULL
+ */
+ virtual Event *commitUndoable()=0;
+ /*@}*/
+
+ /**
+ * @name Create new nodes
+ * @{
+ */
+ virtual Node *createElement(char const *name)=0;
+ virtual Node *createTextNode(char const *content)=0;
+ virtual Node *createTextNode(char const *content, bool is_CData)=0;
+ virtual Node *createComment(char const *content)=0;
+ virtual Node *createPI(char const *target, char const *content)=0;
+ /*@}*/
+
+ /**
+ * @brief Get the event logger for this document
+ *
+ * This is an implementation detail that should not be used outside of node implementations.
+ * It should be made non-public in the future.
+ */
+ virtual NodeObserver *logger()=0;
+};
+
+}
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/element-node.h b/src/xml/element-node.h
new file mode 100644
index 0000000..376790f
--- /dev/null
+++ b/src/xml/element-node.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Element node implementation
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright 2004-2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_ELEMENT_NODE_H
+#define SEEN_INKSCAPE_XML_ELEMENT_NODE_H
+
+#include "xml/simple-node.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+/**
+ * @brief Element node, e.g. &lt;group /&gt;
+ */
+class ElementNode : public SimpleNode {
+public:
+ ElementNode(int code, Document *doc)
+ : SimpleNode(code, doc) {}
+ ElementNode(ElementNode const &other, Document *doc)
+ : SimpleNode(other, doc) {}
+
+ Inkscape::XML::NodeType type() const override { return Inkscape::XML::ELEMENT_NODE; }
+
+protected:
+ SimpleNode *_duplicate(Document* doc) const override { return new ElementNode(*this, doc); }
+};
+
+}
+
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/event-fns.h b/src/xml/event-fns.h
new file mode 100644
index 0000000..f3a6e52
--- /dev/null
+++ b/src/xml/event-fns.h
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_INKSCAPE_XML_SP_REPR_ACTION_FNS_H
+#define SEEN_INKSCAPE_XML_SP_REPR_ACTION_FNS_H
+
+namespace Inkscape {
+namespace XML {
+
+struct Document;
+class Event;
+class NodeObserver;
+
+void replay_log_to_observer(Event const *log, NodeObserver &observer);
+void undo_log_to_observer(Event const *log, NodeObserver &observer);
+
+}
+}
+
+void sp_repr_begin_transaction (Inkscape::XML::Document *doc);
+void sp_repr_rollback (Inkscape::XML::Document *doc);
+void sp_repr_commit (Inkscape::XML::Document *doc);
+Inkscape::XML::Event *sp_repr_commit_undoable (Inkscape::XML::Document *doc);
+
+void sp_repr_undo_log (Inkscape::XML::Event *log);
+void sp_repr_replay_log (Inkscape::XML::Event *log);
+Inkscape::XML::Event *sp_repr_coalesce_log (Inkscape::XML::Event *a, Inkscape::XML::Event *b);
+void sp_repr_free_log (Inkscape::XML::Event *log);
+void sp_repr_debug_print_log(Inkscape::XML::Event const *log);
+
+#endif
diff --git a/src/xml/event.cpp b/src/xml/event.cpp
new file mode 100644
index 0000000..86e042a
--- /dev/null
+++ b/src/xml/event.cpp
@@ -0,0 +1,526 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Repr transaction logging
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright (C) 2004-2005 MenTaLguY
+ * Copyright (C) 1999-2003 authors
+ * Copyright (C) 2001-2002 Ximian, Inc.
+ * g++ port Copyright (C) 2003 Nathan Hurst
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glib.h> // g_assert()
+#include <cstdio>
+
+#include "event.h"
+#include "event-fns.h"
+#include "util/reverse-list.h"
+#include "xml/document.h"
+#include "xml/node-observer.h"
+#include "debug/event-tracker.h"
+#include "debug/simple-event.h"
+
+using Inkscape::Util::List;
+using Inkscape::Util::reverse_list;
+
+int Inkscape::XML::Event::_next_serial=0;
+
+void
+sp_repr_begin_transaction (Inkscape::XML::Document *doc)
+{
+ using Inkscape::Debug::SimpleEvent;
+ using Inkscape::Debug::EventTracker;
+ using Inkscape::Debug::Event;
+
+ EventTracker<SimpleEvent<Event::XML> > tracker("begin-transaction");
+
+ g_assert(doc != nullptr);
+ doc->beginTransaction();
+}
+
+void
+sp_repr_rollback (Inkscape::XML::Document *doc)
+{
+ using Inkscape::Debug::SimpleEvent;
+ using Inkscape::Debug::EventTracker;
+ using Inkscape::Debug::Event;
+
+ EventTracker<SimpleEvent<Event::XML> > tracker("rollback");
+
+ g_assert(doc != nullptr);
+ doc->rollback();
+}
+
+void
+sp_repr_commit (Inkscape::XML::Document *doc)
+{
+ using Inkscape::Debug::SimpleEvent;
+ using Inkscape::Debug::EventTracker;
+ using Inkscape::Debug::Event;
+
+ EventTracker<SimpleEvent<Event::XML> > tracker("commit");
+
+ g_assert(doc != nullptr);
+ doc->commit();
+}
+
+Inkscape::XML::Event *
+sp_repr_commit_undoable (Inkscape::XML::Document *doc)
+{
+ using Inkscape::Debug::SimpleEvent;
+ using Inkscape::Debug::EventTracker;
+ using Inkscape::Debug::Event;
+
+ EventTracker<SimpleEvent<Event::XML> > tracker("commit");
+
+ g_assert(doc != nullptr);
+ return doc->commitUndoable();
+}
+
+namespace {
+
+class LogPerformer : public Inkscape::XML::NodeObserver {
+public:
+ typedef Inkscape::XML::Node Node;
+
+ static LogPerformer &instance() {
+ static LogPerformer singleton;
+ return singleton;
+ }
+
+ void notifyChildAdded(Node &parent, Node &child, Node *ref) override {
+ parent.addChild(&child, ref);
+ }
+
+ void notifyChildRemoved(Node &parent, Node &child, Node */*old_ref*/) override {
+ parent.removeChild(&child);
+ }
+
+ void notifyChildOrderChanged(Node &parent, Node &child,
+ Node */*old_ref*/, Node *new_ref) override
+ {
+ parent.changeOrder(&child, new_ref);
+ }
+
+ void notifyAttributeChanged(Node &node, GQuark name,
+ Inkscape::Util::ptr_shared /*old_value*/,
+ Inkscape::Util::ptr_shared new_value) override
+ {
+ node.setAttribute(g_quark_to_string(name), new_value);
+ }
+
+ void notifyContentChanged(Node &node,
+ Inkscape::Util::ptr_shared /*old_value*/,
+ Inkscape::Util::ptr_shared new_value) override
+ {
+ node.setContent(new_value);
+ }
+
+ void notifyElementNameChanged(Node& node, GQuark /*old_value*/, GQuark new_value) override
+ {
+ node.setCodeUnsafe(new_value);
+ }
+};
+
+}
+
+void Inkscape::XML::undo_log_to_observer(
+ Inkscape::XML::Event const *log,
+ Inkscape::XML::NodeObserver &observer
+) {
+ for ( Event const *action = log ; action ; action = action->next ) {
+ action->undoOne(observer);
+ }
+}
+
+void sp_repr_undo_log (Inkscape::XML::Event *log)
+{
+ using Inkscape::Debug::SimpleEvent;
+ using Inkscape::Debug::EventTracker;
+ using Inkscape::Debug::Event;
+
+ EventTracker<SimpleEvent<Event::XML> > tracker("undo-log");
+
+ if (log) {
+ if (log->repr) {
+ g_assert(!log->repr->document()->inTransaction());
+ }
+ }
+
+ Inkscape::XML::undo_log_to_observer(log, LogPerformer::instance());
+}
+
+void Inkscape::XML::EventAdd::_undoOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyChildRemoved(*this->repr, *this->child, this->ref);
+}
+
+void Inkscape::XML::EventDel::_undoOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyChildAdded(*this->repr, *this->child, this->ref);
+}
+
+void Inkscape::XML::EventChgAttr::_undoOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyAttributeChanged(*this->repr, this->key, this->newval, this->oldval);
+}
+
+void Inkscape::XML::EventChgContent::_undoOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyContentChanged(*this->repr, this->newval, this->oldval);
+}
+
+void Inkscape::XML::EventChgOrder::_undoOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyChildOrderChanged(*this->repr, *this->child, this->newref, this->oldref);
+}
+
+void Inkscape::XML::EventChgElementName::_undoOne(
+ Inkscape::XML::NodeObserver& observer
+) const {
+ observer.notifyElementNameChanged(*this->repr, this->new_name, this->old_name);
+}
+
+void Inkscape::XML::replay_log_to_observer(
+ Inkscape::XML::Event const *log,
+ Inkscape::XML::NodeObserver &observer
+) {
+ List<Inkscape::XML::Event const &> reversed =
+ reverse_list<Inkscape::XML::Event::ConstIterator>(log, nullptr);
+ for ( ; reversed ; ++reversed ) {
+ reversed->replayOne(observer);
+ }
+}
+
+void
+sp_repr_replay_log (Inkscape::XML::Event *log)
+{
+ using Inkscape::Debug::SimpleEvent;
+ using Inkscape::Debug::EventTracker;
+ using Inkscape::Debug::Event;
+
+ EventTracker<SimpleEvent<Event::XML> > tracker("replay-log");
+
+ if (log) {
+ if (log->repr->document()) {
+ g_assert(!log->repr->document()->inTransaction());
+ }
+ }
+
+ Inkscape::XML::replay_log_to_observer(log, LogPerformer::instance());
+}
+
+void Inkscape::XML::EventAdd::_replayOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyChildAdded(*this->repr, *this->child, this->ref);
+}
+
+void Inkscape::XML::EventDel::_replayOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyChildRemoved(*this->repr, *this->child, this->ref);
+}
+
+void Inkscape::XML::EventChgAttr::_replayOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyAttributeChanged(*this->repr, this->key, this->oldval, this->newval);
+}
+
+void Inkscape::XML::EventChgContent::_replayOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyContentChanged(*this->repr, this->oldval, this->newval);
+}
+
+void Inkscape::XML::EventChgOrder::_replayOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyChildOrderChanged(*this->repr, *this->child, this->oldref, this->newref);
+}
+
+void Inkscape::XML::EventChgElementName::_replayOne(
+ Inkscape::XML::NodeObserver &observer
+) const {
+ observer.notifyElementNameChanged(*this->repr, this->new_name, this->old_name);
+}
+
+Inkscape::XML::Event *
+sp_repr_coalesce_log (Inkscape::XML::Event *a, Inkscape::XML::Event *b)
+{
+ Inkscape::XML::Event *action;
+ Inkscape::XML::Event **prev_ptr;
+
+ if (!b) return a;
+ if (!a) return b;
+
+ /* find the earliest action in the second log */
+ /* (also noting the pointer that references it, so we can
+ * replace it later) */
+ prev_ptr = &b;
+ for ( action = b ; action->next ; action = action->next ) {
+ prev_ptr = &action->next;
+ }
+
+ /* add the first log after it */
+ action->next = a;
+
+ /* optimize the result */
+ *prev_ptr = action->optimizeOne();
+
+ return b;
+}
+
+void
+sp_repr_free_log (Inkscape::XML::Event *log)
+{
+ while (log) {
+ Inkscape::XML::Event *action;
+ action = log;
+ log = action->next;
+ delete action;
+ }
+}
+
+namespace {
+
+template <typename T> struct ActionRelations;
+
+template <>
+struct ActionRelations<Inkscape::XML::EventAdd> {
+ typedef Inkscape::XML::EventDel Opposite;
+};
+
+template <>
+struct ActionRelations<Inkscape::XML::EventDel> {
+ typedef Inkscape::XML::EventAdd Opposite;
+};
+
+template <typename A>
+Inkscape::XML::Event *cancel_add_or_remove(A *action) {
+ typedef typename ActionRelations<A>::Opposite Opposite;
+ Opposite *opposite=dynamic_cast<Opposite *>(action->next);
+
+ bool OK = false;
+ if (opposite){
+ if (opposite->repr == action->repr &&
+ opposite->child == action->child &&
+ opposite->ref == action->ref ) {
+ OK = true;
+ }
+ }
+ if (OK){
+ Inkscape::XML::Event *remaining=opposite->next;
+
+ delete opposite;
+ delete action;
+
+ return remaining;
+ } else {
+ return action;
+ }
+}
+}
+
+Inkscape::XML::Event *Inkscape::XML::EventAdd::_optimizeOne() {
+ return cancel_add_or_remove(this);
+}
+
+Inkscape::XML::Event *Inkscape::XML::EventDel::_optimizeOne() {
+ return cancel_add_or_remove(this);
+}
+
+Inkscape::XML::Event *Inkscape::XML::EventChgAttr::_optimizeOne() {
+ Inkscape::XML::EventChgAttr *chg_attr=dynamic_cast<Inkscape::XML::EventChgAttr *>(this->next);
+
+ /* consecutive chgattrs on the same key can be combined */
+ if ( chg_attr) {
+ if ( chg_attr->repr == this->repr &&
+ chg_attr->key == this->key )
+ {
+ /* replace our oldval with the prior action's */
+ this->oldval = chg_attr->oldval;
+
+ /* discard the prior action */
+ this->next = chg_attr->next;
+ delete chg_attr;
+ }
+ }
+
+ return this;
+}
+
+Inkscape::XML::Event *Inkscape::XML::EventChgContent::_optimizeOne() {
+ Inkscape::XML::EventChgContent *chg_content=dynamic_cast<Inkscape::XML::EventChgContent *>(this->next);
+
+ /* consecutive content changes can be combined */
+ if (chg_content) {
+ if (chg_content->repr == this->repr ) {
+ /* replace our oldval with the prior action's */
+ this->oldval = chg_content->oldval;
+
+ /* get rid of the prior action*/
+ this->next = chg_content->next;
+ delete chg_content;
+ }
+ }
+
+ return this;
+}
+
+Inkscape::XML::Event *Inkscape::XML::EventChgOrder::_optimizeOne() {
+ Inkscape::XML::EventChgOrder *chg_order=dynamic_cast<Inkscape::XML::EventChgOrder *>(this->next);
+
+ /* consecutive chgorders for the same child may be combined or
+ * canceled out */
+ bool OK = false;
+ if (chg_order) {
+ if (chg_order->repr == this->repr &&
+ chg_order->child == this->child ){
+ OK = true;
+ }
+ }
+ if (OK) {
+ if ( chg_order->oldref == this->newref ) {
+ /* cancel them out */
+ Inkscape::XML::Event *after=chg_order->next;
+
+ delete chg_order;
+ delete this;
+
+ return after;
+ } else {
+ /* combine them */
+ this->oldref = chg_order->oldref;
+
+ /* get rid of the other one */
+ this->next = chg_order->next;
+ delete chg_order;
+
+ return this;
+ }
+ } else {
+ return this;
+ }
+}
+
+Inkscape::XML::Event* Inkscape::XML::EventChgElementName::_optimizeOne() {
+ auto next_chg_element_name = dynamic_cast<Inkscape::XML::EventChgElementName*>(this->next);
+ if (next_chg_element_name && next_chg_element_name->repr == this->repr) {
+ // Combine name changes to the same element.
+ this->old_name = next_chg_element_name->old_name;
+ this->next = next_chg_element_name->next;
+ delete next_chg_element_name;
+ }
+ return this;
+}
+
+namespace {
+
+class LogPrinter : public Inkscape::XML::NodeObserver {
+public:
+ typedef Inkscape::XML::Node Node;
+
+ static LogPrinter &instance() {
+ static LogPrinter singleton;
+ return singleton;
+ }
+
+ static Glib::ustring node_to_string(Node const &node) {
+ Glib::ustring result;
+ char const *type_name=nullptr;
+ switch (node.type()) {
+ case Inkscape::XML::DOCUMENT_NODE:
+ type_name = "Document";
+ break;
+ case Inkscape::XML::ELEMENT_NODE:
+ type_name = "Element";
+ break;
+ case Inkscape::XML::TEXT_NODE:
+ type_name = "Text";
+ break;
+ case Inkscape::XML::COMMENT_NODE:
+ type_name = "Comment";
+ break;
+ default:
+ g_assert_not_reached();
+ }
+ char buffer[40];
+ result.append("#<");
+ result.append(type_name);
+ result.append(":");
+ snprintf(buffer, 40, "0x%p", &node);
+ result.append(buffer);
+ result.append(">");
+
+ return result;
+ }
+
+ static Glib::ustring ref_to_string(Node *ref) {
+ if (ref) {
+ return node_to_string(*ref);
+ } else {
+ return "beginning";
+ }
+ }
+
+ void notifyChildAdded(Node &parent, Node &child, Node *ref) override {
+ g_warning("Event: Added %s to %s after %s", node_to_string(parent).c_str(), node_to_string(child).c_str(), ref_to_string(ref).c_str());
+ }
+
+ void notifyChildRemoved(Node &parent, Node &child, Node */*ref*/) override {
+ g_warning("Event: Removed %s from %s", node_to_string(parent).c_str(), node_to_string(child).c_str());
+ }
+
+ void notifyChildOrderChanged(Node &parent, Node &child,
+ Node */*old_ref*/, Node *new_ref) override
+ {
+ g_warning("Event: Moved %s after %s in %s", node_to_string(child).c_str(), ref_to_string(new_ref).c_str(), node_to_string(parent).c_str());
+ }
+
+ void notifyAttributeChanged(Node &node, GQuark name,
+ Inkscape::Util::ptr_shared /*old_value*/,
+ Inkscape::Util::ptr_shared new_value) override
+ {
+ if (new_value) {
+ g_warning("Event: Set attribute %s to \"%s\" on %s", g_quark_to_string(name), new_value.pointer(), node_to_string(node).c_str());
+ } else {
+ g_warning("Event: Unset attribute %s on %s", g_quark_to_string(name), node_to_string(node).c_str());
+ }
+ }
+
+ void notifyContentChanged(Node &node,
+ Inkscape::Util::ptr_shared /*old_value*/,
+ Inkscape::Util::ptr_shared new_value) override
+ {
+ if (new_value) {
+ g_warning("Event: Set content of %s to \"%s\"", node_to_string(node).c_str(), new_value.pointer());
+ } else {
+ g_warning("Event: Unset content of %s", node_to_string(node).c_str());
+ }
+ }
+
+ void notifyElementNameChanged(Node& node, GQuark old_value, GQuark new_value) override
+ {
+ g_warning("Event: Changed name of %s from %s to %s\n",
+ node_to_string(node).c_str(), g_quark_to_string(old_value), g_quark_to_string(new_value));
+ }
+};
+
+}
+
+void sp_repr_debug_print_log(Inkscape::XML::Event const *log) {
+ Inkscape::XML::replay_log_to_observer(log, LogPrinter::instance());
+}
+
diff --git a/src/xml/event.h b/src/xml/event.h
new file mode 100644
index 0000000..4256eea
--- /dev/null
+++ b/src/xml/event.h
@@ -0,0 +1,260 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Event object representing a change of the XML document
+ *//*
+ * Authors:
+ * Unknown author(s)
+ * Krzysztof Kosiński <tweenk.pl@gmail.com> (documentation)
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_SP_REPR_ACTION_H
+#define SEEN_INKSCAPE_XML_SP_REPR_ACTION_H
+
+typedef unsigned int GQuark;
+#include <glibmm/ustring.h>
+
+#include <iterator>
+#include "util/share.h"
+#include "util/forward-pointer-iterator.h"
+#include "inkgc/gc-managed.h"
+#include "xml/node.h"
+
+namespace Inkscape {
+namespace XML {
+
+/**
+ * @brief Enumeration of all XML event types
+ */
+// enum EventType {
+// EVENT_ADD, ///< Child added
+// EVENT_DEL, ///< Child removed
+// EVENT_CHG_ATTR, ///< Attribute changed
+// EVENT_CHG_CONTENT, ///< Content changed
+// EVENT_CHG_ORDER ///< Order of children changed
+// };
+
+/**
+ * @brief Generic XML modification event
+ *
+ * This is the base class for all other modification events. It is actually a singly-linked
+ * list of events, called an event chain or an event log. Logs of events that happened
+ * in a transaction can be obtained from Document::commitUndoable(). Events can be replayed
+ * to a NodeObserver, or undone (which is equivalent to replaying opposite events in reverse
+ * order).
+ *
+ * Event logs are built by appending to the front, so by walking the list one iterates over
+ * the events in reverse chronological order.
+ */
+class Event
+: public Inkscape::GC::Managed<Inkscape::GC::SCANNED, Inkscape::GC::MANUAL>
+{
+public:
+ virtual ~Event() = default;
+
+ /**
+ * @brief Pointer to the next event in the event chain
+ *
+ * Note that the event this pointer points to actually happened before this event.
+ * This is because the event log is built by appending to the front.
+ */
+ Event *next;
+ /**
+ * @brief Serial number of the event, not used at the moment
+ */
+ int serial;
+ /**
+ * @brief Pointer to the node that was the object of the event
+ *
+ * Because the nodes are garbage-collected, this pointer guarantees that the node
+ * will stay in memory as long as the event does. This simplifies rolling back
+ * extensive deletions.
+ */
+ Node *repr;
+
+ struct IteratorStrategy {
+ static Event const *next(Event const *action) {
+ return action->next;
+ }
+ };
+
+ typedef Inkscape::Util::ForwardPointerIterator<Event, IteratorStrategy> Iterator;
+ typedef Inkscape::Util::ForwardPointerIterator<Event const, IteratorStrategy> ConstIterator;
+
+ /**
+ * @brief If possible, combine this event with the next to reduce memory use
+ * @return Pointer to the optimized event chain, which may have changed
+ */
+ Event *optimizeOne() { return _optimizeOne(); }
+ /**
+ * @brief Undo this event to an observer
+ *
+ * This method notifies the specified observer of an action opposite to the one that
+ * is described by this event.
+ */
+ void undoOne(NodeObserver &observer) const {
+ _undoOne(observer);
+ }
+ /**
+ * @brief Replay this event to an observer
+ *
+ * This method notifies the specified event of the same action that it describes.
+ */
+ void replayOne(NodeObserver &observer) const {
+ _replayOne(observer);
+ }
+
+protected:
+ Event(Node *r, Event *n)
+ : next(n), serial(_next_serial++), repr(r) {}
+
+ virtual Event *_optimizeOne()=0;
+ virtual void _undoOne(NodeObserver &) const=0;
+ virtual void _replayOne(NodeObserver &) const=0;
+
+private:
+ static int _next_serial;
+};
+
+/**
+ * @brief Object representing child addition
+ */
+class EventAdd : public Event {
+public:
+ EventAdd(Node *repr, Node *c, Node *rr, Event *next)
+ : Event(repr, next), child(c), ref(rr) {}
+
+ /// The added child node
+ Node *child;
+ /// The node after which the child has been added, or NULL if it was added as first
+ Node *ref;
+
+private:
+ Event *_optimizeOne() override;
+ void _undoOne(NodeObserver &observer) const override;
+ void _replayOne(NodeObserver &observer) const override;
+};
+
+/**
+ * @brief Object representing child removal
+ */
+class EventDel : public Event {
+public:
+ EventDel(Node *repr, Node *c, Node *rr, Event *next)
+ : Event(repr, next), child(c), ref(rr) {}
+
+ /// The child node that was removed
+ Node *child;
+ /// The node after which the removed node was in the sibling order, or NULL if it was first
+ Node *ref;
+
+private:
+ Event *_optimizeOne() override;
+ void _undoOne(NodeObserver &observer) const override;
+ void _replayOne(NodeObserver &observer) const override;
+};
+
+/**
+ * @brief Object representing attribute change
+ */
+class EventChgAttr : public Event {
+public:
+ EventChgAttr(Node *repr, GQuark k,
+ Inkscape::Util::ptr_shared ov,
+ Inkscape::Util::ptr_shared nv,
+ Event *next)
+ : Event(repr, next), key(k),
+ oldval(ov), newval(nv) {}
+
+ /// GQuark corresponding to the changed attribute's name
+ GQuark key;
+ /// Value of the attribute before the change
+ Inkscape::Util::ptr_shared oldval;
+ /// Value of the attribute after the change
+ Inkscape::Util::ptr_shared newval;
+
+private:
+ Event *_optimizeOne() override;
+ void _undoOne(NodeObserver &observer) const override;
+ void _replayOne(NodeObserver &observer) const override;
+};
+
+/**
+ * @brief Object representing content change
+ */
+class EventChgContent : public Event {
+public:
+ EventChgContent(Node *repr,
+ Inkscape::Util::ptr_shared ov,
+ Inkscape::Util::ptr_shared nv,
+ Event *next)
+ : Event(repr, next), oldval(ov), newval(nv) {}
+
+ /// Content of the node before the change
+ Inkscape::Util::ptr_shared oldval;
+ /// Content of the node after the change
+ Inkscape::Util::ptr_shared newval;
+
+private:
+ Event *_optimizeOne() override;
+ void _undoOne(NodeObserver &observer) const override;
+ void _replayOne(NodeObserver &observer) const override;
+};
+
+/**
+ * @brief Object representing child order change
+ */
+class EventChgOrder : public Event {
+public:
+ EventChgOrder(Node *repr, Node *c, Node *orr, Node *nrr, Event *next)
+ : Event(repr, next), child(c),
+ oldref(orr), newref(nrr) {}
+
+ /// The node that was relocated in sibling order
+ Node *child;
+ /// The node after which the relocated node was in the sibling order before the change, or NULL if it was first
+ Node *oldref;
+ /// The node after which the relocated node is after the change, or if it's first
+ Node *newref;
+
+private:
+ Event *_optimizeOne() override;
+ void _undoOne(NodeObserver &observer) const override;
+ void _replayOne(NodeObserver &observer) const override;
+};
+
+/**
+ * @brief Object representing element name change.
+ */
+class EventChgElementName : public Event {
+public:
+ EventChgElementName(Node* repr, GQuark old_name, GQuark new_name, Event* next)
+ : Event(repr, next), old_name(old_name), new_name(new_name) {}
+
+ /// GQuark corresponding to the old element name.
+ GQuark old_name;
+ /// GQuark corresponding to the new element name.
+ GQuark new_name;
+
+private:
+ Event* _optimizeOne() override;
+ void _undoOne(NodeObserver& observer) const override;
+ void _replayOne(NodeObserver& observer) const override;
+};
+
+}
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/helper-observer.cpp b/src/xml/helper-observer.cpp
new file mode 100644
index 0000000..05161e9
--- /dev/null
+++ b/src/xml/helper-observer.cpp
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "helper-observer.h"
+
+#include "object/sp-object.h"
+
+namespace Inkscape {
+namespace XML {
+
+// Very simple observer that just emits a signal if anything happens to a node
+SignalObserver::SignalObserver()
+ : _oldsel(nullptr)
+{}
+
+SignalObserver::~SignalObserver()
+{
+ set(nullptr); // if _oldsel!=nullptr, remove observer and decrease refcount
+}
+
+// Add this observer to the SPObject and remove it from any previous object
+void SignalObserver::set(SPObject* o)
+{
+ // XML Tree being used directly in this function in the following code
+ // while it shouldn't be
+ // Pointer to object is stored, so refcounting should be increased/decreased
+ if(_oldsel) {
+ if (_oldsel->getRepr()) {
+ _oldsel->getRepr()->removeObserver(*this);
+ }
+ sp_object_unref(_oldsel);
+ _oldsel = nullptr;
+ }
+ if(o) {
+ if (o->getRepr()) {
+ o->getRepr()->addObserver(*this);
+ sp_object_ref(o);
+ _oldsel = o;
+ }
+ }
+}
+
+void SignalObserver::notifyChildAdded(XML::Node&, XML::Node&, XML::Node*)
+{ signal_changed()(); }
+
+void SignalObserver::notifyChildRemoved(XML::Node&, XML::Node&, XML::Node*)
+{ signal_changed()(); }
+
+void SignalObserver::notifyChildOrderChanged(XML::Node&, XML::Node&, XML::Node*, XML::Node*)
+{ signal_changed()(); }
+
+void SignalObserver::notifyContentChanged(XML::Node&, Util::ptr_shared, Util::ptr_shared)
+{}
+
+void SignalObserver::notifyAttributeChanged(XML::Node&, GQuark, Util::ptr_shared, Util::ptr_shared)
+{ signal_changed()(); }
+
+void SignalObserver::notifyElementNameChanged(Node&, GQuark, GQuark)
+{
+ signal_changed()();
+}
+
+sigc::signal<void>& SignalObserver::signal_changed()
+{
+ return _signal_changed;
+}
+
+} //namespace XML
+} //namespace Inkscape
+
diff --git a/src/xml/helper-observer.h b/src/xml/helper-observer.h
new file mode 100644
index 0000000..4bf172b
--- /dev/null
+++ b/src/xml/helper-observer.h
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_XML_HELPER_OBSERVER
+#define SEEN_XML_HELPER_OBSERVER
+
+#include <cstddef>
+#include <sigc++/sigc++.h>
+
+#include "node-observer.h"
+#include "node.h"
+
+class SPObject;
+
+namespace Inkscape {
+namespace XML {
+
+class Node;
+
+// Very simple observer that just emits a signal if anything happens to a node
+class SignalObserver : public NodeObserver {
+public:
+ SignalObserver();
+ ~SignalObserver() override;
+
+ // Add this observer to the SPObject and remove it from any previous object
+ void set(SPObject* o);
+ void notifyChildAdded(Node&, Node&, Node*) override;
+ void notifyChildRemoved(Node&, Node&, Node*) override;
+ void notifyChildOrderChanged(Node&, Node&, Node*, Node*) override;
+ void notifyContentChanged(Node&, Util::ptr_shared, Util::ptr_shared) override;
+ void notifyAttributeChanged(Node&, GQuark, Util::ptr_shared, Util::ptr_shared) override;
+ void notifyElementNameChanged(Node&, GQuark, GQuark) override;
+ sigc::signal<void>& signal_changed();
+private:
+ sigc::signal<void> _signal_changed;
+ SPObject* _oldsel;
+};
+
+}
+}
+
+#endif //#ifndef __XML_HELPER_OBSERVER__
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
diff --git a/src/xml/invalid-operation-exception.h b/src/xml/invalid-operation-exception.h
new file mode 100644
index 0000000..e43529e
--- /dev/null
+++ b/src/xml/invalid-operation-exception.h
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Inkscape::XML::InvalidOperationException - invalid operation for node type
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2010 Authors
+ * Copyright 2004-2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_INVALID_OPERATION_EXCEPTION_H
+#define SEEN_INKSCAPE_XML_INVALID_OPERATION_EXCEPTION_H
+
+#include <exception>
+#include <stdexcept>
+
+namespace Inkscape {
+
+namespace XML {
+
+class InvalidOperationException : public std::logic_error {
+public:
+ InvalidOperationException(std::string const &message) :
+ std::logic_error(message)
+ { }
+};
+
+}
+
+}
+
+#endif
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/log-builder.cpp b/src/xml/log-builder.cpp
new file mode 100644
index 0000000..3fd831a
--- /dev/null
+++ b/src/xml/log-builder.cpp
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Object building an event log.
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright 2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "xml/log-builder.h"
+#include "xml/event.h"
+#include "xml/event-fns.h"
+
+namespace Inkscape {
+namespace XML {
+
+void LogBuilder::discard() {
+ sp_repr_free_log(_log);
+ _log = nullptr;
+}
+
+Event *LogBuilder::detach() {
+ Event *log=_log;
+ _log = nullptr;
+ return log;
+}
+
+void LogBuilder::addChild(Node &node, Node &child, Node *prev) {
+ _log = new Inkscape::XML::EventAdd(&node, &child, prev, _log);
+ _log = _log->optimizeOne();
+}
+
+void LogBuilder::removeChild(Node &node, Node &child, Node *prev) {
+ _log = new Inkscape::XML::EventDel(&node, &child, prev, _log);
+ _log = _log->optimizeOne();
+}
+
+void LogBuilder::setChildOrder(Node &node, Node &child,
+ Node *old_prev, Node *new_prev)
+{
+ _log = new Inkscape::XML::EventChgOrder(&node, &child, old_prev, new_prev, _log);
+ _log = _log->optimizeOne();
+}
+
+void LogBuilder::setContent(Node &node,
+ Util::ptr_shared old_content,
+ Util::ptr_shared new_content)
+{
+ _log = new Inkscape::XML::EventChgContent(&node, old_content, new_content, _log);
+ _log = _log->optimizeOne();
+}
+
+void LogBuilder::setAttribute(Node &node, GQuark name,
+ Util::ptr_shared old_value,
+ Util::ptr_shared new_value)
+{
+ _log = new Inkscape::XML::EventChgAttr(&node, name, old_value, new_value, _log);
+ _log = _log->optimizeOne();
+}
+
+void LogBuilder::setElementName(Node& node, GQuark old_name, GQuark new_name)
+{
+ _log = new Inkscape::XML::EventChgElementName(&node, old_name, new_name, _log);
+ _log = _log->optimizeOne();
+}
+
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/log-builder.h b/src/xml/log-builder.h
new file mode 100644
index 0000000..bd7628b
--- /dev/null
+++ b/src/xml/log-builder.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Object building an event log
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright 2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_LOG_BUILDER_H
+#define SEEN_INKSCAPE_XML_LOG_BUILDER_H
+
+#include "inkgc/gc-managed.h"
+#include "xml/node-observer.h"
+
+namespace Inkscape {
+namespace XML {
+
+class Event;
+class Node;
+
+/**
+ * @brief Event log builder
+ *
+ * This object records all events sent to it via the public methods in an internal event log.
+ * Calling detach() then returns the built log. Calling discard() will clear all the events
+ * recorded so far.
+ */
+class LogBuilder {
+public:
+ LogBuilder() : _log(nullptr) {}
+ ~LogBuilder() { discard(); }
+
+ /** @name Manipulate the recorded event log
+ * @{ */
+ /**
+ * @brief Clear the internal log
+ */
+ void discard();
+ /**
+ * @brief Get the internal event log
+ * @return The recorded event chain
+ */
+ Event *detach();
+ /*@}*/
+
+ /** @name Record events in the log
+ * @{ */
+ void addChild(Node &node, Node &child, Node *prev);
+
+ void removeChild(Node &node, Node &child, Node *prev);
+
+ void setChildOrder(Node &node, Node &child,
+ Node *old_prev, Node *new_prev);
+
+ void setContent(Node &node,
+ Util::ptr_shared old_content,
+ Util::ptr_shared new_content);
+
+ void setAttribute(Node &node, GQuark name,
+ Util::ptr_shared old_value,
+ Util::ptr_shared new_value);
+
+ void setElementName(Node& node, GQuark old_name, GQuark new_name);
+ /*@}*/
+
+private:
+ Event *_log;
+};
+
+}
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/node-event-vector.h b/src/xml/node-event-vector.h
new file mode 100644
index 0000000..263751e
--- /dev/null
+++ b/src/xml/node-event-vector.h
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Deprecated structure for a set of callbacks for node state changes
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Frank Felfe <innerspace@iname.com>
+ *
+ * Copyright (C) 1999-2002 Lauris Kaplinski and Frank Felfe
+ * Copyright (C) 2000-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_SP_REPR_EVENT_VECTOR
+#define SEEN_INKSCAPE_XML_SP_REPR_EVENT_VECTOR
+
+#include "xml/node.h"
+
+namespace Inkscape {
+namespace XML {
+struct NodeEventVector;
+}
+}
+
+/**
+ * @brief Generate events corresponding to the node's state
+ * @deprecated Use Node::synthesizeEvents(NodeObserver &) instead
+ */
+inline void sp_repr_synthesize_events (Inkscape::XML::Node *repr, const Inkscape::XML::NodeEventVector *vector, void* data) {
+ repr->synthesizeEvents(vector, data);
+}
+/**
+ * @brief Add a set of callbacks for node state changes and its associated data
+ * @deprecated Use Node::addObserver() instead
+ */
+inline void sp_repr_add_listener (Inkscape::XML::Node *repr, const Inkscape::XML::NodeEventVector *vector, void* data) {
+ repr->addListener(vector, data);
+}
+/**
+ * @brief Remove a set of callbacks based on associated data
+ * @deprecated Use Node::removeObserver() instead
+ */
+inline void sp_repr_remove_listener_by_data (Inkscape::XML::Node *repr, void* data) {
+ repr->removeListenerByData(data);
+}
+
+namespace Inkscape {
+namespace XML {
+
+/**
+ * @brief Structure holding callbacks for node state changes
+ * @deprecated Derive an observer object from the NodeObserver class instead
+ */
+struct NodeEventVector {
+ /* Immediate signals */
+ void (* child_added) (Node *repr, Node *child, Node *ref, void* data);
+ void (* child_removed) (Node *repr, Node *child, Node *ref, void* data);
+ void (* attr_changed) (Node *repr, char const *key, char const *oldval, char const *newval, bool is_interactive, void* data);
+ void (* content_changed) (Node *repr, char const *oldcontent, char const *newcontent, void * data);
+ void (* order_changed) (Node *repr, Node *child, Node *oldref, Node *newref, void* data);
+ void (* element_name_changed) (Node* repr, char const* oldname, char const* newname, void* data);
+};
+
+}
+}
+
+#endif
diff --git a/src/xml/node-fns.cpp b/src/xml/node-fns.cpp
new file mode 100644
index 0000000..c996cfd
--- /dev/null
+++ b/src/xml/node-fns.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifdef HAVE_CONFIG_H
+#endif
+
+#include <map>
+#include <cstring>
+#include <string>
+#include <glib.h> // g_assert()
+
+#include "xml/node-iterators.h"
+#include "util/find-if-before.h"
+#include "node-fns.h"
+
+namespace Inkscape {
+namespace XML {
+
+/* the id_permitted stuff is a temporary-ish hack */
+
+namespace {
+
+bool id_permitted_internal(GQuark qname) {
+ char const *qname_s=g_quark_to_string(qname);
+ return !strncmp("svg:", qname_s, 4) || !strncmp("sodipodi:", qname_s, 9) ||
+ !strncmp("inkscape:", qname_s, 9);
+}
+
+
+bool id_permitted_internal_memoized(GQuark qname) {
+ typedef std::map<GQuark, bool> IdPermittedMap;
+ static IdPermittedMap id_permitted_names;
+
+ IdPermittedMap::iterator found;
+ found = id_permitted_names.find(qname);
+ if ( found != id_permitted_names.end() ) {
+ return found->second;
+ } else {
+ bool permitted=id_permitted_internal(qname);
+ id_permitted_names[qname] = permitted;
+ return permitted;
+ }
+}
+
+}
+
+bool id_permitted(Node const *node) {
+ g_return_val_if_fail(node != nullptr, false);
+
+ if ( node->type() != ELEMENT_NODE ) {
+ return false;
+ }
+
+ return id_permitted_internal_memoized((GQuark)node->code());
+}
+
+struct node_matches {
+ node_matches(Node const &n) : node(n) {}
+ bool operator()(Node const &other) { return &other == &node; }
+ Node const &node;
+};
+
+// documentation moved to header
+Node *previous_node(Node *node) {
+ return node->prev();
+ using Inkscape::Algorithms::find_if_before;
+
+ if ( !node || !node->parent() ) {
+ return nullptr;
+ }
+
+ Node *previous=find_if_before<NodeSiblingIterator>(
+ node->parent()->firstChild(), nullptr, node_matches(*node)
+ );
+
+ g_assert(previous == nullptr
+ ? node->parent()->firstChild() == node
+ : previous->next() == node);
+
+ return previous;
+}
+
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/node-fns.h b/src/xml/node-fns.h
new file mode 100644
index 0000000..82724e2
--- /dev/null
+++ b/src/xml/node-fns.h
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Helper functions for XML nodes
+ *//*
+ * Authors:
+ * see git history
+ * Unknown author
+ * Krzysztof Kosiński <tweenk.pl@gmail.com> (documentation)
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_XML_NODE_FNS_H
+#define SEEN_XML_NODE_FNS_H
+
+#include "xml/node.h"
+
+namespace Inkscape {
+namespace XML {
+
+bool id_permitted(Node const *node);
+
+//@{
+/**
+ * @brief Get the next node in sibling order
+ * @param node The origin node
+ * @return The next node in sibling order
+ * @relates Inkscape::XML::Node
+ */
+inline Node *next_node(Node *node) {
+ return ( node ? node->next() : nullptr );
+}
+inline Node const *next_node(Node const *node) {
+ return ( node ? node->next() : nullptr );
+}
+//@}
+
+//@{
+/**
+ * @brief Get the previous node in sibling order
+ *
+ * This method, unlike Node::next(), is a linear search over the children of @c node's parent.
+ * The return value is NULL when the node has no parent or is first in the sibling order.
+ *
+ * @param node The origin node
+ * @return The previous node in sibling order, or NULL
+ * @relates Inkscape::XML::Node
+ */
+Node *previous_node(Node *node);
+inline Node const *previous_node(Node const *node) {
+ return previous_node(const_cast<Node *>(node));
+}
+//@}
+
+//@{
+/**
+ * @brief Get the node's parent
+ * @param node The origin node
+ * @return The node's parent
+ * @relates Inkscape::XML::Node
+ */
+inline Node *parent_node(Node *node) {
+ return ( node ? node->parent() : nullptr );
+}
+inline Node const *parent_node(Node const *node) {
+ return ( node ? node->parent() : nullptr );
+}
+//@}
+
+}
+}
+
+#endif /* !SEEN_XML_NODE_FNS_H */
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/node-iterators.h b/src/xml/node-iterators.h
new file mode 100644
index 0000000..11c9450
--- /dev/null
+++ b/src/xml/node-iterators.h
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Node iterators
+ *
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright (C) 2004 MenTaLguY
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_SP_REPR_ITERATORS_H
+#define SEEN_INKSCAPE_XML_SP_REPR_ITERATORS_H
+
+#include "util/forward-pointer-iterator.h"
+#include "xml/node.h"
+
+namespace Inkscape {
+namespace XML {
+
+struct NodeSiblingIteratorStrategy {
+ static Node const *next(Node const *node) {
+ return ( node ? node->next() : nullptr );
+ }
+};
+
+struct NodeParentIteratorStrategy {
+ static Node const *next(Node const *node) {
+ return ( node ? node->parent() : nullptr );
+ }
+};
+
+typedef Inkscape::Util::ForwardPointerIterator<Node,
+ NodeSiblingIteratorStrategy>
+ NodeSiblingIterator;
+
+typedef Inkscape::Util::ForwardPointerIterator<Node const,
+ NodeSiblingIteratorStrategy>
+ NodeConstSiblingIterator;
+
+typedef Inkscape::Util::ForwardPointerIterator<Node,
+ NodeParentIteratorStrategy>
+ NodeParentIterator;
+
+typedef Inkscape::Util::ForwardPointerIterator<Node const,
+ NodeParentIteratorStrategy>
+ NodeConstParentIterator;
+
+}
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/node-observer.h b/src/xml/node-observer.h
new file mode 100644
index 0000000..e3173a8
--- /dev/null
+++ b/src/xml/node-observer.h
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Interface for XML node observers
+ *//*
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Krzysztof Kosiński <tweenk.pl@gmail.com> (documentation)
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+/** @file
+ */
+
+#ifndef SEEN_INKSCAPE_XML_NODE_OBSERVER_H
+#define SEEN_INKSCAPE_XML_NODE_OBSERVER_H
+
+#include "util/share.h"
+typedef unsigned int GQuark;
+
+#ifndef INK_UNUSED
+#define INK_UNUSED(x) ((void)(x))
+#endif // INK_UNUSED
+
+namespace Inkscape {
+namespace XML {
+
+class Node;
+
+/**
+ * @brief Interface for XML node observers
+ *
+ * This class defines an interface for objects that can receive
+ * XML node state change notifications. The observer has to be registered using
+ * the Node::addObserver() method to be notified of changes of this node only,
+ * or using Node::addSubtreeObserver() to also receive notifications about its
+ * descendants. All observer methods are called when the operations in question have
+ * been completed, just before returning from the modifying methods.
+ *
+ * Be careful when e.g. changing an attribute of @c node in notifyAttributeChanged().
+ * The method will be called again due to the XML modification performed in it. If you
+ * don't take special precautions to ignore the second call, it will result in infinite
+ * recursion.
+ *
+ * The virtual methods of this class do nothing by default, so you don't need to provide
+ * stubs for things you don't use. A good idea is to make the observer register itself
+ * on construction and unregister itself on destruction. This will ensure there are
+ * no dangling references.
+ */
+class NodeObserver {
+protected:
+ /* the constructor is protected to prevent instantiation */
+ NodeObserver() = default;
+public:
+ virtual ~NodeObserver() = default;
+
+ // FIXME: somebody needs to learn what "pure virtual" means
+
+ /**
+ * @brief Child addition callback
+ *
+ * This method is called whenever a child is added to the observed node. The @c prev
+ * parameter is NULL when the newly added child is first in the sibling order.
+ *
+ * @param node The changed XML node
+ * @param child The newly added child node
+ * @param prev The node after which the new child was inserted into the sibling order, or NULL
+ */
+ virtual void notifyChildAdded(Node &node, Node &child, Node *prev) {
+ INK_UNUSED(node);
+ INK_UNUSED(child);
+ INK_UNUSED(prev);
+ }
+
+ /**
+ * @brief Child removal callback
+ *
+ * This method is called whenever a child is removed from the observed node. The @c prev
+ * parameter is NULL when the removed child was first in the sibling order.
+ *
+ * @param node The changed XML node
+ * @param child The removed child node
+ * @param prev The node that was before the removed node in sibling order, or NULL
+ */
+ virtual void notifyChildRemoved(Node &node, Node &child, Node *prev) {
+ INK_UNUSED(node);
+ INK_UNUSED(child);
+ INK_UNUSED(prev);
+ }
+
+ /**
+ * @brief Child order change callback
+ *
+ * This method is called whenever the order of a node's children is changed using
+ * Node::changeOrder(). The @c old_prev parameter is NULL if the relocated node
+ * was first in the sibling order before the order change, and @c new_prev is NULL
+ * if it was moved to the first position by this operation.
+ *
+ * @param node The changed XML node
+ * @param child The child node that was relocated in the sibling order
+ * @param old_prev The node that was before @c child prior to the order change
+ * @param new_prev The node that is before @c child after the order change
+ */
+ virtual void notifyChildOrderChanged(Node &node, Node &child,
+ Node *old_prev, Node *new_prev) {
+ INK_UNUSED(node);
+ INK_UNUSED(child);
+ INK_UNUSED(old_prev);
+ INK_UNUSED(new_prev);
+ }
+
+ /**
+ * @brief Content change callback
+ *
+ * This method is called whenever a node's content is changed using Node::setContent(),
+ * e.g. for text or comment nodes.
+ *
+ * @param node The changed XML node
+ * @param old_content Old content of @c node
+ * @param new_content New content of @c node
+ */
+ virtual void notifyContentChanged(Node &node,
+ Util::ptr_shared old_content,
+ Util::ptr_shared new_content) {
+ INK_UNUSED(node);
+ INK_UNUSED(old_content);
+ INK_UNUSED(new_content);
+ }
+
+ /**
+ * @brief Attribute change callback
+ *
+ * This method is called whenever one of a node's attributes is changed.
+ *
+ * @param node The changed XML node
+ * @param name GQuark corresponding to the attribute's name
+ * @param old_value Old value of the modified attribute
+ * @param new_value New value of the modified attribute
+ */
+ virtual void notifyAttributeChanged(Node &node, GQuark name,
+ Util::ptr_shared old_value,
+ Util::ptr_shared new_value) {
+ INK_UNUSED(node);
+ INK_UNUSED(name);
+ INK_UNUSED(old_value);
+ INK_UNUSED(new_value);
+ }
+
+ /**
+ * @brief Element name change callback.
+ *
+ * This method is called whenever an element node's name is changed.
+ *
+ * @param node The changed XML node.
+ * @param old_name GQuark corresponding to the old element name.
+ * @param new_name GQuark corresponding to the new element name.
+ */
+ virtual void notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) {
+ INK_UNUSED(node);
+ INK_UNUSED(old_name);
+ INK_UNUSED(new_name);
+ }
+
+};
+
+} // namespace XML
+} // namespace Inkscape
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/node.h b/src/xml/node.h
new file mode 100644
index 0000000..9891a67
--- /dev/null
+++ b/src/xml/node.h
@@ -0,0 +1,543 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Interface for XML nodes
+ *
+ * Authors:
+ * MenTaLguY <mental@rydia.net>
+ * Krzysztof Kosiński <tweenk.pl@gmail.com> (documentation)
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_NODE_H
+#define SEEN_INKSCAPE_XML_NODE_H
+
+#include <glibmm/ustring.h>
+#include "gc-anchored.h"
+#include "util/list.h"
+#include "util/const_char_ptr.h"
+
+namespace Inkscape {
+namespace XML {
+
+struct AttributeRecord;
+struct Document;
+class Event;
+class NodeObserver;
+struct NodeEventVector;
+
+/**
+ * @brief Enumeration containing all supported node types.
+ */
+enum NodeType {
+ DOCUMENT_NODE, ///< Top-level document node. Do not confuse with the root node.
+ ELEMENT_NODE, ///< Regular element node, e.g. &lt;group /&gt;.
+ TEXT_NODE, ///< Text node, e.g. "Some text" in &lt;group&gt;Some text&lt;/group&gt; is represented by a text node.
+ COMMENT_NODE, ///< Comment node, e.g. &lt;!-- some comment --&gt;
+ PI_NODE ///< Processing instruction node, e.g. &lt;?xml version="1.0" encoding="utf-8" standalone="no"?&gt;
+};
+
+// careful; GC::Anchored should only appear once in the inheritance
+// hierarchy; otherwise there will be leaks
+
+/**
+ * @brief Interface for refcounted XML nodes
+ *
+ * This class is an abstract base type for all nodes in an XML document - this includes
+ * everything except attributes. An XML document is also a node itself. This is the main
+ * class used for interfacing with Inkscape's documents. Everything that has to be stored
+ * in the SVG has to go through this class at some point.
+ *
+ * Each node unconditionally has to belong to a document. There no "documentless" nodes,
+ * and it's not possible to move nodes between documents - they have to be duplicated.
+ * Each node can only refer to the nodes in the same document. Name of the node is immutable,
+ * it cannot be changed after its creation. Same goes for the type of the node. To simplify
+ * the use of this class, you can perform all operations on all nodes, but only some of them
+ * make any sense. For example, only element nodes can have attributes, only element and
+ * document nodes can have children, and all nodes except element and document nodes can
+ * have content. Although you can set content for element nodes, it won't make any difference
+ * in the XML output.
+ *
+ * To create new nodes, use the methods of the Inkscape::XML::Document class. You can obtain
+ * the nodes' document using the document() method. To destroy a node, just unparent it
+ * by calling sp_repr_unparent() or node->parent->removeChild() and release any references
+ * to it. The garbage collector will reclaim the memory in the next pass. There are additional
+ * convenience functions defined in @ref xml/repr.h
+ *
+ * In addition to regular DOM manipulations, you can register observer objects that will
+ * receive notifications about changes made to the node. See the NodeObserver class.
+ *
+ * @see Inkscape::XML::Document
+ * @see Inkscape::XML::NodeObserver
+ */
+class Node : public Inkscape::GC::Anchored {
+public:
+ Node() = default;
+ ~Node() override = default;
+
+ /**
+ * @name Retrieve information about the node
+ * @{
+ */
+
+ /**
+ * @brief Get the type of the node
+ * @return NodeType enumeration member corresponding to the type of the node.
+ */
+ virtual NodeType type() const=0;
+
+ /**
+ * @brief Get the name of the element node
+ *
+ * This method only makes sense for element nodes. Names are stored as
+ * GQuarks to accelerate conversions.
+ *
+ * @return Name for element nodes, NULL for others
+ */
+ virtual char const *name() const=0;
+ /**
+ * @brief Get the integer code corresponding to the node's name
+ * @return GQuark code corresponding to the name
+ */
+ virtual int code() const=0;
+
+ /**
+ * @brief Get the index of this node in parent's child order
+ *
+ * If this method is used on a node that doesn't have a parent, the method will return 0,
+ * and a warning will be printed on the console.
+ *
+ * @return The node's index, or 0 if the node does not have a parent
+ */
+ virtual unsigned position() const=0;
+
+ /**
+ * @brief Get the number of children of this node
+ * @return The number of children
+ */
+ virtual unsigned childCount() const=0;
+
+ /**
+ * @brief Get the content of a text or comment node
+ *
+ * This method makes no sense for element nodes. To retrieve the element node's name,
+ * use the name() method.
+ *
+ * @return The node's content
+ */
+ virtual char const *content() const=0;
+
+ /**
+ * @brief Get the string representation of a node's attribute
+ *
+ * If there is no attribute with the given name, the method will return NULL.
+ * All strings returned by this method are owned by the node and may not be freed.
+ * The returned pointer will become invalid when the attribute changes. If you need
+ * to store the return value, use g_strdup(). To parse the string, use methods
+ * in repr.h
+ *
+ * @param key The name of the node's attribute
+ */
+ virtual char const *attribute(char const *key) const=0;
+
+ /**
+ * @brief Get a list of the node's attributes
+ *
+ * The returned list is a functional programming style list rather than a standard one.
+ *
+ * @return A list of AttributeRecord structures describing the attributes
+ * @todo This method should return std::map<Glib::Quark const, gchar const *>
+ * or something similar with a custom allocator
+ */
+ virtual Inkscape::Util::List<AttributeRecord const> attributeList() const=0;
+
+ /**
+ * @brief Check whether this node has any attribute that matches a string
+ *
+ * This method checks whether this node has any attributes whose names
+ * have @c partial_name as their substrings. The check is done using
+ * the strstr() function of the C library. I don't know what would require that
+ * functionality, because matchAttributeName("id") matches both "identity" and "hidden".
+ *
+ * @param partial_name The string to match against all attributes
+ * @return true if there is such an attribute, false otherwise
+ */
+ virtual bool matchAttributeName(char const *partial_name) const=0;
+
+ /*@}*/
+
+ /**
+ * @name Modify the node
+ * @{
+ */
+
+ /**
+ * @brief Set the position of this node in parent's child order
+ *
+ * To move the node to the end of the parent's child order, pass a negative argument.
+ *
+ * @param pos The new position in parent's child order
+ */
+ virtual void setPosition(int pos)=0;
+
+ /**
+ * @brief Set the content of a text or comment node
+ *
+ * This method doesn't make sense for element nodes.
+ *
+ * @param value The node's new content
+ */
+ virtual void setContent(char const *value)=0;
+
+ //@{
+ /**
+ * @brief Change an attribute of this node
+ *
+ * The strings passed to this method are copied, so you can free them after use.
+ *
+ * @param key Name of the attribute to change
+ * @param value The new value of the attribute
+ * @param is_interactive Ignored
+ */
+
+ void setAttribute(Inkscape::Util::const_char_ptr key,
+ Inkscape::Util::const_char_ptr value,
+ bool is_interactive=false) {
+ this->setAttributeImpl(key.data(), value.data(), is_interactive);
+ }
+
+ /**
+ * @brief Change an attribute of this node. Empty string deletes the attribute.
+ *
+ * @param key Name of the attribute to change
+ * @param value The new value of the attribute
+ *
+ */
+ void setAttributeOrRemoveIfEmpty(Inkscape::Util::const_char_ptr key,
+ Inkscape::Util::const_char_ptr value) {
+ this->setAttributeImpl(key.data(),
+ (value.data() == nullptr || value.data()[0]=='\0') ? nullptr : value.data(), false);
+ }
+
+
+ /**
+ * @brief Remove an attribute of this node
+ *
+ * @param key Name of the attribute to delete
+ *
+ */
+ void removeAttribute(Inkscape::Util::const_char_ptr key) {
+ this->setAttributeImpl(key.data(), nullptr, false);
+ }
+
+ //@}
+ /**
+ * @brief Set the integer GQuark code for the name of the node.
+ *
+ * Do not use this function unless you really have a good reason.
+ *
+ * @param code The integer value corresponding to the string to be set as
+ * the name of this node
+ */
+ virtual void setCodeUnsafe(int code) = 0;
+
+ /*@}*/
+
+
+ /**
+ * @name Traverse the XML tree
+ * @{
+ */
+
+ //@{
+ /**
+ * @brief Get the node's associated document
+ * @return The document to which the node belongs. Never NULL.
+ */
+ virtual Document *document()=0;
+ virtual Document const *document() const=0;
+ //@}
+
+ //@{
+ /**
+ * @brief Get the root node of this node's document
+ *
+ * This method works on any node that is part of an XML document, and returns
+ * the root node of the document in which it resides. For detached node hierarchies
+ * (i.e. nodes that are not descendants of a document node) this method
+ * returns the highest-level element node. For detached non-element nodes this method
+ * returns NULL.
+ *
+ * @return A pointer to the root element node, or NULL if the node is detached
+ */
+ virtual Node *root()=0;
+ virtual Node const *root() const=0;
+ //@}
+
+ //@{
+ /**
+ * @brief Get the parent of this node
+ *
+ * This method will return NULL for detached nodes.
+ *
+ * @return Pointer to the parent, or NULL
+ */
+ virtual Node *parent()=0;
+ virtual Node const *parent() const=0;
+ //@}
+
+ //@{
+ /**
+ * @brief Get the next sibling of this node
+ *
+ * This method will return NULL if the node is the last sibling element of the parent.
+ * The nodes form a singly-linked list, so there is no "prev()" method. Use the provided
+ * external function for that.
+ *
+ * @return Pointer to the next sibling, or NULL
+ * @see Inkscape::XML::previous_node()
+ */
+ virtual Node *next()=0;
+ virtual Node const *next() const=0;
+ virtual Node *prev()=0;
+ virtual Node const *prev() const=0;
+ //@}
+
+ //@{
+ /**
+ * @brief Get the first child of this node
+ *
+ * For nodes without any children, this method returns NULL.
+ *
+ * @return Pointer to the first child, or NULL
+ */
+ virtual Node *firstChild()=0;
+ virtual Node const *firstChild() const=0;
+ //@}
+
+ //@{
+ /**
+ * @brief Get the last child of this node
+ *
+ * For nodes without any children, this method returns NULL.
+ *
+ * @return Pointer to the last child, or NULL
+ */
+ virtual Node *lastChild()=0;
+ virtual Node const *lastChild() const=0;
+ //@}
+
+ //@{
+ /**
+ * @brief Get the child of this node with a given index
+ *
+ * If there is no child with the specified index number, this method will return NULL.
+ *
+ * @param index The zero-based index of the child to retrieve
+ * @return Pointer to the appropriate child, or NULL
+ */
+ virtual Node *nthChild(unsigned index)=0;
+ virtual Node const *nthChild(unsigned index) const=0;
+ //@}
+
+ /*@}*/
+
+ /**
+ * @name Manipulate the XML tree
+ * @{
+ */
+
+ /**
+ * @brief Create a duplicate of this node
+ *
+ * The newly created node has no parent, and a refcount equal 1.
+ * You need to manually insert it into the document, using e.g. appendChild().
+ * Afterwards, call Inkscape::GC::release on it, so that it will be
+ * automatically collected when the parent is collected.
+ *
+ * @param doc The document in which the duplicate should be created
+ * @return A pointer to the duplicated node
+ */
+ virtual Node *duplicate(Document *doc) const=0;
+
+ /**
+ * @brief Insert another node as a child of this node
+ *
+ * When @c after is NULL, the inserted node will be placed as the first child
+ * of this node. @c after must be a child of this node.
+ *
+ * @param child The node to insert
+ * @param after The node after which the inserted node should be placed, or NULL
+ */
+ virtual void addChild(Node *child, Node *after)=0;
+
+ /**
+ * @brief Insert another node as a child of this node
+ *
+ * This is more efficient than appendChild() + setPosition().
+ *
+ * @param child The node to insert
+ * @param pos The position in parent's child order
+ */
+ void addChildAtPos(Node *child, unsigned pos)
+ {
+ Node *after = (pos == 0) ? nullptr : nthChild(pos - 1);
+ addChild(child, after);
+ }
+
+ /**
+ * @brief Append a node as the last child of this node
+ * @param child The node to append
+ */
+ virtual void appendChild(Node *child)=0;
+
+ /**
+ * @brief Remove a child of this node
+ *
+ * Once the pointer to the removed node disappears from the stack, the removed node
+ * will be collected in the next GC pass, but only as long as its refcount is zero.
+ * You should keep a refcount of zero for all nodes in the document except for
+ * the document node itself, because they will be held in memory by the parent.
+ *
+ * @param child The child to remove
+ */
+ virtual void removeChild(Node *child)=0;
+
+ /**
+ * @brief Move a given node in this node's child order
+ *
+ * Both @c child and @c after must be children of this node for the method to work.
+ *
+ * @param child The node to move in the order
+ * @param after The sibling node after which the moved node should be placed
+ */
+ virtual void changeOrder(Node *child, Node *after)=0;
+
+ /**
+ * @brief Remove all elements that not in src node
+ * @param src The node to check for elements into this node
+ * @param key The attribute to use as the identity attribute
+ */
+ virtual void cleanOriginal(Node *src, gchar const *key)=0;
+
+
+ /**
+ * @brief Compare 2 nodes equality
+ * @param other The other node to compare
+ * @param recursive Recursive mode check
+ */
+ virtual bool equal(Node const *other, bool recursive)=0;
+ /**
+ * @brief Merge all children of another node with the current
+ *
+ * This method merges two node hierarchies, where @c src takes precedence.
+ * @c key is the name of the attribute that determines whether two nodes are
+ * corresponding (it must be the same for both, and all of their ancestors). If there is
+ * a corresponding node in @c src hierarchy, their attributes and content override the ones
+ * already present in this node's hierarchy. If there is no corresponding node,
+ * it is copied from @c src to this node. This method is used when merging the user's
+ * preferences file with the defaults, and has little use beyond that.
+ *
+ * @param src The node to merge into this node
+ * @param key The attribute to use as the identity attribute
+ * @param noid If true process noid items
+ * @param key If clean callback to cleanOriginal
+ */
+
+ virtual void mergeFrom(Node const *src, char const *key, bool extension = false, bool clean = false)=0;
+
+ /*@}*/
+
+
+ /**
+ * @name Notify observers about operations on the node
+ * @{
+ */
+
+ /**
+ * @brief Add an object that will be notified of the changes to this node
+ *
+ * @c observer must be an object deriving from the NodeObserver class.
+ * The virtual methods of this object will be called when a corresponding change
+ * happens to this node. You can also notify the observer of the node's current state
+ * using synthesizeEvents(NodeObserver &).
+ *
+ * @param observer The observer object
+ */
+ virtual void addObserver(NodeObserver &observer)=0;
+ /**
+ * @brief Remove an object from the list of observers
+ * @param observer The object to be removed
+ */
+ virtual void removeObserver(NodeObserver &observer)=0;
+ /**
+ * @brief Generate a sequence of events corresponding to the state of this node
+ *
+ * This function notifies the specified observer of all the events that would
+ * recreate the current state of this node; e.g. the observer is notified of
+ * all the attributes, children and content like they were just created.
+ * This function can greatly simplify observer logic.
+ *
+ * @param observer The node observer to notify of the events
+ */
+ virtual void synthesizeEvents(NodeObserver &observer)=0;
+
+ /**
+ * @brief Add an object that will be notified of the changes to this node and its descendants
+ *
+ * The difference between adding a regular observer and a subtree observer is that
+ * the subtree observer will also be notified if a change occurs to any of the node's
+ * descendants, while a regular observer will only be notified of changes to the node
+ * it was assigned to.
+ *
+ * @param observer The observer object
+ */
+ virtual void addSubtreeObserver(NodeObserver &observer)=0;
+
+ /**
+ * @brief Remove an object from the subtree observers list
+ * @param observer The object to be removed
+ */
+ virtual void removeSubtreeObserver(NodeObserver &observer)=0;
+
+ /**
+ * @brief Add a set node change callbacks with an associated data
+ * @deprecated Use addObserver(NodeObserver &) instead
+ */
+ virtual void addListener(NodeEventVector const *vector, void *data)=0;
+ /**
+ * @brief Remove a set of node change callbacks by their associated data
+ * @deprecated Use removeObserver(NodeObserver &) instead
+ */
+ virtual void removeListenerByData(void *data)=0;
+ /**
+ * @brief Generate a sequence of events corresponding to the state of this node
+ * @deprecated Use synthesizeEvents(NodeObserver &) instead
+ */
+ virtual void synthesizeEvents(NodeEventVector const *vector, void *data)=0;
+
+ virtual void recursivePrintTree(unsigned level)=0;
+
+ /*@}*/
+
+protected:
+ Node(Node const &) : Anchored() {}
+
+ virtual void setAttributeImpl(char const *key, char const *value, bool is_interactive)=0;
+};
+
+}
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/pi-node.h b/src/xml/pi-node.h
new file mode 100644
index 0000000..2ec7942
--- /dev/null
+++ b/src/xml/pi-node.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Processing instruction node implementation
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright 2004-2005 MenTaLguY <mental@rydia.net>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_PI_NODE_H
+#define SEEN_INKSCAPE_XML_PI_NODE_H
+
+#include "xml/simple-node.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+/**
+ * @brief Processing instruction node, e.g. &lt;?xml version="1.0" encoding="utf-8" standalone="no"?&gt;
+ */
+struct PINode : public SimpleNode {
+ PINode(GQuark target, Util::ptr_shared content, Document *doc)
+ : SimpleNode(target, doc)
+ {
+ setContent(content);
+ }
+ PINode(PINode const &other, Document *doc)
+ : SimpleNode(other, doc) {}
+
+ Inkscape::XML::NodeType type() const override { return Inkscape::XML::PI_NODE; }
+
+protected:
+ SimpleNode *_duplicate(Document* doc) const override { return new PINode(*this, doc); }
+};
+
+}
+
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/quote-test.h b/src/xml/quote-test.h
new file mode 100644
index 0000000..7e08b8d
--- /dev/null
+++ b/src/xml/quote-test.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ * Initial author: Peter Moulder.
+ *
+ * Copyright (C) 2013 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <cxxtest/TestSuite.h>
+#include "streq.h"
+
+
+#include <cstring>
+#include <functional>
+
+#include "quote.h"
+
+class XmlQuoteTest : public CxxTest::TestSuite
+{
+public:
+
+ XmlQuoteTest()
+ {
+ }
+ virtual ~XmlQuoteTest() {}
+
+// createSuite and destroySuite get us per-suite setup and teardown
+// without us having to worry about static initialization order, etc.
+ static XmlQuoteTest *createSuite() { return new XmlQuoteTest(); }
+ static void destroySuite( XmlQuoteTest *suite ) { delete suite; }
+
+ void testXmlQuotedStrlen()
+ {
+ struct {
+ char const *s;
+ size_t len;
+ } cases[] = {
+ {"", 0},
+ {"x", 1},
+ {"Foo", 3},
+ {"\"", 6},
+ {"&", 5},
+ {"<", 4},
+ {">", 4},
+ {"a\"b", 8},
+ {"a\"b<c>d;!@#$%^*(\\)?", 30}
+ };
+ for(size_t i=0; i<G_N_ELEMENTS(cases); i++) {
+ TS_ASSERT_EQUALS( xml_quoted_strlen(cases[i].s) , cases[i].len );
+ }
+ }
+
+ void testXmlQuoteStrdup()
+ {
+ struct {
+ char const * s1;
+ char const * s2;
+ } cases[] = {
+ {"", ""},
+ {"x", "x"},
+ {"Foo", "Foo"},
+ {"\"", "&quot;"},
+ {"&", "&amp;"},
+ {"<", "&lt;"},
+ {">", "&gt;"},
+ {"a\"b<c>d;!@#$%^*(\\)?", "a&quot;b&lt;c&gt;d;!@#$%^*(\\)?"}
+ };
+ for(size_t i=0; i<G_N_ELEMENTS(cases); i++) {
+ char* str = xml_quote_strdup(cases[i].s1);
+ TS_ASSERT_RELATION( streq_rel, cases[i].s2, str );
+ g_free(str);
+ }
+ }
+};
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/quote.cpp b/src/xml/quote.cpp
new file mode 100644
index 0000000..148cb9d
--- /dev/null
+++ b/src/xml/quote.cpp
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief XML quoting routines
+ *//*
+ * Authors:
+ * see git history
+ * Krzysztof Kosiński <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2015 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "xml/quote.h"
+#include <cstring>
+#include <glib.h>
+
+/// Returns the length of the string after quoting the characters <code>"&amp;&lt;&gt;</code>.
+size_t xml_quoted_strlen(char const *val)
+{
+ if (!val) return 0;
+ size_t len = 0;
+
+ for (char const *valp = val; *valp; ++valp) {
+ switch (*valp) {
+ case '"':
+ len += 6; // &quot;
+ break;
+ case '&':
+ len += 5; // &amp;
+ break;
+ case '<':
+ case '>':
+ len += 4; // &lt; or &gt;
+ break;
+ default:
+ ++len;
+ break;
+ }
+ }
+ return len;
+}
+
+char *xml_quote_strdup(char const *src)
+{
+ size_t len = xml_quoted_strlen(src);
+ char *result = static_cast<char*>(g_malloc(len + 1));
+ char *resp = result;
+
+ for (char const *srcp = src; *srcp; ++srcp) {
+ switch(*srcp) {
+ case '"':
+ strcpy(resp, "&quot;");
+ resp += 6;
+ break;
+ case '&':
+ strcpy(resp, "&amp;");
+ resp += 5;
+ break;
+ case '<':
+ strcpy(resp, "&lt;");
+ resp += 4;
+ break;
+ case '>':
+ strcpy(resp, "&gt;");
+ resp += 4;
+ break;
+ default:
+ *resp++ = *srcp;
+ break;
+ }
+ }
+ *resp = 0;
+ return result;
+}
+
+// quote: ", &, <, >
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/quote.h b/src/xml/quote.h
new file mode 100644
index 0000000..a2e5950
--- /dev/null
+++ b/src/xml/quote.h
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef SEEN_XML_QUOTE_H
+#define SEEN_XML_QUOTE_H
+
+#include <cstddef>
+
+size_t xml_quoted_strlen(char const *val);
+char *xml_quote_strdup(char const *src);
+
+
+#endif /* !SEEN_XML_QUOTE_H */
diff --git a/src/xml/rebase-hrefs.cpp b/src/xml/rebase-hrefs.cpp
new file mode 100644
index 0000000..b6f7e55
--- /dev/null
+++ b/src/xml/rebase-hrefs.cpp
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glibmm/convert.h>
+#include <glibmm/miscutils.h>
+#include <glibmm/uriutils.h>
+
+#include "../document.h" /* Unfortunately there's a separate xml/document.h. */
+#include "streq.h"
+
+#include "io/dir-util.h"
+#include "io/sys.h"
+
+#include "object/sp-object.h"
+#include "object/uri.h"
+
+#include "xml/node.h"
+#include "xml/rebase-hrefs.h"
+
+using Inkscape::XML::AttributeRecord;
+
+/**
+ * Determine if a href needs rebasing.
+ */
+static bool href_needs_rebasing(std::string const &href)
+{
+ bool ret = true;
+
+ if ( href.empty() || (href[0] == '#') ) {
+ ret = false;
+ /* False (no change) is the right behaviour even when the base URI differs from the
+ * document URI: RFC 3986 defines empty string relative URL as referring to the containing
+ * document, rather than referring to the base URI. */
+ } else {
+ /* Don't change data or http hrefs. */
+ std::string scheme = Glib::uri_parse_scheme(href);
+ if ( !scheme.empty() ) {
+ /* Assume it shouldn't be changed. This is probably wrong if the scheme is `file'
+ * (or if the scheme of the new base is non-file, though I believe that never
+ * happens at the time of writing), but that's rare, and we won't try too hard to
+ * handle this now: wait until after the freeze, then add liburiparser (or similar)
+ * as a dependency and do it properly. For now we'll just try to be simple (while
+ * at least still correctly handling data hrefs). */
+ ret = false;
+ }
+ }
+
+ return ret;
+}
+
+Inkscape::Util::List<AttributeRecord const>
+Inkscape::XML::rebase_href_attrs(gchar const *const old_abs_base,
+ gchar const *const new_abs_base,
+ Inkscape::Util::List<AttributeRecord const> attributes)
+{
+ using Inkscape::Util::List;
+ using Inkscape::Util::cons;
+ using Inkscape::Util::ptr_shared;
+ using Inkscape::Util::share_string;
+
+
+ if (old_abs_base == new_abs_base) {
+ return attributes;
+ }
+
+ GQuark const href_key = g_quark_from_static_string("xlink:href");
+ GQuark const absref_key = g_quark_from_static_string("sodipodi:absref");
+
+ /* First search attributes for xlink:href and sodipodi:absref, putting the rest in ret.
+ *
+ * However, if we find that xlink:href doesn't need rebasing, then return immediately
+ * with no change to attributes. */
+ ptr_shared old_href;
+ ptr_shared sp_absref;
+ List<AttributeRecord const> ret;
+ {
+ for (List<AttributeRecord const> ai(attributes); ai; ++ai) {
+ if (ai->key == href_key) {
+ old_href = ai->value;
+ if (!href_needs_rebasing(static_cast<char const *>(old_href))) {
+ return attributes;
+ }
+ } else if (ai->key == absref_key) {
+ sp_absref = ai->value;
+ } else {
+ ret = cons(AttributeRecord(ai->key, ai->value), ret);
+ }
+ }
+ }
+
+ if (!old_href) {
+ return attributes;
+ /* We could instead return ret in this case, i.e. ensure that sodipodi:absref is cleared if
+ * no xlink:href attribute. However, retaining it might be more cautious.
+ *
+ * (For the usual case of not present, attributes and ret will be the same except
+ * reversed.) */
+ }
+
+ auto uri = URI::from_href_and_basedir(static_cast<char const *>(old_href), old_abs_base);
+ auto abs_href = uri.toNativeFilename();
+
+ if (!Inkscape::IO::file_test(abs_href.c_str(), G_FILE_TEST_EXISTS) &&
+ Inkscape::IO::file_test(sp_absref, G_FILE_TEST_EXISTS)) {
+ uri = URI::from_native_filename(sp_absref);
+ }
+
+ std::string baseuri;
+ if (new_abs_base && new_abs_base[0]) {
+ baseuri = URI::from_dirname(new_abs_base).str();
+ }
+
+ auto new_href = uri.str(baseuri.c_str());
+
+ ret = cons(AttributeRecord(href_key, share_string(new_href.c_str())), ret); // Check if this is safe/copied or if it is only held.
+ if (sp_absref) {
+ /* We assume that if there wasn't previously a sodipodi:absref attribute
+ * then we shouldn't create one. */
+ ret = cons(AttributeRecord(absref_key, ( streq(abs_href.c_str(), sp_absref)
+ ? sp_absref
+ : share_string(abs_href.c_str()) )),
+ ret);
+ }
+
+ return ret;
+}
+
+void Inkscape::XML::rebase_hrefs(SPDocument *const doc, gchar const *const new_base, bool const spns)
+{
+ using Inkscape::URI;
+
+ std::string old_base_url_str = URI::from_dirname(doc->getDocumentBase()).str();
+ std::string new_base_url_str;
+
+ if (new_base) {
+ new_base_url_str = URI::from_dirname(new_base).str();
+ }
+
+ /* TODO: Should handle not just image but also:
+ *
+ * a, altGlyph, animElementAttrs, animate, animateColor, animateMotion, animateTransform,
+ * animation, audio, color-profile, cursor, definition-src, discard, feImage, filter,
+ * font-face-uri, foreignObject, glyphRef, handler, linearGradient, mpath, pattern,
+ * prefetch, radialGradient, script, set, textPath, tref, use, video
+ *
+ * (taken from the union of the xlink:href elements listed at
+ * http://www.w3.org/TR/SVG11/attindex.html and
+ * http://www.w3.org/TR/SVGMobile12/attributeTable.html).
+ *
+ * Also possibly some other attributes of type <URI> or <IRI> or list-thereof, or types like
+ * <paint> that can include an IRI/URI, and stylesheets and style attributes. (xlink:base is a
+ * special case. xlink:role and xlink:arcrole can be assumed to be already absolute, based on
+ * http://www.w3.org/TR/SVG11/struct.html#xlinkRefAttrs .)
+ *
+ * Note that it may not useful to set sodipodi:absref for anything other than image.
+ *
+ * Note also that Inkscape only supports fragment hrefs (href="#pattern257") for many of these
+ * cases. */
+ std::vector<SPObject *> images = doc->getResourceList("image");
+ for (auto image : images) {
+ Inkscape::XML::Node *ir = image->getRepr();
+
+ auto href_cstr = ir->attribute("xlink:href");
+ if (!href_cstr) {
+ continue;
+ }
+
+ // skip fragment URLs
+ if (href_cstr[0] == '#') {
+ continue;
+ }
+
+ // make absolute
+ URI url;
+ try {
+ url = URI(href_cstr, old_base_url_str.c_str());
+ } catch (...) {
+ continue;
+ }
+
+ // skip non-file URLs
+ if (!url.hasScheme("file")) {
+ continue;
+ }
+
+ // if path doesn't exist, use sodipodi:absref
+ if (!g_file_test(url.toNativeFilename().c_str(), G_FILE_TEST_EXISTS)) {
+ auto spabsref = ir->attribute("sodipodi:absref");
+ if (spabsref && g_file_test(spabsref, G_FILE_TEST_EXISTS)) {
+ url = URI::from_native_filename(spabsref);
+ }
+ } else if (spns) {
+ ir->setAttributeOrRemoveIfEmpty("sodipodi:absref", url.toNativeFilename());
+ }
+
+ if (!spns) {
+ ir->removeAttribute("sodipodi:absref");
+ }
+
+ auto href_str = url.str(new_base_url_str.c_str());
+ href_str = Inkscape::uri_to_iri(href_str.c_str());
+
+ ir->setAttribute("xlink:href", href_str);
+ }
+
+ doc->setDocumentBase(new_base);
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vi: set autoindent shiftwidth=4 tabstop=8 filetype=cpp expandtab softtabstop=4 fileencoding=utf-8 textwidth=99 :
diff --git a/src/xml/rebase-hrefs.h b/src/xml/rebase-hrefs.h
new file mode 100644
index 0000000..afab3e4
--- /dev/null
+++ b/src/xml/rebase-hrefs.h
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2014 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef REBASE_HREFS_H_SEEN
+#define REBASE_HREFS_H_SEEN
+
+#include "util/list.h"
+#include "xml/attribute-record.h"
+class SPDocument;
+
+namespace Inkscape {
+namespace XML {
+
+/**
+ * Change relative hrefs in doc to be relative to \a new_base instead of doc.base.
+ *
+ * (NULL doc base or new_base is interpreted as current working directory.)
+ *
+ * @param spns True if doc should contain sodipodi:absref attributes.
+ */
+void rebase_hrefs(SPDocument *doc, char const *new_base, bool spns);
+
+/**
+ * Change relative xlink:href attributes to be relative to \a new_abs_base instead of old_abs_base.
+ *
+ * Note that old_abs_base and new_abs_base must each be non-NULL, absolute directory paths.
+ */
+Inkscape::Util::List<AttributeRecord const> rebase_href_attrs(
+ char const *old_abs_base,
+ char const *new_abs_base,
+ Inkscape::Util::List<AttributeRecord const> attributes);
+
+
+// /**
+// * .
+// * @return a non-empty replacement href if needed, empty otherwise.
+// */
+// std::string rebase_href_attrs( std::string const &oldAbsBase, std::string const &newAbsBase, gchar const *href, gchar const *absref = 0 );
+
+} // namespace XML
+} // namespace Inkscape
+
+
+#endif /* !REBASE_HREFS_H_SEEN */
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vi: set autoindent shiftwidth=4 tabstop=8 filetype=cpp expandtab softtabstop=4 fileencoding=utf-8 textwidth=99 :
diff --git a/src/xml/repr-action-test.h b/src/xml/repr-action-test.h
new file mode 100644
index 0000000..29df246
--- /dev/null
+++ b/src/xml/repr-action-test.h
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2012 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#include <cxxtest/TestSuite.h>
+
+#include <cstdlib>
+#include <glib.h>
+
+#include "repr.h"
+#include "event-fns.h"
+
+static void * const null_ptr = 0;
+
+class XmlReprActionTest : public CxxTest::TestSuite
+{
+ Inkscape::XML::Document *document;
+ Inkscape::XML::Node *a, *b, *c, *root;
+
+public:
+
+ XmlReprActionTest()
+ {
+ Inkscape::GC::init();
+
+ document = sp_repr_document_new("test");
+ root = document->root();
+
+ a = document->createElement("a");
+ b = document->createElement("b");
+ c = document->createElement("c");
+ }
+ virtual ~XmlReprActionTest() {}
+
+// createSuite and destroySuite get us per-suite setup and teardown
+// without us having to worry about static initialization order, etc.
+ static XmlReprActionTest *createSuite() { return new XmlReprActionTest(); }
+ static void destroySuite( XmlReprActionTest *suite ) { delete suite; }
+
+ void testRollbackOfNodeAddition()
+ {
+ sp_repr_begin_transaction(document);
+ TS_ASSERT_EQUALS(a->parent() , null_ptr);
+
+ root->appendChild(a);
+ TS_ASSERT_EQUALS(a->parent() , root);
+
+ sp_repr_rollback(document);
+ TS_ASSERT_EQUALS(a->parent() , null_ptr);
+ }
+
+ void testRollbackOfNodeRemoval()
+ {
+ root->appendChild(a);
+
+ sp_repr_begin_transaction(document);
+ TS_ASSERT_EQUALS(a->parent() , root);
+
+ sp_repr_unparent(a);
+ TS_ASSERT_EQUALS(a->parent() , null_ptr);
+
+ sp_repr_rollback(document);
+ TS_ASSERT_EQUALS(a->parent() , root);
+
+ sp_repr_unparent(a);
+ }
+
+ void testRollbackOfNodeReordering()
+ {
+ root->appendChild(a);
+ root->appendChild(b);
+ root->appendChild(c);
+
+ sp_repr_begin_transaction(document);
+ TS_ASSERT_EQUALS(a->next() , b);
+ TS_ASSERT_EQUALS(b->next() , c);
+ TS_ASSERT_EQUALS(c->next() , null_ptr);
+
+ root->changeOrder(b, c);
+ TS_ASSERT_EQUALS(a->next() , c);
+ TS_ASSERT_EQUALS(b->next() , null_ptr);
+ TS_ASSERT_EQUALS(c->next() , b);
+
+ sp_repr_rollback(document);
+ TS_ASSERT_EQUALS(a->next() , b);
+ TS_ASSERT_EQUALS(b->next() , c);
+ TS_ASSERT_EQUALS(c->next() , null_ptr);
+
+ sp_repr_unparent(a);
+ sp_repr_unparent(b);
+ sp_repr_unparent(c);
+ }
+
+ /* lots more tests needed ... */
+};
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/repr-css.cpp b/src/xml/repr-css.cpp
new file mode 100644
index 0000000..f1b1e0c
--- /dev/null
+++ b/src/xml/repr-css.cpp
@@ -0,0 +1,519 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+/*
+ * bulia byak <buliabyak@users.sf.net>
+ * Tavmjong Bah <tavmjong@free.fr> (Documentation)
+ *
+ * Functions to manipulate SPCSSAttr which is a class derived from Inkscape::XML::Node See
+ * sp-css-attr.h and node.h
+ *
+ * SPCSSAttr is a special node type where the "attributes" are the properties in an element's style
+ * attribute. For example, style="fill:blue;stroke:none" is stored in a List (Inkscape::Util:List)
+ * where the key is the property (e.g. "fill" or "stroke") and the value is the property's value
+ * (e.g. "blue" or "none"). An element's properties are manipulated by adding, removing, or
+ * changing an item in the List. Utility functions are provided to go back and forth between the
+ * two ways of representing properties (by a string or by a list).
+ *
+ * Use sp_repr_css_write_string to go from a property list to a style string.
+ *
+ */
+
+#define SP_REPR_CSS_C
+
+#include <cstring>
+#include <string>
+#include <sstream>
+
+#include <glibmm/ustring.h>
+
+#include "3rdparty/libcroco/cr-declaration.h"
+
+#include "svg/css-ostringstream.h"
+
+#include "xml/repr.h"
+#include "xml/simple-document.h"
+#include "xml/sp-css-attr.h"
+
+using Inkscape::Util::List;
+using Inkscape::XML::AttributeRecord;
+using Inkscape::XML::SimpleNode;
+using Inkscape::XML::Node;
+using Inkscape::XML::NodeType;
+using Inkscape::XML::Document;
+
+struct SPCSSAttrImpl : public SimpleNode, public SPCSSAttr {
+public:
+ SPCSSAttrImpl(Document *doc)
+ : SimpleNode(g_quark_from_static_string("css"), doc) {}
+ SPCSSAttrImpl(SPCSSAttrImpl const &other, Document *doc)
+ : SimpleNode(other, doc) {}
+
+ NodeType type() const override { return Inkscape::XML::ELEMENT_NODE; }
+
+protected:
+ SimpleNode *_duplicate(Document* doc) const override { return new SPCSSAttrImpl(*this, doc); }
+};
+
+static void sp_repr_css_add_components(SPCSSAttr *css, Node const *repr, gchar const *attr);
+
+/**
+ * Creates an empty SPCSSAttr (a class for manipulating CSS style properties).
+ */
+SPCSSAttr *sp_repr_css_attr_new()
+{
+ static Inkscape::XML::Document *attr_doc=nullptr;
+ if (!attr_doc) {
+ attr_doc = new Inkscape::XML::SimpleDocument();
+ }
+ return new SPCSSAttrImpl(attr_doc);
+}
+
+/**
+ * Unreferences an SPCSSAttr (will be garbage collected if no references remain).
+ */
+void sp_repr_css_attr_unref(SPCSSAttr *css)
+{
+ g_assert(css != nullptr);
+ Inkscape::GC::release((Node *) css);
+}
+
+/**
+ * Creates a new SPCSSAttr with one attribute (i.e. style) copied from an existing repr (node). The
+ * repr attribute data is in the form of a char const * string (e.g. fill:#00ff00;stroke:none). The
+ * string is parsed by libcroco which returns a CRDeclaration list (a typical C linked list) of
+ * properties and values. This list is then used to fill the attributes of the new SPCSSAttr.
+ */
+SPCSSAttr *sp_repr_css_attr(Node const *repr, gchar const *attr)
+{
+ g_assert(repr != nullptr);
+ g_assert(attr != nullptr);
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+ sp_repr_css_add_components(css, repr, attr);
+ return css;
+}
+
+
+/**
+ * Attempt to parse the passed string as a hexadecimal RGB or RGBA color.
+ * @param text The Glib::ustring to parse
+ * @return New CSS style representation if the parsing was successful, NULL otherwise
+ */
+SPCSSAttr *sp_repr_css_attr_parse_color_to_fill(const Glib::ustring &text)
+{
+// TODO reuse existing code instead of replicating here.
+ Glib::ustring::size_type len = text.bytes();
+ char *str = const_cast<char *>(text.data());
+ bool attempt_alpha = false;
+ if ( !str || ( *str == '\0' ) ) {
+ return nullptr; // this is OK due to boolean short-circuit
+ }
+
+ // those conditionals guard against parsing e.g. the string "fab" as "fab000"
+ // (incomplete color) and "45fab71" as "45fab710" (incomplete alpha)
+ if ( *str == '#' ) {
+ if ( len < 7 ) {
+ return nullptr;
+ }
+ if ( len >= 9 ) {
+ attempt_alpha = true;
+ }
+ } else {
+ if ( len < 6 ) {
+ return nullptr;
+ }
+ if ( len >= 8 ) {
+ attempt_alpha = true;
+ }
+ }
+
+ unsigned int color = 0, alpha = 0xff;
+
+ // skip a leading #, if present
+ if ( *str == '#' ) {
+ ++str;
+ }
+
+ // try to parse first 6 digits
+ int res = sscanf(str, "%6x", &color);
+ if ( res && ( res != EOF ) ) {
+ if (attempt_alpha) {// try to parse alpha if there's enough characters
+ sscanf(str + 6, "%2x", &alpha);
+ if ( !res || res == EOF ) {
+ alpha = 0xff;
+ }
+ }
+
+ SPCSSAttr *color_css = sp_repr_css_attr_new();
+
+ // print and set properties
+ gchar color_str[16];
+ g_snprintf(color_str, 16, "#%06x", color);
+ sp_repr_css_set_property(color_css, "fill", color_str);
+
+ float opacity = static_cast<float>(alpha)/static_cast<float>(0xff);
+ if (opacity > 1.0) {
+ opacity = 1.0; // safeguard
+ }
+ Inkscape::CSSOStringStream opcss;
+ opcss << opacity;
+ sp_repr_css_set_property(color_css, "fill-opacity", opcss.str().data());
+ return color_css;
+ }
+ return nullptr;
+}
+
+
+/**
+ * Adds an attribute to an existing SPCSAttr with the cascaded value including all parents.
+ */
+static void sp_repr_css_attr_inherited_recursive(SPCSSAttr *css, Node const *repr, gchar const *attr)
+{
+ const Node *parent = repr->parent();
+
+ // read the ancestors from root down, using head recursion, so that children override parents
+ if (parent) {
+ sp_repr_css_attr_inherited_recursive(css, parent, attr);
+ }
+ sp_repr_css_add_components(css, repr, attr);
+}
+
+/**
+ * Creates a new SPCSSAttr with one attribute whose value is determined by cascading.
+ */
+SPCSSAttr *sp_repr_css_attr_inherited(Node const *repr, gchar const *attr)
+{
+ g_assert(repr != nullptr);
+ g_assert(attr != nullptr);
+
+ SPCSSAttr *css = sp_repr_css_attr_new();
+
+ sp_repr_css_attr_inherited_recursive(css, repr, attr);
+
+ return css;
+}
+
+/**
+ * Adds components (style properties) to an existing SPCSAttr from the specified attribute's data
+ * (nominally a style attribute).
+ *
+ */
+static void sp_repr_css_add_components(SPCSSAttr *css, Node const *repr, gchar const *attr)
+{
+ g_assert(css != nullptr);
+ g_assert(repr != nullptr);
+ g_assert(attr != nullptr);
+
+ char const *data = repr->attribute(attr);
+ sp_repr_css_attr_add_from_string(css, data);
+}
+
+/**
+ * Returns a character string of the value of a given style property or a default value if the
+ * attribute is not found.
+ */
+char const *sp_repr_css_property(SPCSSAttr *css, gchar const *name, gchar const *defval)
+{
+ g_assert(css != nullptr);
+ g_assert(name != nullptr);
+
+ char const *attr = ((Node *)css)->attribute(name);
+ return ( attr == nullptr
+ ? defval
+ : attr );
+}
+
+/**
+ * Returns a character string of the value of a given style property or a default value if the
+ * attribute is not found.
+ */
+Glib::ustring sp_repr_css_property(SPCSSAttr *css, Glib::ustring const &name, Glib::ustring const &defval)
+{
+ g_assert(css != nullptr);
+
+ Glib::ustring retval = defval;
+ char const *attr = ((Node *)css)->attribute(name.c_str());
+ if (attr) {
+ retval = attr;
+ }
+
+ return retval;
+}
+
+/**
+ * Returns true if a style property is present and its value is unset.
+ */
+bool sp_repr_css_property_is_unset(SPCSSAttr *css, gchar const *name)
+{
+ g_assert(css != nullptr);
+ g_assert(name != nullptr);
+
+ char const *attr = ((Node *)css)->attribute(name);
+ return (attr && !strcmp(attr, "inkscape:unset"));
+}
+
+
+/**
+ * Set a style property to a new value (e.g. fill to #ffff00).
+ */
+void sp_repr_css_set_property(SPCSSAttr *css, gchar const *name, gchar const *value)
+{
+ g_assert(css != nullptr);
+ g_assert(name != nullptr);
+
+ ((Node *) css)->setAttribute(name, value);
+}
+
+/**
+ * Set a style property to "inkscape:unset".
+ */
+void sp_repr_css_unset_property(SPCSSAttr *css, gchar const *name)
+{
+ g_assert(css != nullptr);
+ g_assert(name != nullptr);
+
+ ((Node *) css)->setAttribute(name, "inkscape:unset");
+}
+
+/**
+ * Return the value of a style property if property define, or a default value if not.
+ */
+double sp_repr_css_double_property(SPCSSAttr *css, gchar const *name, double defval)
+{
+ g_assert(css != nullptr);
+ g_assert(name != nullptr);
+
+ double val = defval;
+ sp_repr_get_double((Node *) css, name, &val);
+ return val;
+}
+
+/**
+ * Write a style attribute string from a list of properties stored in an SPCSAttr object.
+ */
+void sp_repr_css_write_string(SPCSSAttr *css, Glib::ustring &str)
+{
+ str.clear();
+ for ( List<AttributeRecord const> iter = css->attributeList() ;
+ iter ; ++iter )
+ {
+ if (iter->value && !strcmp(iter->value, "inkscape:unset")) {
+ continue;
+ }
+
+ str.append(g_quark_to_string(iter->key));
+ str.push_back(':');
+ str.append(iter->value); // Any necessary quoting to be done by calling routine.
+
+ if (rest(iter)) {
+ str.push_back(';');
+ }
+ }
+}
+
+/**
+ * Sets an attribute (e.g. style) to a string created from a list of style properties.
+ */
+void sp_repr_css_set(Node *repr, SPCSSAttr *css, gchar const *attr)
+{
+ g_assert(repr != nullptr);
+ g_assert(css != nullptr);
+ g_assert(attr != nullptr);
+
+ Glib::ustring value;
+ sp_repr_css_write_string(css, value);
+
+ /*
+ * If the new value is different from the old value, this will sometimes send a signal via
+ * CompositeNodeObserver::notiftyAttributeChanged() which results in calling
+ * SPObject::repr_attr_changed and thus updates the object's SPStyle. This update
+ * results in another call to repr->setAttribute().
+ */
+ repr->setAttributeOrRemoveIfEmpty(attr, value);
+}
+
+/**
+ * Loops through a List of style properties, printing key/value pairs.
+ */
+void sp_repr_css_print(SPCSSAttr *css)
+{
+ for ( List<AttributeRecord const> iter = css->attributeList() ;
+ iter ; ++iter )
+ {
+ gchar const * key = g_quark_to_string(iter->key);
+ gchar const * val = iter->value;
+ g_print("%s:\t%s\n",key,val);
+ }
+}
+
+/**
+ * Merges two SPCSSAttr's. Properties in src overwrite properties in dst if present in both.
+ */
+void sp_repr_css_merge(SPCSSAttr *dst, SPCSSAttr *src)
+{
+ g_assert(dst != nullptr);
+ g_assert(src != nullptr);
+
+ dst->mergeFrom(src, "");
+}
+
+/**
+ * Merges style properties as parsed by libcroco into an existing SPCSSAttr.
+ * libcroco converts all single quotes to double quotes, which needs to be
+ * undone as we always use single quotes inside our 'style' strings since
+ * double quotes are used outside: e.g.:
+ * style="font-family:'DejaVu Sans'"
+ */
+static void sp_repr_css_merge_from_decl(SPCSSAttr *css, CRDeclaration const *const decl)
+{
+ guchar *const str_value_unsigned = cr_term_to_string(decl->value);
+
+ Glib::ustring value( reinterpret_cast<gchar *>(str_value_unsigned ) );
+ g_free(str_value_unsigned);
+
+ Glib::ustring::size_type pos = 0;
+ while( (pos=value.find("\"",pos)) != Glib::ustring::npos) {
+ value.replace(pos,1,"'");
+ ++pos;
+ }
+
+ Glib::ustring units;
+
+ /*
+ * Problem with parsing of units em and ex, like font-size "1.2em" and "3.4ex"
+ * stringstream thinks they are in scientific "e" notation and fails
+ * Must be a better way using std::fixed, precision etc
+ *
+ * HACK for now is to strip off em and ex units and add them back at the end
+ */
+ int le = value.length();
+ if (le > 2) {
+ units = value.substr(le-2, 2);
+ if ((units == "em") || (units == "ex")) {
+ value = value.substr(0, le-2);
+ }
+ else {
+ units.clear();
+ }
+ }
+
+ // libcroco uses %.17f for formatting... leading to trailing zeros or small rounding errors.
+ // CSSOStringStream is used here to write valid CSS (as in sp_style_write_string). This has
+ // the additional benefit of respecting the numerical precision set in the SVG Output
+ // preferences. We assume any numerical part comes first (if not, the whole string is copied).
+ std::stringstream ss( value );
+ double number = 0;
+ std::string characters;
+ std::string temp;
+ bool number_valid = !(ss >> number).fail();
+ if (!number_valid) {
+ ss.clear();
+ ss.seekg(0); // work-around for a bug in libc++ (see lp:1300271)
+ }
+ while( !(ss >> temp).eof() ) {
+ characters += temp;
+ characters += " ";
+ }
+ characters += temp;
+ Inkscape::CSSOStringStream os;
+ if( number_valid ) os << number;
+ os << characters;
+ if (!units.empty()) {
+ os << units;
+ //g_message("sp_repr_css_merge_from_decl looks like em or ex units %s --> %s", str_value, os.str().c_str());
+ }
+ ((Node *) css)->setAttribute(decl->property->stryng->str, os.str());
+}
+
+/**
+ * Merges style properties as parsed by libcroco into an existing SPCSSAttr.
+ *
+ * \pre decl_list != NULL
+ */
+static void sp_repr_css_merge_from_decl_list(SPCSSAttr *css, CRDeclaration const *const decl_list)
+{
+ // read the decls from start to end, using tail recursion, so that latter declarations override
+ // (Ref: http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order point 4.)
+ // because sp_repr_css_merge_from_decl sets properties unconditionally
+ sp_repr_css_merge_from_decl(css, decl_list);
+ if (decl_list->next) {
+ sp_repr_css_merge_from_decl_list(css, decl_list->next);
+ }
+}
+
+/**
+ * Use libcroco to parse a string for CSS properties and then merge
+ * them into an existing SPCSSAttr.
+ */
+void sp_repr_css_attr_add_from_string(SPCSSAttr *css, gchar const *p)
+{
+ if (p != nullptr) {
+ CRDeclaration *const decl_list
+ = cr_declaration_parse_list_from_buf(reinterpret_cast<guchar const *>(p), CR_UTF_8);
+ if (decl_list) {
+ sp_repr_css_merge_from_decl_list(css, decl_list);
+ cr_declaration_destroy(decl_list);
+ }
+ }
+}
+
+/**
+ * Creates a new SPCSAttr with the values filled from a repr, merges in properties from the given
+ * SPCSAttr, and then replaces that SPCSAttr with the new one. This is called, for example, for
+ * each object in turn when a selection's style is updated via sp_desktop_set_style().
+ */
+void sp_repr_css_change(Node *repr, SPCSSAttr *css, gchar const *attr)
+{
+ g_assert(repr != nullptr);
+ g_assert(css != nullptr);
+ g_assert(attr != nullptr);
+
+ SPCSSAttr *current = sp_repr_css_attr(repr, attr);
+ sp_repr_css_merge(current, css);
+ sp_repr_css_set(repr, current, attr);
+
+ sp_repr_css_attr_unref(current);
+}
+
+void sp_repr_css_change_recursive(Node *repr, SPCSSAttr *css, gchar const *attr)
+{
+ g_assert(repr != nullptr);
+ g_assert(css != nullptr);
+ g_assert(attr != nullptr);
+
+ sp_repr_css_change(repr, css, attr);
+
+ for (Node *child = repr->firstChild(); child != nullptr; child = child->next()) {
+ sp_repr_css_change_recursive(child, css, attr);
+ }
+}
+
+/**
+ * Return a new SPCSSAttr with all the properties found in the input SPCSSAttr unset.
+ */
+SPCSSAttr* sp_repr_css_attr_unset_all(SPCSSAttr *css)
+{
+ SPCSSAttr* css_unset = sp_repr_css_attr_new();
+ for ( List<AttributeRecord const> iter = css->attributeList() ; iter ; ++iter ) {
+ sp_repr_css_set_property (css_unset, g_quark_to_string(iter->key), "inkscape:unset");
+ }
+ return css_unset;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/repr-io.cpp b/src/xml/repr-io.cpp
new file mode 100644
index 0000000..ac9794e
--- /dev/null
+++ b/src/xml/repr-io.cpp
@@ -0,0 +1,1065 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Dirty DOM-like tree
+ *
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * bulia byak <buliabyak@users.sf.net>
+ *
+ * Copyright (C) 1999-2002 Lauris Kaplinski
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <string>
+#include <stdexcept>
+
+#include <libxml/parser.h>
+
+#include "xml/repr.h"
+#include "xml/attribute-record.h"
+#include "xml/rebase-hrefs.h"
+#include "xml/simple-document.h"
+#include "xml/text-node.h"
+
+#include "io/sys.h"
+#include "io/stream/stringstream.h"
+#include "io/stream/gzipstream.h"
+#include "io/stream/uristream.h"
+
+#include "extension/extension.h"
+
+#include "attribute-rel-util.h"
+#include "attribute-sort-util.h"
+
+#include "preferences.h"
+
+#include <glibmm/miscutils.h>
+
+using Inkscape::IO::Writer;
+using Inkscape::Util::List;
+using Inkscape::Util::cons;
+using Inkscape::XML::Document;
+using Inkscape::XML::SimpleDocument;
+using Inkscape::XML::Node;
+using Inkscape::XML::AttributeRecord;
+using Inkscape::XML::rebase_href_attrs;
+
+Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns);
+static Node *sp_repr_svg_read_node (Document *xml_doc, xmlNodePtr node, const gchar *default_ns, std::map<std::string, std::string> &prefix_map);
+static gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar *default_ns, std::map<std::string, std::string> &prefix_map);
+static void sp_repr_write_stream_root_element(Node *repr, Writer &out,
+ bool add_whitespace, gchar const *default_ns,
+ int inlineattrs, int indent,
+ gchar const *old_href_abs_base,
+ gchar const *new_href_abs_base);
+
+static void sp_repr_write_stream_element(Node *repr, Writer &out,
+ gint indent_level, bool add_whitespace,
+ Glib::QueryQuark elide_prefix,
+ List<AttributeRecord const> attributes,
+ int inlineattrs, int indent,
+ gchar const *old_href_abs_base,
+ gchar const *new_href_abs_base);
+
+
+class XmlSource
+{
+public:
+ XmlSource()
+ : filename(nullptr),
+ encoding(nullptr),
+ fp(nullptr),
+ firstFewLen(0),
+ LoadEntities(false),
+ cachedData(),
+ cachedPos(0),
+ instr(nullptr),
+ gzin(nullptr)
+ {
+ for (unsigned char & k : firstFew)
+ {
+ k=0;
+ }
+ }
+ virtual ~XmlSource()
+ {
+ close();
+ if ( encoding ) {
+ g_free(encoding);
+ encoding = nullptr;
+ }
+ }
+
+ int setFile( char const * filename, bool load_entities );
+
+ xmlDocPtr readXml();
+
+ static int readCb( void * context, char * buffer, int len );
+ static int closeCb( void * context );
+
+ char const* getEncoding() const { return encoding; }
+ int read( char * buffer, int len );
+ int close();
+private:
+ const char* filename;
+ char* encoding;
+ FILE* fp;
+ unsigned char firstFew[4];
+ int firstFewLen;
+ bool LoadEntities; // Checks for SYSTEM Entities (requires cached data)
+ std::string cachedData;
+ unsigned int cachedPos;
+ Inkscape::IO::FileInputStream* instr;
+ Inkscape::IO::GzipInputStream* gzin;
+};
+
+int XmlSource::setFile(char const *filename, bool load_entities=false)
+{
+ int retVal = -1;
+
+ this->filename = filename;
+
+ fp = Inkscape::IO::fopen_utf8name(filename, "r");
+ if ( fp ) {
+ // First peek in the file to see what it is
+ memset( firstFew, 0, sizeof(firstFew) );
+
+ size_t some = fread( firstFew, 1, 4, fp );
+ if ( fp ) {
+ // first check for compression
+ if ( (some >= 2) && (firstFew[0] == 0x1f) && (firstFew[1] == 0x8b) ) {
+ //g_message(" the file being read is gzip'd. extract it");
+ fclose(fp);
+ fp = nullptr;
+ fp = Inkscape::IO::fopen_utf8name(filename, "r");
+ instr = new Inkscape::IO::FileInputStream(fp);
+ gzin = new Inkscape::IO::GzipInputStream(*instr);
+
+ memset( firstFew, 0, sizeof(firstFew) );
+ some = 0;
+ int single = 0;
+ while ( some < 4 && single >= 0 )
+ {
+ single = gzin->get();
+ if ( single >= 0 ) {
+ firstFew[some++] = 0x0ff & single;
+ } else {
+ break;
+ }
+ }
+ }
+
+ int encSkip = 0;
+ if ( (some >= 2) &&(firstFew[0] == 0xfe) && (firstFew[1] == 0xff) ) {
+ encoding = g_strdup("UTF-16BE");
+ encSkip = 2;
+ } else if ( (some >= 2) && (firstFew[0] == 0xff) && (firstFew[1] == 0xfe) ) {
+ encoding = g_strdup("UTF-16LE");
+ encSkip = 2;
+ } else if ( (some >= 3) && (firstFew[0] == 0xef) && (firstFew[1] == 0xbb) && (firstFew[2] == 0xbf) ) {
+ encoding = g_strdup("UTF-8");
+ encSkip = 3;
+ }
+
+ if ( encSkip ) {
+ memmove( firstFew, firstFew + encSkip, (some - encSkip) );
+ some -= encSkip;
+ }
+
+ firstFewLen = some;
+ retVal = 0; // no error
+ }
+ }
+ if(load_entities) {
+ this->cachedData = std::string("");
+ this->cachedPos = 0;
+
+ // First get data from file in typical way (cache it all)
+ char *buffer = new char [4096];
+ while(true) {
+ int len = this->read(buffer, 4096);
+ if(len <= 0) break;
+ buffer[len] = 0;
+ this->cachedData += buffer;
+ }
+ delete[] buffer;
+
+ // Check for SYSTEM or PUBLIC entities and remove them from the cache
+ GMatchInfo *info;
+ gint start, end;
+
+ GRegex *regex = g_regex_new(
+ "<!ENTITY\\s+[^>\\s]+\\s+(SYSTEM|PUBLIC\\s+\"[^>\"]+\")\\s+\"[^>\"]+\"\\s*>",
+ G_REGEX_CASELESS, G_REGEX_MATCH_NEWLINE_ANY, nullptr);
+
+ g_regex_match (regex, this->cachedData.c_str(), G_REGEX_MATCH_NEWLINE_ANY, &info);
+
+ while (g_match_info_matches (info)) {
+ if (g_match_info_fetch_pos (info, 1, &start, &end))
+ this->cachedData.erase(start, end - start);
+ g_match_info_next (info, nullptr);
+ }
+ g_match_info_free(info);
+ g_regex_unref(regex);
+ }
+ // Do this after loading cache, so reads don't return cache to fill cache.
+ this->LoadEntities = load_entities;
+ return retVal;
+}
+
+xmlDocPtr XmlSource::readXml()
+{
+ int parse_options = XML_PARSE_HUGE | XML_PARSE_RECOVER;
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool allowNetAccess = prefs->getBool("/options/externalresources/xml/allow_net_access", false);
+ if (!allowNetAccess) parse_options |= XML_PARSE_NONET;
+
+ // Allow NOENT only if we're filtering out SYSTEM and PUBLIC entities
+ if (LoadEntities) parse_options |= XML_PARSE_NOENT;
+
+ return xmlReadIO( readCb, closeCb, this,
+ filename, getEncoding(), parse_options);
+}
+
+int XmlSource::readCb( void * context, char * buffer, int len )
+{
+ int retVal = -1;
+
+ if ( context ) {
+ XmlSource* self = static_cast<XmlSource*>(context);
+ retVal = self->read( buffer, len );
+ }
+ return retVal;
+}
+
+int XmlSource::closeCb(void * context)
+{
+ if ( context ) {
+ XmlSource* self = static_cast<XmlSource*>(context);
+ self->close();
+ }
+ return 0;
+}
+
+int XmlSource::read( char *buffer, int len )
+{
+ int retVal = 0;
+ size_t got = 0;
+
+ if ( LoadEntities ) {
+ if (cachedPos >= cachedData.length()) {
+ return -1;
+ } else {
+ retVal = cachedData.copy(buffer, len, cachedPos);
+ cachedPos += retVal;
+ return retVal; // Do NOT continue.
+ }
+ } else if ( firstFewLen > 0 ) {
+ int some = (len < firstFewLen) ? len : firstFewLen;
+ memcpy( buffer, firstFew, some );
+ if ( len < firstFewLen ) {
+ memmove( firstFew, firstFew + some, (firstFewLen - some) );
+ }
+ firstFewLen -= some;
+ got = some;
+ } else if ( gzin ) {
+ int single = 0;
+ while ( (static_cast<int>(got) < len) && (single >= 0) )
+ {
+ single = gzin->get();
+ if ( single >= 0 ) {
+ buffer[got++] = 0x0ff & single;
+ } else {
+ break;
+ }
+ }
+ } else {
+ got = fread( buffer, 1, len, fp );
+ }
+
+ if ( feof(fp) ) {
+ retVal = got;
+ } else if ( ferror(fp) ) {
+ retVal = -1;
+ } else {
+ retVal = got;
+ }
+
+ return retVal;
+}
+
+int XmlSource::close()
+{
+ if ( gzin ) {
+ gzin->close();
+ delete gzin;
+ gzin = nullptr;
+ }
+ if ( instr ) {
+ instr->close();
+ fp = nullptr;
+ delete instr;
+ instr = nullptr;
+ }
+ if ( fp ) {
+ fclose(fp);
+ fp = nullptr;
+ }
+ return 0;
+}
+
+/**
+ * Reads XML from a file, and returns the Document.
+ * The default namespace can also be specified, if desired.
+ */
+Document *sp_repr_read_file (const gchar * filename, const gchar *default_ns)
+{
+ xmlDocPtr doc = nullptr;
+ Document * rdoc = nullptr;
+
+ xmlSubstituteEntitiesDefault(1);
+
+ g_return_val_if_fail(filename != nullptr, NULL);
+ if (!Inkscape::IO::file_test(filename, G_FILE_TEST_EXISTS)) {
+ g_warning("Can't open file: %s (doesn't exist)", filename);
+ return nullptr;
+ }
+ /* fixme: A file can disappear at any time, including between now and when we actually try to
+ * open it. Get rid of the above test once we're sure that we correctly handle
+ * non-existence. */
+
+ // TODO: bulia, please look over
+ gsize bytesRead = 0;
+ gsize bytesWritten = 0;
+ GError* error = nullptr;
+ // TODO: need to replace with our own fopen and reading
+ gchar* localFilename = g_filename_from_utf8(filename, -1, &bytesRead, &bytesWritten, &error);
+ g_return_val_if_fail(localFilename != nullptr, NULL);
+
+ Inkscape::IO::dump_fopen_call(filename, "N");
+
+ XmlSource src;
+
+ if (src.setFile(filename) == 0) {
+ doc = src.readXml();
+ rdoc = sp_repr_do_read(doc, default_ns);
+ // For some reason, failed ns loading results in this
+ // We try a system check version of load with NOENT for adobe
+ if (rdoc && strcmp(rdoc->root()->name(), "ns:svg") == 0) {
+ xmlFreeDoc(doc);
+ src.setFile(filename, true);
+ doc = src.readXml();
+ rdoc = sp_repr_do_read(doc, default_ns);
+ }
+ }
+
+ if (doc) {
+ xmlFreeDoc(doc);
+ }
+
+ if (localFilename) {
+ g_free(localFilename);
+ }
+
+ return rdoc;
+}
+
+/**
+ * Reads and parses XML from a buffer, returning it as an Document
+ */
+Document *sp_repr_read_mem (const gchar * buffer, gint length, const gchar *default_ns)
+{
+ xmlDocPtr doc;
+ Document * rdoc;
+
+ xmlSubstituteEntitiesDefault(1);
+
+ g_return_val_if_fail (buffer != nullptr, NULL);
+
+ int parser_options = XML_PARSE_HUGE | XML_PARSE_RECOVER;
+ parser_options |= XML_PARSE_NONET; // TODO: should we allow network access?
+ // proper solution would be to check the preference "/options/externalresources/xml/allow_net_access"
+ // as done in XmlSource::readXml which gets called by the analogous sp_repr_read_file()
+ // but sp_repr_read_mem() seems to be called in locations where Inkscape::Preferences::get() fails badly
+ doc = xmlReadMemory (const_cast<gchar *>(buffer), length, nullptr, nullptr, parser_options);
+
+ rdoc = sp_repr_do_read (doc, default_ns);
+ if (doc) {
+ xmlFreeDoc (doc);
+ }
+ return rdoc;
+}
+
+/**
+ * Reads and parses XML from a buffer, returning it as an Document
+ */
+Document *sp_repr_read_buf (const Glib::ustring &buf, const gchar *default_ns)
+{
+ return sp_repr_read_mem(buf.c_str(), buf.size(), default_ns);
+}
+
+
+namespace Inkscape {
+
+struct compare_quark_ids {
+ bool operator()(Glib::QueryQuark const &a, Glib::QueryQuark const &b) const {
+ return a.id() < b.id();
+ }
+};
+
+}
+
+namespace {
+
+typedef std::map<Glib::QueryQuark, Glib::QueryQuark, Inkscape::compare_quark_ids> PrefixMap;
+
+Glib::QueryQuark qname_prefix(Glib::QueryQuark qname) {
+ static PrefixMap prefix_map;
+ PrefixMap::iterator iter = prefix_map.find(qname);
+ if ( iter != prefix_map.end() ) {
+ return (*iter).second;
+ } else {
+ gchar const *name_string=g_quark_to_string(qname);
+ gchar const *prefix_end=strchr(name_string, ':');
+ if (prefix_end) {
+ Glib::Quark prefix=Glib::ustring(name_string, prefix_end);
+ prefix_map.insert(PrefixMap::value_type(qname, prefix));
+ return prefix;
+ } else {
+ return GQuark(0);
+ }
+ }
+}
+
+}
+
+namespace {
+
+void promote_to_namespace(Node *repr, const gchar *prefix) {
+ if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) {
+ GQuark code = repr->code();
+ if (!qname_prefix(code).id()) {
+ gchar *svg_name = g_strconcat(prefix, ":", g_quark_to_string(code), NULL);
+ repr->setCodeUnsafe(g_quark_from_string(svg_name));
+ g_free(svg_name);
+ }
+ for ( Node *child = repr->firstChild() ; child ; child = child->next() ) {
+ promote_to_namespace(child, prefix);
+ }
+ }
+}
+
+}
+
+/**
+ * Reads in a XML file to create a Document
+ */
+Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns)
+{
+ if (doc == nullptr) {
+ return nullptr;
+ }
+ xmlNodePtr node=xmlDocGetRootElement (doc);
+ if (node == nullptr) {
+ return nullptr;
+ }
+
+ std::map<std::string, std::string> prefix_map;
+
+ Document *rdoc = new Inkscape::XML::SimpleDocument();
+
+ Node *root=nullptr;
+ for ( node = doc->children ; node != nullptr ; node = node->next ) {
+ if (node->type == XML_ELEMENT_NODE) {
+ Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map);
+ rdoc->appendChild(repr);
+ Inkscape::GC::release(repr);
+
+ if (!root) {
+ root = repr;
+ } else {
+ root = nullptr;
+ break;
+ }
+ } else if ( node->type == XML_COMMENT_NODE || node->type == XML_PI_NODE ) {
+ Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map);
+ rdoc->appendChild(repr);
+ Inkscape::GC::release(repr);
+ }
+ }
+
+ if (root != nullptr) {
+ /* promote elements of some XML documents that don't use namespaces
+ * into their default namespace */
+ if ( default_ns && !strchr(root->name(), ':') ) {
+ if ( !strcmp(default_ns, SP_SVG_NS_URI) ) {
+ promote_to_namespace(root, "svg");
+ }
+ if ( !strcmp(default_ns, INKSCAPE_EXTENSION_URI) ) {
+ promote_to_namespace(root, INKSCAPE_EXTENSION_NS_NC);
+ }
+ }
+
+
+ // Clean unnecessary attributes and style properties from SVG documents. (Controlled by
+ // preferences.) Note: internal Inkscape svg files will also be cleaned (filters.svg,
+ // icons.svg). How can one tell if a file is internal?
+ if ( !strcmp(root->name(), "svg:svg" ) ) {
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool clean = prefs->getBool("/options/svgoutput/check_on_reading");
+ if( clean ) {
+ sp_attribute_clean_tree( root );
+ }
+ }
+ }
+
+ return rdoc;
+}
+
+gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar */*default_ns*/, std::map<std::string, std::string> &prefix_map)
+{
+ const xmlChar *prefix;
+ if (ns){
+ if (ns->href ) {
+ prefix = reinterpret_cast<const xmlChar*>( sp_xml_ns_uri_prefix(reinterpret_cast<const gchar*>(ns->href),
+ reinterpret_cast<const char*>(ns->prefix)) );
+ prefix_map[reinterpret_cast<const char*>(prefix)] = reinterpret_cast<const char*>(ns->href);
+ }
+ else {
+ prefix = nullptr;
+ }
+ }
+ else {
+ prefix = nullptr;
+ }
+
+ if (prefix) {
+ return g_snprintf (p, len, "%s:%s", reinterpret_cast<const gchar*>(prefix), name);
+ } else {
+ return g_snprintf (p, len, "%s", name);
+ }
+}
+
+static Node *sp_repr_svg_read_node (Document *xml_doc, xmlNodePtr node, const gchar *default_ns, std::map<std::string, std::string> &prefix_map)
+{
+ xmlAttrPtr prop;
+ xmlNodePtr child;
+ gchar c[256];
+
+ if (node->type == XML_TEXT_NODE || node->type == XML_CDATA_SECTION_NODE) {
+
+ if (node->content == nullptr || *(node->content) == '\0') {
+ return nullptr; // empty text node
+ }
+
+ // Since libxml2 2.9.0, only element nodes are checked, thus check parent.
+ // Note: this only handles XML's rules for white space. SVG's specific rules
+ // are handled in sp-string.cpp.
+ bool preserve = (xmlNodeGetSpacePreserve (node->parent) == 1);
+
+ xmlChar *p;
+ for (p = node->content; *p && g_ascii_isspace (*p) && !preserve; p++)
+ ; // skip all whitespace
+
+ if (!(*p)) { // this is an all-whitespace node, and preserve == default
+ return nullptr; // we do not preserve all-whitespace nodes unless we are asked to
+ }
+
+ // We keep track of original node type so that CDATA sections are preserved on output.
+ return xml_doc->createTextNode(reinterpret_cast<gchar *>(node->content),
+ node->type == XML_CDATA_SECTION_NODE );
+ }
+
+ if (node->type == XML_COMMENT_NODE) {
+ return xml_doc->createComment(reinterpret_cast<gchar *>(node->content));
+ }
+
+ if (node->type == XML_PI_NODE) {
+ return xml_doc->createPI(reinterpret_cast<const gchar *>(node->name),
+ reinterpret_cast<const gchar *>(node->content));
+ }
+
+ if (node->type == XML_ENTITY_DECL) {
+ return nullptr;
+ }
+
+ sp_repr_qualified_name (c, 256, node->ns, node->name, default_ns, prefix_map);
+ Node *repr = xml_doc->createElement(c);
+ /* TODO remember node->ns->prefix if node->ns != NULL */
+
+ for (prop = node->properties; prop != nullptr; prop = prop->next) {
+ if (prop->children) {
+ sp_repr_qualified_name (c, 256, prop->ns, prop->name, default_ns, prefix_map);
+ repr->setAttribute(c, reinterpret_cast<gchar*>(prop->children->content));
+ /* TODO remember prop->ns->prefix if prop->ns != NULL */
+ }
+ }
+
+ if (node->content) {
+ repr->setContent(reinterpret_cast<gchar*>(node->content));
+ }
+
+ for (child = node->xmlChildrenNode; child != nullptr; child = child->next) {
+ Node *crepr = sp_repr_svg_read_node (xml_doc, child, default_ns, prefix_map);
+ if (crepr) {
+ repr->appendChild(crepr);
+ Inkscape::GC::release(crepr);
+ }
+ }
+
+ return repr;
+}
+
+
+static void sp_repr_save_writer(Document *doc, Inkscape::IO::Writer *out,
+ gchar const *default_ns,
+ gchar const *old_href_abs_base,
+ gchar const *new_href_abs_base)
+{
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool inlineattrs = prefs->getBool("/options/svgoutput/inlineattrs");
+ int indent = prefs->getInt("/options/svgoutput/indent", 2);
+
+ /* fixme: do this The Right Way */
+ out->writeString( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" );
+
+ const gchar *str = static_cast<Node *>(doc)->attribute("doctype");
+ if (str) {
+ out->writeString( str );
+ }
+
+ for (Node *repr = sp_repr_document_first_child(doc);
+ repr; repr = repr->next())
+ {
+ Inkscape::XML::NodeType const node_type = repr->type();
+ if ( node_type == Inkscape::XML::ELEMENT_NODE ) {
+ sp_repr_write_stream_root_element(repr, *out, TRUE, default_ns, inlineattrs, indent,
+ old_href_abs_base, new_href_abs_base);
+ } else {
+ sp_repr_write_stream(repr, *out, 0, TRUE, GQuark(0), inlineattrs, indent,
+ old_href_abs_base, new_href_abs_base);
+ if ( node_type == Inkscape::XML::COMMENT_NODE ) {
+ out->writeChar('\n');
+ }
+ }
+ }
+}
+
+
+Glib::ustring sp_repr_save_buf(Document *doc)
+{
+ Inkscape::IO::StringOutputStream souts;
+ Inkscape::IO::OutputStreamWriter outs(souts);
+
+ sp_repr_save_writer(doc, &outs, SP_INKSCAPE_NS_URI, nullptr, nullptr);
+
+ outs.close();
+ Glib::ustring buf = souts.getString();
+
+ return buf;
+}
+
+
+void sp_repr_save_stream(Document *doc, FILE *fp, gchar const *default_ns, bool compress,
+ gchar const *const old_href_abs_base,
+ gchar const *const new_href_abs_base)
+{
+ Inkscape::IO::FileOutputStream bout(fp);
+ Inkscape::IO::GzipOutputStream *gout = compress ? new Inkscape::IO::GzipOutputStream(bout) : nullptr;
+ Inkscape::IO::OutputStreamWriter *out = compress ? new Inkscape::IO::OutputStreamWriter( *gout ) : new Inkscape::IO::OutputStreamWriter( bout );
+
+ sp_repr_save_writer(doc, out, default_ns, old_href_abs_base, new_href_abs_base);
+
+ delete out;
+ delete gout;
+}
+
+
+
+/**
+ * Returns true if file successfully saved.
+ *
+ * \param filename The actual file to do I/O to, which might be a temp file.
+ *
+ * \param for_filename The base URI [actually filename] to assume for purposes of rewriting
+ * xlink:href attributes.
+ */
+bool sp_repr_save_rebased_file(Document *doc, gchar const *const filename, gchar const *default_ns,
+ gchar const *old_base, gchar const *for_filename)
+{
+ if (!filename) {
+ return false;
+ }
+
+ bool compress;
+ {
+ size_t const filename_len = strlen(filename);
+ compress = ( filename_len > 5
+ && strcasecmp(".svgz", filename + filename_len - 5) == 0 );
+ }
+
+ Inkscape::IO::dump_fopen_call( filename, "B" );
+ FILE *file = Inkscape::IO::fopen_utf8name(filename, "w");
+ if (file == nullptr) {
+ return false;
+ }
+
+ Glib::ustring old_href_abs_base;
+ Glib::ustring new_href_abs_base;
+
+ if (old_base) {
+ old_href_abs_base = old_base;
+ if (!Glib::path_is_absolute(old_href_abs_base)) {
+ old_href_abs_base = Glib::build_filename(Glib::get_current_dir(), old_href_abs_base);
+ }
+ }
+
+ if (for_filename) {
+ if (Glib::path_is_absolute(for_filename)) {
+ new_href_abs_base = Glib::path_get_dirname(for_filename);
+ } else {
+ Glib::ustring const cwd = Glib::get_current_dir();
+ Glib::ustring const for_abs_filename = Glib::build_filename(cwd, for_filename);
+ new_href_abs_base = Glib::path_get_dirname(for_abs_filename);
+ }
+
+ /* effic: Once we're confident that we never need (or never want) to resort
+ * to using sodipodi:absref instead of the xlink:href value,
+ * then we should do `if streq() { free them and set both to NULL; }'. */
+ }
+ sp_repr_save_stream(doc, file, default_ns, compress, old_href_abs_base.c_str(), new_href_abs_base.c_str());
+
+ if (fclose (file) != 0) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Returns true iff file successfully saved.
+ */
+bool sp_repr_save_file(Document *doc, gchar const *const filename, gchar const *default_ns)
+{
+ return sp_repr_save_rebased_file(doc, filename, default_ns, nullptr, nullptr);
+}
+
+
+/* (No doubt this function already exists elsewhere.) */
+static void repr_quote_write (Writer &out, const gchar * val)
+{
+ if (val) {
+ for (; *val != '\0'; val++) {
+ switch (*val) {
+ case '"': out.writeString( "&quot;" ); break;
+ case '&': out.writeString( "&amp;" ); break;
+ case '<': out.writeString( "&lt;" ); break;
+ case '>': out.writeString( "&gt;" ); break;
+ default: out.writeChar( *val ); break;
+ }
+ }
+ }
+}
+
+static void repr_write_comment( Writer &out, const gchar * val, bool addWhitespace, gint indentLevel, int indent )
+{
+ if ( indentLevel > 16 ) {
+ indentLevel = 16;
+ }
+ if (addWhitespace && indent) {
+ for (gint i = 0; i < indentLevel; i++) {
+ for (gint j = 0; j < indent; j++) {
+ out.writeChar(' ');
+ }
+ }
+ }
+
+ out.printf("<!--%s-->", val);
+
+ if (addWhitespace) {
+ out.writeChar('\n');
+ }
+}
+
+namespace {
+
+typedef std::map<Glib::QueryQuark, gchar const *, Inkscape::compare_quark_ids> LocalNameMap;
+typedef std::map<Glib::QueryQuark, Inkscape::Util::ptr_shared, Inkscape::compare_quark_ids> NSMap;
+
+gchar const *qname_local_name(Glib::QueryQuark qname) {
+ static LocalNameMap local_name_map;
+ LocalNameMap::iterator iter = local_name_map.find(qname);
+ if ( iter != local_name_map.end() ) {
+ return (*iter).second;
+ } else {
+ gchar const *name_string=g_quark_to_string(qname);
+ gchar const *prefix_end=strchr(name_string, ':');
+ if (prefix_end) {
+ return prefix_end + 1;
+ } else {
+ return name_string;
+ }
+ }
+}
+
+void add_ns_map_entry(NSMap &ns_map, Glib::QueryQuark prefix) {
+ using Inkscape::Util::ptr_shared;
+ using Inkscape::Util::share_unsafe;
+
+ static const Glib::QueryQuark xml_prefix("xml");
+
+ NSMap::iterator iter=ns_map.find(prefix);
+ if ( iter == ns_map.end() ) {
+ if (prefix.id()) {
+ gchar const *uri=sp_xml_ns_prefix_uri(g_quark_to_string(prefix));
+ if (uri) {
+ ns_map.insert(NSMap::value_type(prefix, share_unsafe(uri)));
+ } else if ( prefix != xml_prefix ) {
+ g_warning("No namespace known for normalized prefix %s", g_quark_to_string(prefix));
+ }
+ } else {
+ ns_map.insert(NSMap::value_type(prefix, ptr_shared()));
+ }
+ }
+}
+
+void populate_ns_map(NSMap &ns_map, Node &repr) {
+ if ( repr.type() == Inkscape::XML::ELEMENT_NODE ) {
+ add_ns_map_entry(ns_map, qname_prefix(repr.code()));
+ for ( List<AttributeRecord const> iter=repr.attributeList() ;
+ iter ; ++iter )
+ {
+ Glib::QueryQuark prefix=qname_prefix(iter->key);
+ if (prefix.id()) {
+ add_ns_map_entry(ns_map, prefix);
+ }
+ }
+ for ( Node *child=repr.firstChild() ;
+ child ; child = child->next() )
+ {
+ populate_ns_map(ns_map, *child);
+ }
+ }
+}
+
+}
+
+static void sp_repr_write_stream_root_element(Node *repr, Writer &out,
+ bool add_whitespace, gchar const *default_ns,
+ int inlineattrs, int indent,
+ gchar const *const old_href_base,
+ gchar const *const new_href_base)
+{
+ using Inkscape::Util::ptr_shared;
+
+ g_assert(repr != nullptr);
+
+ // Clean unnecessary attributes and stype properties. (Controlled by preferences.)
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ bool clean = prefs->getBool("/options/svgoutput/check_on_writing");
+ if (clean) sp_attribute_clean_tree( repr );
+
+ // Sort attributes in a canonical order (helps with "diffing" SVG files).only if not set disable optimizations
+ bool sort = !prefs->getBool("/options/svgoutput/disable_optimizations") && prefs->getBool("/options/svgoutput/sort_attributes");
+ if (sort) sp_attribute_sort_tree( repr );
+
+ Glib::QueryQuark xml_prefix=g_quark_from_static_string("xml");
+
+ NSMap ns_map;
+ populate_ns_map(ns_map, *repr);
+
+ Glib::QueryQuark elide_prefix=GQuark(0);
+ if ( default_ns && ns_map.find(GQuark(0)) == ns_map.end() ) {
+ elide_prefix = g_quark_from_string(sp_xml_ns_uri_prefix(default_ns, nullptr));
+ }
+
+ List<AttributeRecord const> attributes=repr->attributeList();
+ for (auto & iter : ns_map)
+ {
+ Glib::QueryQuark prefix=iter.first;
+ ptr_shared ns_uri=iter.second;
+
+ if (prefix.id()) {
+ if ( prefix != xml_prefix ) {
+ if ( elide_prefix == prefix ) {
+ attributes = cons(AttributeRecord(g_quark_from_static_string("xmlns"), ns_uri), attributes);
+ }
+
+ Glib::ustring attr_name="xmlns:";
+ attr_name.append(g_quark_to_string(prefix));
+ GQuark key = g_quark_from_string(attr_name.c_str());
+ attributes = cons(AttributeRecord(key, ns_uri), attributes);
+ }
+ } else {
+ // if there are non-namespaced elements, we can't globally
+ // use a default namespace
+ elide_prefix = GQuark(0);
+ }
+ }
+
+ return sp_repr_write_stream_element(repr, out, 0, add_whitespace, elide_prefix, attributes,
+ inlineattrs, indent, old_href_base, new_href_base);
+}
+
+void sp_repr_write_stream( Node *repr, Writer &out, gint indent_level,
+ bool add_whitespace, Glib::QueryQuark elide_prefix,
+ int inlineattrs, int indent,
+ gchar const *const old_href_base,
+ gchar const *const new_href_base)
+{
+ switch (repr->type()) {
+ case Inkscape::XML::TEXT_NODE: {
+ if( dynamic_cast<const Inkscape::XML::TextNode *>(repr)->is_CData() ) {
+ // Preserve CDATA sections, not converting '&' to &amp;, etc.
+ out.printf( "<![CDATA[%s]]>", repr->content() );
+ } else {
+ repr_quote_write( out, repr->content() );
+ }
+ break;
+ }
+ case Inkscape::XML::COMMENT_NODE: {
+ repr_write_comment( out, repr->content(), add_whitespace, indent_level, indent );
+ break;
+ }
+ case Inkscape::XML::PI_NODE: {
+ out.printf( "<?%s %s?>", repr->name(), repr->content() );
+ break;
+ }
+ case Inkscape::XML::ELEMENT_NODE: {
+ sp_repr_write_stream_element( repr, out, indent_level,
+ add_whitespace, elide_prefix,
+ repr->attributeList(),
+ inlineattrs, indent,
+ old_href_base, new_href_base);
+ break;
+ }
+ case Inkscape::XML::DOCUMENT_NODE: {
+ g_assert_not_reached();
+ break;
+ }
+ default: {
+ g_assert_not_reached();
+ }
+ }
+}
+
+
+void sp_repr_write_stream_element( Node * repr, Writer & out,
+ gint indent_level, bool add_whitespace,
+ Glib::QueryQuark elide_prefix,
+ List<AttributeRecord const> attributes,
+ int inlineattrs, int indent,
+ gchar const *old_href_base,
+ gchar const *new_href_base )
+{
+ Node *child = nullptr;
+ bool loose = false;
+ bool const add_whitespace_parent = add_whitespace;
+
+ g_return_if_fail (repr != nullptr);
+
+ if ( indent_level > 16 ) {
+ indent_level = 16;
+ }
+
+ if (add_whitespace && indent) {
+ for (gint i = 0; i < indent_level; i++) {
+ for (gint j = 0; j < indent; j++) {
+ out.writeChar(' ');
+ }
+ }
+ }
+
+ GQuark code = repr->code();
+ gchar const *element_name;
+ if ( elide_prefix == qname_prefix(code) ) {
+ element_name = qname_local_name(code);
+ } else {
+ element_name = g_quark_to_string(code);
+ }
+ out.printf( "<%s", element_name );
+
+ // If this is a <text> element, suppress formatting whitespace
+ // for its content and children:
+ if (strcmp(repr->name(), "svg:text") == 0 ||
+ strcmp(repr->name(), "svg:flowRoot") == 0) {
+ add_whitespace = false;
+ } else {
+ // Suppress formatting whitespace for xml:space="preserve"
+ gchar const *xml_space_attr = repr->attribute("xml:space");
+ if (g_strcmp0(xml_space_attr, "preserve") == 0) {
+ add_whitespace = false;
+ } else if (g_strcmp0(xml_space_attr, "default") == 0) {
+ add_whitespace = true;
+ }
+ }
+
+ for ( List<AttributeRecord const> iter = rebase_href_attrs(old_href_base, new_href_base,
+ attributes);
+ iter ; ++iter )
+ {
+ if (!inlineattrs) {
+ out.writeChar('\n');
+ if (indent) {
+ for ( gint i = 0 ; i < indent_level + 1 ; i++ ) {
+ for ( gint j = 0 ; j < indent ; j++ ) {
+ out.writeChar(' ');
+ }
+ }
+ }
+ }
+ out.printf(" %s=\"", g_quark_to_string(iter->key));
+ repr_quote_write(out, iter->value);
+ out.writeChar('"');
+ }
+
+ loose = TRUE;
+ for (child = repr->firstChild() ; child != nullptr; child = child->next()) {
+ if (child->type() == Inkscape::XML::TEXT_NODE) {
+ loose = FALSE;
+ break;
+ }
+ }
+
+ if (repr->firstChild()) {
+ out.writeChar('>');
+ if (loose && add_whitespace) {
+ out.writeChar('\n');
+ }
+ for (child = repr->firstChild(); child != nullptr; child = child->next()) {
+ sp_repr_write_stream(child, out, ( loose ? indent_level + 1 : 0 ),
+ add_whitespace, elide_prefix, inlineattrs, indent,
+ old_href_base, new_href_base);
+ }
+
+ if (loose && add_whitespace && indent) {
+ for (gint i = 0; i < indent_level; i++) {
+ for ( gint j = 0 ; j < indent ; j++ ) {
+ out.writeChar(' ');
+ }
+ }
+ }
+ out.printf( "</%s>", element_name );
+ } else {
+ out.writeString( " />" );
+ }
+
+ if (add_whitespace_parent) {
+ out.writeChar('\n');
+ }
+}
+
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/repr-sorting.cpp b/src/xml/repr-sorting.cpp
new file mode 100644
index 0000000..325b2a6
--- /dev/null
+++ b/src/xml/repr-sorting.cpp
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "util/longest-common-suffix.h"
+#include "xml/repr.h"
+#include "xml/node-iterators.h"
+#include "repr-sorting.h"
+
+static bool same_repr(Inkscape::XML::Node const &a, Inkscape::XML::Node const &b)
+{
+ /* todo: I'm not certain that it's legal to take the address of a reference. Check the exact wording of the spec on this matter. */
+ return &a == &b;
+}
+
+Inkscape::XML::Node const *LCA(Inkscape::XML::Node const *a, Inkscape::XML::Node const *b)
+{
+ using Inkscape::Algorithms::longest_common_suffix;
+ Inkscape::XML::Node const *ancestor = longest_common_suffix<Inkscape::XML::NodeConstParentIterator>(
+ a, b, nullptr, &same_repr);
+ bool OK = false;
+ if (ancestor) {
+ if (ancestor->type() != Inkscape::XML::DOCUMENT_NODE) {
+ OK = true;
+ }
+ }
+ if ( OK ) {
+ return ancestor;
+ } else {
+ return nullptr;
+ }
+}
+
+Inkscape::XML::Node *LCA(Inkscape::XML::Node *a, Inkscape::XML::Node *b)
+{
+ Inkscape::XML::Node const *tmp = LCA(const_cast<Inkscape::XML::Node const *>(a), const_cast<Inkscape::XML::Node const *>(b));
+ return const_cast<Inkscape::XML::Node *>(tmp);
+}
+
+Inkscape::XML::Node const *AncetreFils(Inkscape::XML::Node const *descendent, Inkscape::XML::Node const *ancestor)
+{
+ Inkscape::XML::Node const *result = nullptr;
+ if ( descendent && ancestor ) {
+ if (descendent->parent() == ancestor) {
+ result = descendent;
+ } else {
+ result = AncetreFils(descendent->parent(), ancestor);
+ }
+ }
+ return result;
+}
+
+Inkscape::XML::Node *AncetreFils(Inkscape::XML::Node *descendent, Inkscape::XML::Node *ancestor)
+{
+ Inkscape::XML::Node const * tmp = AncetreFils(const_cast<Inkscape::XML::Node const*>(descendent), const_cast<Inkscape::XML::Node const*>(ancestor));
+ return const_cast<Inkscape::XML::Node *>(tmp);
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/repr-sorting.h b/src/xml/repr-sorting.h
new file mode 100644
index 0000000..c111623
--- /dev/null
+++ b/src/xml/repr-sorting.h
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * TODO: insert short description here
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2011 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+/** @file
+ * @brief Some functions relevant sorting reprs by position within document.
+ * @todo Functions in this file have non-English names. Determine what they do and rename
+ * accordingly.
+ */
+
+#ifndef SEEN_XML_REPR_SORTING_H
+#define SEEN_XML_REPR_SORTING_H
+
+namespace Inkscape {
+namespace XML {
+
+class Node;
+
+} // namespace XML
+} // namespace Inkscape
+
+
+Inkscape::XML::Node *LCA(Inkscape::XML::Node *a, Inkscape::XML::Node *b);
+Inkscape::XML::Node const *LCA(Inkscape::XML::Node const *a, Inkscape::XML::Node const *b);
+
+/**
+ * Returns a child of \a ancestor such that ret is itself an ancestor of \a descendent.
+ *
+ * The current version returns NULL if ancestor or descendent is NULL, though future versions may
+ * call g_log. Please update this comment if you rely on the current behaviour.
+ */
+Inkscape::XML::Node const *AncetreFils(Inkscape::XML::Node const *descendent, Inkscape::XML::Node const *ancestor);
+
+Inkscape::XML::Node *AncetreFils(Inkscape::XML::Node *descendent, Inkscape::XML::Node *ancestor);
+
+#endif // SEEN_XML_REPR_SOTRING_H
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/repr-util.cpp b/src/xml/repr-util.cpp
new file mode 100644
index 0000000..4ecc4a7
--- /dev/null
+++ b/src/xml/repr-util.cpp
@@ -0,0 +1,661 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Miscellaneous helpers for reprs.
+ */
+
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@ximian.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 1999-2000 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ * g++ port Copyright (C) 2003 Nathan Hurst
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <cstring>
+#include <cstdlib>
+
+
+#include <glib.h>
+#include <glibmm.h>
+
+#include <2geom/point.h>
+#include "svg/stringstream.h"
+#include "svg/css-ostringstream.h"
+#include "svg/svg-length.h"
+
+#include "xml/repr.h"
+#include "xml/repr-sorting.h"
+
+
+#define OSB_NS_URI "http://www.openswatchbook.org/uri/2009/osb"
+
+
+struct SPXMLNs {
+ SPXMLNs *next;
+ unsigned int uri, prefix;
+};
+
+/*#####################
+# DEFINITIONS
+#####################*/
+
+#ifndef FALSE
+# define FALSE 0
+#endif
+
+#ifndef TRUE
+# define TRUE (!FALSE)
+#endif
+
+#ifndef MAX
+# define MAX(a,b) (((a) < (b)) ? (b) : (a))
+#endif
+
+/*#####################
+# FORWARD DECLARATIONS
+#####################*/
+
+static void sp_xml_ns_register_defaults();
+static char *sp_xml_ns_auto_prefix(char const *uri);
+
+/*#####################
+# MAIN
+#####################*/
+
+/**
+ * SPXMLNs
+ */
+
+static SPXMLNs *namespaces=nullptr;
+
+/*
+ * There are the prefixes to use for the XML namespaces defined
+ * in repr.h
+ */
+static void sp_xml_ns_register_defaults()
+{
+ static SPXMLNs defaults[11];
+
+ defaults[0].uri = g_quark_from_static_string(SP_SODIPODI_NS_URI);
+ defaults[0].prefix = g_quark_from_static_string("sodipodi");
+ defaults[0].next = &defaults[1];
+
+ defaults[1].uri = g_quark_from_static_string(SP_XLINK_NS_URI);
+ defaults[1].prefix = g_quark_from_static_string("xlink");
+ defaults[1].next = &defaults[2];
+
+ defaults[2].uri = g_quark_from_static_string(SP_SVG_NS_URI);
+ defaults[2].prefix = g_quark_from_static_string("svg");
+ defaults[2].next = &defaults[3];
+
+ defaults[3].uri = g_quark_from_static_string(SP_INKSCAPE_NS_URI);
+ defaults[3].prefix = g_quark_from_static_string("inkscape");
+ defaults[3].next = &defaults[4];
+
+ defaults[4].uri = g_quark_from_static_string(SP_RDF_NS_URI);
+ defaults[4].prefix = g_quark_from_static_string("rdf");
+ defaults[4].next = &defaults[5];
+
+ defaults[5].uri = g_quark_from_static_string(SP_CC_NS_URI);
+ defaults[5].prefix = g_quark_from_static_string("cc");
+ defaults[5].next = &defaults[6];
+
+ defaults[6].uri = g_quark_from_static_string(SP_DC_NS_URI);
+ defaults[6].prefix = g_quark_from_static_string("dc");
+ defaults[6].next = &defaults[7];
+
+ defaults[7].uri = g_quark_from_static_string(OSB_NS_URI);
+ defaults[7].prefix = g_quark_from_static_string("osb");
+ defaults[7].next = &defaults[8];
+
+ // Inkscape versions prior to 0.44 would write this namespace
+ // URI instead of the correct sodipodi namespace; by adding this
+ // entry to the table last (where it gets used for URI -> prefix
+ // lookups, but not prefix -> URI lookups), we effectively transfer
+ // elements in this namespace to the correct sodipodi namespace:
+
+ defaults[8].uri = g_quark_from_static_string(SP_BROKEN_SODIPODI_NS_URI);
+ defaults[8].prefix = g_quark_from_static_string("sodipodi");
+ defaults[8].next = &defaults[9];
+
+ // "Duck prion"
+ // This URL became widespread due to a bug in versions <= 0.43
+
+ defaults[9].uri = g_quark_from_static_string("http://inkscape.sourceforge.net/DTD/s odipodi-0.dtd");
+ defaults[9].prefix = g_quark_from_static_string("sodipodi");
+ defaults[9].next = &defaults[10];
+
+ // This namespace URI is being phased out by Creative Commons
+
+ defaults[10].uri = g_quark_from_static_string(SP_OLD_CC_NS_URI);
+ defaults[10].prefix = g_quark_from_static_string("cc");
+ defaults[10].next = nullptr;
+
+ namespaces = &defaults[0];
+}
+
+char *sp_xml_ns_auto_prefix(char const *uri)
+{
+ char const *start, *end;
+ char *new_prefix;
+ start = uri;
+ while ((end = strpbrk(start, ":/"))) {
+ start = end + 1;
+ }
+ end = start + strspn(start, "abcdefghijklmnopqrstuvwxyz");
+ if (end == start) {
+ start = "ns";
+ end = start + 2;
+ }
+ new_prefix = g_strndup(start, end - start);
+ if (sp_xml_ns_prefix_uri(new_prefix)) {
+ char *temp;
+ int counter=0;
+ do {
+ temp = g_strdup_printf("%s%d", new_prefix, counter++);
+ } while (sp_xml_ns_prefix_uri(temp));
+ g_free(new_prefix);
+ new_prefix = temp;
+ }
+ return new_prefix;
+}
+
+gchar const *sp_xml_ns_uri_prefix(gchar const *uri, gchar const *suggested)
+{
+ char const *prefix;
+
+ if (!uri) return nullptr;
+
+ if (!namespaces) {
+ sp_xml_ns_register_defaults();
+ }
+
+ GQuark const key = g_quark_from_string(uri);
+ prefix = nullptr;
+ for ( SPXMLNs *iter=namespaces ; iter ; iter = iter->next ) {
+ if ( iter->uri == key ) {
+ prefix = g_quark_to_string(iter->prefix);
+ break;
+ }
+ }
+
+ if (!prefix) {
+ char *new_prefix;
+ SPXMLNs *ns;
+ if (suggested) {
+ GQuark const prefix_key=g_quark_from_string(suggested);
+
+ SPXMLNs *found=namespaces;
+ while (found) {
+ if (found->prefix != prefix_key) {
+ found = found->next;
+ }
+ else {
+ break;
+ }
+ }
+
+ if (found) { // prefix already used?
+ new_prefix = sp_xml_ns_auto_prefix(uri);
+ } else { // safe to use suggested
+ new_prefix = g_strdup(suggested);
+ }
+ } else {
+ new_prefix = sp_xml_ns_auto_prefix(uri);
+ }
+
+ ns = g_new(SPXMLNs, 1);
+ g_assert( ns != nullptr );
+ ns->uri = g_quark_from_string(uri);
+ ns->prefix = g_quark_from_string(new_prefix);
+
+ g_free(new_prefix);
+
+ ns->next = namespaces;
+ namespaces = ns;
+
+ prefix = g_quark_to_string(ns->prefix);
+ }
+
+ return prefix;
+}
+
+gchar const *sp_xml_ns_prefix_uri(gchar const *prefix)
+{
+ SPXMLNs *iter;
+ char const *uri;
+
+ if (!prefix) return nullptr;
+
+ if (!namespaces) {
+ sp_xml_ns_register_defaults();
+ }
+
+ GQuark const key = g_quark_from_string(prefix);
+ uri = nullptr;
+ for ( iter = namespaces ; iter ; iter = iter->next ) {
+ if ( iter->prefix == key ) {
+ uri = g_quark_to_string(iter->uri);
+ break;
+ }
+ }
+ return uri;
+}
+
+/**
+ * Works for different-parent objects, so long as they have a common ancestor. Return value:
+ * 0 positions are equivalent
+ * 1 first object's position is greater than the second
+ * -1 first object's position is less than the second
+ * @todo Rewrite this function's description to be understandable
+ */
+int sp_repr_compare_position(Inkscape::XML::Node const *first, Inkscape::XML::Node const *second)
+{
+ int p1, p2;
+ if (first->parent() == second->parent()) {
+ /* Basic case - first and second have same parent */
+ p1 = first->position();
+ p2 = second->position();
+ } else {
+ /* Special case - the two objects have different parents. They
+ could be in different groups or on different layers for
+ instance. */
+
+ // Find the lowest common ancestor(LCA)
+ Inkscape::XML::Node const *ancestor = LCA(first, second);
+ g_assert(ancestor != nullptr);
+
+ if (ancestor == first) {
+ return 1;
+ } else if (ancestor == second) {
+ return -1;
+ } else {
+ Inkscape::XML::Node const *to_first = AncetreFils(first, ancestor);
+ Inkscape::XML::Node const *to_second = AncetreFils(second, ancestor);
+ g_assert(to_second->parent() == to_first->parent());
+ p1 = to_first->position();
+ p2 = to_second->position();
+ }
+ }
+
+ if (p1 > p2) return 1;
+ if (p1 < p2) return -1;
+ return 0;
+
+ /* effic: Assuming that the parent--child relationship is consistent
+ (i.e. that the parent really does contain first and second among
+ its list of children), it should be equivalent to walk along the
+ children and see which we encounter first (returning 0 iff first
+ == second).
+
+ Given that this function is used solely for sorting, we can use a
+ similar approach to do the sort: gather the things to be sorted,
+ into an STL vector (to allow random access and faster
+ traversals). Do a single pass of the parent's children; for each
+ child, do a pass on whatever items in the vector we haven't yet
+ encountered. If the child is found, then swap it to the
+ beginning of the yet-unencountered elements of the vector.
+ Continue until no more than one remains unencountered. --
+ pjrm */
+}
+
+bool sp_repr_compare_position_bool(Inkscape::XML::Node const *first, Inkscape::XML::Node const *second){
+ return sp_repr_compare_position(first, second)<0;
+}
+
+
+/**
+ * Find an element node using an unique attribute.
+ *
+ * This function returns the first child of the specified node that has the attribute
+ * @c key equal to @c value. Note that this function does not recurse.
+ *
+ * @param repr The node to start from
+ * @param key The name of the attribute to use for comparisons
+ * @param value The value of the attribute to look for
+ * @relatesalso Inkscape::XML::Node
+ */
+Inkscape::XML::Node *sp_repr_lookup_child(Inkscape::XML::Node *repr,
+ gchar const *key,
+ gchar const *value)
+{
+ g_return_val_if_fail(repr != nullptr, NULL);
+ for ( Inkscape::XML::Node *child = repr->firstChild() ; child ; child = child->next() ) {
+ gchar const *child_value = child->attribute(key);
+ if ( (child_value == value) ||
+ (value && child_value && !strcmp(child_value, value)) )
+ {
+ return child;
+ }
+ }
+ return nullptr;
+}
+
+/**
+ * Recursive version of sp_repr_lookup_child().
+ */
+Inkscape::XML::Node const *sp_repr_lookup_descendant(Inkscape::XML::Node const *repr,
+ gchar const *key,
+ gchar const *value)
+{
+ Inkscape::XML::Node const *found = nullptr;
+ g_return_val_if_fail(repr != nullptr, NULL);
+ gchar const *repr_value = repr->attribute(key);
+ if ( (repr_value == value) ||
+ (repr_value && value && strcmp(repr_value, value) == 0) ) {
+ found = repr;
+ } else {
+ for (Inkscape::XML::Node const *child = repr->firstChild() ; child && !found; child = child->next() ) {
+ found = sp_repr_lookup_descendant( child, key, value );
+ }
+ }
+ return found;
+}
+
+
+Inkscape::XML::Node *sp_repr_lookup_descendant(Inkscape::XML::Node *repr,
+ gchar const *key,
+ gchar const *value)
+{
+ Inkscape::XML::Node const *found = sp_repr_lookup_descendant( const_cast<Inkscape::XML::Node const *>(repr), key, value );
+ return const_cast<Inkscape::XML::Node *>(found);
+}
+
+Inkscape::XML::Node const *sp_repr_lookup_name( Inkscape::XML::Node const *repr, gchar const *name, gint maxdepth )
+{
+ Inkscape::XML::Node const *found = nullptr;
+ g_return_val_if_fail(repr != nullptr, NULL);
+ g_return_val_if_fail(name != nullptr, NULL);
+
+ GQuark const quark = g_quark_from_string(name);
+
+ if ( (GQuark)repr->code() == quark ) {
+ found = repr;
+ } else if ( maxdepth != 0 ) {
+ // maxdepth == -1 means unlimited
+ if ( maxdepth == -1 ) {
+ maxdepth = 0;
+ }
+
+ for (Inkscape::XML::Node const *child = repr->firstChild() ; child && !found; child = child->next() ) {
+ found = sp_repr_lookup_name( child, name, maxdepth - 1 );
+ }
+ }
+ return found;
+}
+
+Inkscape::XML::Node *sp_repr_lookup_name( Inkscape::XML::Node *repr, gchar const *name, gint maxdepth )
+{
+ Inkscape::XML::Node const *found = sp_repr_lookup_name( const_cast<Inkscape::XML::Node const *>(repr), name, maxdepth );
+ return const_cast<Inkscape::XML::Node *>(found);
+}
+
+std::vector<Inkscape::XML::Node const *> sp_repr_lookup_name_many( Inkscape::XML::Node const *repr, gchar const *name, gint maxdepth )
+{
+ std::vector<Inkscape::XML::Node const *> nodes;
+ std::vector<Inkscape::XML::Node const *> found;
+ g_return_val_if_fail(repr != nullptr, nodes);
+ g_return_val_if_fail(name != nullptr, nodes);
+
+ GQuark const quark = g_quark_from_string(name);
+
+ if ( (GQuark)repr->code() == quark ) {
+ nodes.push_back(repr);
+ }
+
+ if ( maxdepth != 0 ) {
+ // maxdepth == -1 means unlimited
+ if ( maxdepth == -1 ) {
+ maxdepth = 0;
+ }
+
+ for (Inkscape::XML::Node const *child = repr->firstChild() ; child; child = child->next() ) {
+ found = sp_repr_lookup_name_many( child, name, maxdepth - 1);
+ nodes.insert(nodes.end(), found.begin(), found.end());
+ }
+ }
+
+ return nodes;
+}
+
+std::vector<Inkscape::XML::Node *>
+sp_repr_lookup_property_many( Inkscape::XML::Node *repr, Glib::ustring const& property,
+ Glib::ustring const &value, int maxdepth )
+{
+ std::vector<Inkscape::XML::Node *> nodes;
+ std::vector<Inkscape::XML::Node *> found;
+ g_return_val_if_fail(repr != nullptr, nodes);
+
+ SPCSSAttr* css = sp_repr_css_attr (repr, "style");
+ if (value == sp_repr_css_property (css, property, "")) {
+ nodes.push_back(repr);
+ }
+
+ if ( maxdepth != 0 ) {
+ // maxdepth == -1 means unlimited
+ if ( maxdepth == -1 ) {
+ maxdepth = 0;
+ }
+
+ for (Inkscape::XML::Node *child = repr->firstChild() ; child; child = child->next() ) {
+ found = sp_repr_lookup_property_many( child, property, value, maxdepth - 1);
+ nodes.insert(nodes.end(), found.begin(), found.end());
+ }
+ }
+
+ return nodes;
+}
+
+/**
+ * Determine if the node is a 'title', 'desc' or 'metadata' element.
+ */
+bool sp_repr_is_meta_element(const Inkscape::XML::Node *node)
+{
+ if (node == nullptr) return false;
+ if (node->type() != Inkscape::XML::ELEMENT_NODE) return false;
+ gchar const *name = node->name();
+ if (name == nullptr) return false;
+ if (!std::strcmp(name, "svg:title")) return true;
+ if (!std::strcmp(name, "svg:desc")) return true;
+ if (!std::strcmp(name, "svg:metadata")) return true;
+ return false;
+}
+
+/**
+ * Parses the boolean value of an attribute "key" in repr and sets val accordingly, or to FALSE if
+ * the attr is not set.
+ *
+ * \return TRUE if the attr was set, FALSE otherwise.
+ */
+unsigned int sp_repr_get_boolean(Inkscape::XML::Node *repr, gchar const *key, unsigned int *val)
+{
+ gchar const *v;
+
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+ g_return_val_if_fail(val != nullptr, FALSE);
+
+ v = repr->attribute(key);
+
+ if (v != nullptr) {
+ if (!g_ascii_strcasecmp(v, "true") ||
+ !g_ascii_strcasecmp(v, "yes" ) ||
+ !g_ascii_strcasecmp(v, "y" ) ||
+ (atoi(v) != 0)) {
+ *val = TRUE;
+ } else {
+ *val = FALSE;
+ }
+ return TRUE;
+ } else {
+ *val = FALSE;
+ return FALSE;
+ }
+}
+
+unsigned int sp_repr_get_int(Inkscape::XML::Node *repr, gchar const *key, int *val)
+{
+ gchar const *v;
+
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+ g_return_val_if_fail(val != nullptr, FALSE);
+
+ v = repr->attribute(key);
+
+ if (v != nullptr) {
+ *val = atoi(v);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+unsigned int sp_repr_get_double(Inkscape::XML::Node *repr, gchar const *key, double *val)
+{
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+ g_return_val_if_fail(val != nullptr, FALSE);
+
+ gchar const *v = repr->attribute(key);
+
+ if (v != nullptr) {
+ *val = g_ascii_strtod(v, nullptr);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+unsigned int sp_repr_set_boolean(Inkscape::XML::Node *repr, gchar const *key, unsigned int val)
+{
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+
+ repr->setAttribute(key, (val) ? "true" : "false");
+ return true;
+}
+
+unsigned int sp_repr_set_int(Inkscape::XML::Node *repr, gchar const *key, int val)
+{
+ gchar c[32];
+
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+
+ g_snprintf(c, 32, "%d", val);
+
+ repr->setAttribute(key, c);
+ return true;
+}
+
+/**
+ * Set a property attribute to \a val [slightly rounded], in the format
+ * required for CSS properties: in particular, it never uses exponent
+ * notation.
+ */
+unsigned int sp_repr_set_css_double(Inkscape::XML::Node *repr, gchar const *key, double val)
+{
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+
+ Inkscape::CSSOStringStream os;
+ os << val;
+
+ repr->setAttribute(key, os.str());
+ return true;
+}
+
+/**
+ * For attributes where an exponent is allowed.
+ *
+ * Not suitable for property attributes (fill-opacity, font-size etc.).
+ */
+unsigned int sp_repr_set_svg_double(Inkscape::XML::Node *repr, gchar const *key, double val)
+{
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+ g_return_val_if_fail(val==val, FALSE);//tests for nan
+
+ Inkscape::SVGOStringStream os;
+ os << val;
+
+ repr->setAttribute(key, os.str());
+ return true;
+}
+
+unsigned int sp_repr_set_svg_non_default_double(Inkscape::XML::Node *repr, gchar const *key, double val, double default_value)
+{
+ if (val==default_value){
+ repr->removeAttribute(key);
+ return true;
+ }
+ return sp_repr_set_svg_double(repr, key, val);
+}
+
+/**
+ * For attributes where an exponent is allowed.
+ *
+ * Not suitable for property attributes.
+ */
+unsigned int sp_repr_set_svg_length(Inkscape::XML::Node *repr, gchar const *key, SVGLength &val)
+{
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+
+ repr->setAttribute(key, val.write());
+ return true;
+}
+
+unsigned sp_repr_set_point(Inkscape::XML::Node *repr, gchar const *key, Geom::Point const & val)
+{
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+
+ Inkscape::SVGOStringStream os;
+ os << val[Geom::X] << "," << val[Geom::Y];
+
+ repr->setAttribute(key, os.str());
+ return true;
+}
+
+unsigned int sp_repr_get_point(Inkscape::XML::Node *repr, gchar const *key, Geom::Point *val)
+{
+ g_return_val_if_fail(repr != nullptr, FALSE);
+ g_return_val_if_fail(key != nullptr, FALSE);
+ g_return_val_if_fail(val != nullptr, FALSE);
+
+ gchar const *v = repr->attribute(key);
+
+ g_return_val_if_fail(v != nullptr, FALSE);
+
+ gchar ** strarray = g_strsplit(v, ",", 2);
+
+ if (strarray && strarray[0] && strarray[1]) {
+ double newx, newy;
+ newx = g_ascii_strtod(strarray[0], nullptr);
+ newy = g_ascii_strtod(strarray[1], nullptr);
+ g_strfreev (strarray);
+ *val = Geom::Point(newx, newy);
+ return TRUE;
+ }
+
+ g_strfreev (strarray);
+ return FALSE;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/repr.cpp b/src/xml/repr.cpp
new file mode 100644
index 0000000..8988586
--- /dev/null
+++ b/src/xml/repr.cpp
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * A few non-inline functions of the C facade to Inkscape::XML::Node.
+ */
+
+/*
+ * Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * MenTaLguY <mental@rydia.net>
+ *
+ * Copyright (C) 1999-2003 authors
+ * Copyright (C) 2000-2002 Ximian, Inc.
+ * g++ port Copyright (C) 2003 Nathan Hurst
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#define noREPR_VERBOSE
+
+#include <cstring>
+
+#include "xml/repr.h"
+#include "xml/text-node.h"
+#include "xml/element-node.h"
+#include "xml/comment-node.h"
+#include "xml/simple-document.h"
+
+using Inkscape::Util::share_string;
+
+/// Returns new document having as first child a node named rootname.
+Inkscape::XML::Document *
+sp_repr_document_new(char const *rootname)
+{
+ Inkscape::XML::Document *doc = new Inkscape::XML::SimpleDocument();
+ if (!strcmp(rootname, "svg:svg")) {
+ doc->setAttribute("version", "1.0");
+ doc->setAttribute("standalone", "no");
+ Inkscape::XML::Node *comment = doc->createComment(" Created with Inkscape (http://www.inkscape.org/) ");
+ doc->appendChild(comment);
+ Inkscape::GC::release(comment);
+ }
+
+ Inkscape::XML::Node *root = doc->createElement(rootname);
+ doc->appendChild(root);
+ Inkscape::GC::release(root);
+
+ return doc;
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
diff --git a/src/xml/repr.h b/src/xml/repr.h
new file mode 100644
index 0000000..68a857b
--- /dev/null
+++ b/src/xml/repr.h
@@ -0,0 +1,246 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief C facade to Inkscape::XML::Node
+ */
+/* Authors:
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 1999-2002 authors
+ * Copyright (C) 2000-2002 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_SP_REPR_H
+#define SEEN_SP_REPR_H
+
+#include <vector>
+#include <glibmm/quark.h>
+
+#include "xml/node.h"
+#include "xml/document.h"
+
+#define SP_SODIPODI_NS_URI "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+#define SP_BROKEN_SODIPODI_NS_URI "http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd"
+#define SP_INKSCAPE_NS_URI "http://www.inkscape.org/namespaces/inkscape"
+#define SP_XLINK_NS_URI "http://www.w3.org/1999/xlink"
+#define SP_SVG_NS_URI "http://www.w3.org/2000/svg"
+#define SP_RDF_NS_URI "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+#define SP_CC_NS_URI "http://creativecommons.org/ns#"
+#define SP_OLD_CC_NS_URI "http://web.resource.org/cc/"
+#define SP_DC_NS_URI "http://purl.org/dc/elements/1.1/"
+
+class SPCSSAttr;
+class SVGLength;
+
+namespace Inkscape {
+namespace IO {
+class Writer;
+} // namespace IO
+} // namespace Inkscape
+
+namespace Geom {
+class Point;
+}
+
+/* SPXMLNs */
+char const *sp_xml_ns_uri_prefix(char const *uri, char const *suggested);
+char const *sp_xml_ns_prefix_uri(char const *prefix);
+
+Inkscape::XML::Document *sp_repr_document_new(char const *rootname);
+
+/* IO */
+
+Inkscape::XML::Document *sp_repr_read_file(char const *filename, char const *default_ns);
+Inkscape::XML::Document *sp_repr_read_mem(char const *buffer, int length, char const *default_ns);
+void sp_repr_write_stream(Inkscape::XML::Node *repr, Inkscape::IO::Writer &out,
+ int indent_level, bool add_whitespace, Glib::QueryQuark elide_prefix,
+ int inlineattrs, int indent,
+ char const *old_href_base = nullptr,
+ char const *new_href_base = nullptr);
+Inkscape::XML::Document *sp_repr_read_buf (const Glib::ustring &buf, const char *default_ns);
+Glib::ustring sp_repr_save_buf(Inkscape::XML::Document *doc);
+
+// TODO convert to std::string
+void sp_repr_save_stream(Inkscape::XML::Document *doc, FILE *to_file,
+ char const *default_ns = nullptr, bool compress = false,
+ char const *old_href_base = nullptr,
+ char const *new_href_base = nullptr);
+
+bool sp_repr_save_file(Inkscape::XML::Document *doc, char const *filename, char const *default_ns=nullptr);
+bool sp_repr_save_rebased_file(Inkscape::XML::Document *doc, char const *filename_utf8,
+ char const *default_ns,
+ char const *old_base, char const *new_base_filename);
+
+
+/* CSS stuff */
+
+SPCSSAttr *sp_repr_css_attr_new();
+void sp_repr_css_attr_unref(SPCSSAttr *css);
+SPCSSAttr *sp_repr_css_attr(Inkscape::XML::Node const *repr, char const *attr);
+SPCSSAttr *sp_repr_css_attr_parse_color_to_fill(const Glib::ustring &text);
+SPCSSAttr *sp_repr_css_attr_inherited(Inkscape::XML::Node const *repr, char const *attr);
+SPCSSAttr *sp_repr_css_attr_unset_all(SPCSSAttr *css);
+
+char const *sp_repr_css_property(SPCSSAttr *css, char const *name, char const *defval);
+Glib::ustring sp_repr_css_property(SPCSSAttr *css, Glib::ustring const &name, Glib::ustring const &defval);
+void sp_repr_css_set_property(SPCSSAttr *css, char const *name, char const *value);
+void sp_repr_css_unset_property(SPCSSAttr *css, char const *name);
+bool sp_repr_css_property_is_unset(SPCSSAttr *css, char const *name);
+double sp_repr_css_double_property(SPCSSAttr *css, char const *name, double defval);
+
+void sp_repr_css_write_string(SPCSSAttr *css, Glib::ustring &str);
+void sp_repr_css_set(Inkscape::XML::Node *repr, SPCSSAttr *css, char const *key);
+void sp_repr_css_merge(SPCSSAttr *dst, SPCSSAttr *src);
+void sp_repr_css_attr_add_from_string(SPCSSAttr *css, const char *data);
+void sp_repr_css_change(Inkscape::XML::Node *repr, SPCSSAttr *css, char const *key);
+void sp_repr_css_change_recursive(Inkscape::XML::Node *repr, SPCSSAttr *css, char const *key);
+void sp_repr_css_print(SPCSSAttr *css);
+
+/* Utility finctions */
+/// Remove \a repr from children of its parent node.
+inline void sp_repr_unparent(Inkscape::XML::Node *repr) {
+ if (repr) {
+ Inkscape::XML::Node *parent=repr->parent();
+ if (parent) {
+ parent->removeChild(repr);
+ }
+ }
+}
+
+bool sp_repr_is_meta_element(const Inkscape::XML::Node *node);
+
+/* Convenience */
+unsigned sp_repr_get_boolean(Inkscape::XML::Node *repr, char const *key, unsigned *val);
+unsigned sp_repr_get_int(Inkscape::XML::Node *repr, char const *key, int *val);
+unsigned sp_repr_get_double(Inkscape::XML::Node *repr, char const *key, double *val);
+unsigned sp_repr_set_boolean(Inkscape::XML::Node *repr, char const *key, unsigned val);
+unsigned sp_repr_set_int(Inkscape::XML::Node *repr, char const *key, int val);
+unsigned sp_repr_set_css_double(Inkscape::XML::Node *repr, char const *key, double val);
+unsigned sp_repr_set_svg_double(Inkscape::XML::Node *repr, char const *key, double val);
+unsigned sp_repr_set_svg_length(Inkscape::XML::Node *repr, char const *key, SVGLength &val);
+unsigned sp_repr_set_point(Inkscape::XML::Node *repr, char const *key, Geom::Point const & val);
+unsigned sp_repr_get_point(Inkscape::XML::Node *repr, char const *key, Geom::Point *val);
+
+//c++-style comparison : returns (bool)(a<b)
+int sp_repr_compare_position(Inkscape::XML::Node const *first, Inkscape::XML::Node const *second);
+bool sp_repr_compare_position_bool(Inkscape::XML::Node const *first, Inkscape::XML::Node const *second);
+
+// Searching
+/**
+ * @brief Find an element node with the given name.
+ *
+ * This function searches the descendants of the specified node depth-first for
+ * the first XML node with the specified name.
+ *
+ * @param repr The node to start from
+ * @param name The name of the element node to find
+ * @param maxdepth Maximum search depth, or -1 for an unlimited depth
+ * @return A pointer to the matching Inkscape::XML::Node
+ * @relatesalso Inkscape::XML::Node
+ */
+Inkscape::XML::Node *sp_repr_lookup_name(Inkscape::XML::Node *repr,
+ char const *name,
+ int maxdepth = -1);
+
+Inkscape::XML::Node const *sp_repr_lookup_name(Inkscape::XML::Node const *repr,
+ char const *name,
+ int maxdepth = -1);
+
+std::vector<Inkscape::XML::Node const *> sp_repr_lookup_name_many(Inkscape::XML::Node const *repr,
+ char const *name,
+ int maxdepth = -1);
+
+// Find an element node using an unique attribute.
+Inkscape::XML::Node *sp_repr_lookup_child(Inkscape::XML::Node *repr,
+ char const *key,
+ char const *value);
+
+// Find an element node using an unique attribute recursively.
+Inkscape::XML::Node *sp_repr_lookup_descendant(Inkscape::XML::Node *repr,
+ char const *key,
+ char const *value);
+
+Inkscape::XML::Node const *sp_repr_lookup_descendant(Inkscape::XML::Node const *repr,
+ char const *key,
+ char const *value);
+
+// Find element nodes using a property value.
+std::vector<Inkscape::XML::Node *> sp_repr_lookup_property_many(Inkscape::XML::Node *repr,
+ Glib::ustring const &property,
+ Glib::ustring const &value,
+ int maxdepth = -1);
+
+inline Inkscape::XML::Node *sp_repr_document_first_child(Inkscape::XML::Document const *doc) {
+ return const_cast<Inkscape::XML::Node *>(doc->firstChild());
+}
+
+inline bool sp_repr_is_def(Inkscape::XML::Node const *node) {
+ return node->parent() != nullptr &&
+ node->parent()->name() != nullptr &&
+ strcmp("svg:defs", node->parent()->name()) == 0;
+}
+
+inline bool sp_repr_is_layer(Inkscape::XML::Node const *node) {
+ return node->attribute("inkscape:groupmode") != nullptr &&
+ strcmp("layer", node->attribute("inkscape:groupmode")) == 0;
+}
+
+/**
+ * @brief Visit all descendants recursively.
+ *
+ * Traverse all descendants of node and call visitor on it.
+ * Stop descending when visitor returns false
+ *
+ * @param node The root node to start visiting
+ * @param visitor The visitor lambda (Node *) -> bool
+ * If visitor returns false child nodes of current node are not visited.
+ * @relatesalso Inkscape::XML::Node
+ */
+template <typename Visitor>
+void sp_repr_visit_descendants(Inkscape::XML::Node *node, Visitor visitor) {
+ if (!visitor(node)) {
+ return;
+ }
+ for (Inkscape::XML::Node *child = node->firstChild();
+ child != nullptr;
+ child = child->next()) {
+ sp_repr_visit_descendants(child, visitor);
+ }
+}
+
+/**
+ * @brief Visit descendants of 2 nodes in parallel.
+ * The assumption is that one a and b trees are the same in terms of structure (like one is
+ * a duplicate of the other).
+ *
+ * @param a first node tree root
+ * @param b second node tree root
+ * @param visitor The visitor lambda (Node *, Node *) -> bool
+ * If visitor returns false child nodes are not visited.
+ * @relatesalso Inkscape::XML::Node
+ */
+template <typename Visitor>
+void sp_repr_visit_descendants(Inkscape::XML::Node *a, Inkscape::XML::Node *b, Visitor visitor) {
+ if (!visitor(a, b)) {
+ return;
+ }
+ for (Inkscape::XML::Node *ac = a->firstChild(), *bc = b->firstChild();
+ ac != nullptr && bc != nullptr;
+ ac = ac->next(), bc = bc->next()) {
+ sp_repr_visit_descendants(ac, bc, visitor);
+ }
+}
+
+#endif // SEEN_SP_REPR_H
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/simple-document.cpp b/src/xml/simple-document.cpp
new file mode 100644
index 0000000..c1227ac
--- /dev/null
+++ b/src/xml/simple-document.cpp
@@ -0,0 +1,137 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Garbage collected XML document implementation.
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright 2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <glib.h> // g_assert()
+
+#include "xml/simple-document.h"
+#include "xml/event-fns.h"
+#include "xml/element-node.h"
+#include "xml/text-node.h"
+#include "xml/comment-node.h"
+#include "xml/pi-node.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+void SimpleDocument::beginTransaction() {
+ g_assert(!_in_transaction);
+ _in_transaction = true;
+}
+
+void SimpleDocument::rollback() {
+ g_assert(_in_transaction);
+ _in_transaction = false;
+ Event *log = _log_builder.detach();
+ sp_repr_undo_log(log);
+ sp_repr_free_log(log);
+}
+
+void SimpleDocument::commit() {
+ g_assert(_in_transaction);
+ _in_transaction = false;
+ _log_builder.discard();
+}
+
+Inkscape::XML::Event *SimpleDocument::commitUndoable() {
+ g_assert(_in_transaction);
+ _in_transaction = false;
+ return _log_builder.detach();
+}
+
+Node *SimpleDocument::createElement(char const *name) {
+ return new ElementNode(g_quark_from_string(name), this);
+}
+
+Node *SimpleDocument::createTextNode(char const *content) {
+ return new TextNode(Util::share_string(content), this);
+}
+
+Node *SimpleDocument::createTextNode(char const *content, bool const is_CData) {
+ return new TextNode(Util::share_string(content), this, is_CData);
+}
+
+Node *SimpleDocument::createComment(char const *content) {
+ return new CommentNode(Util::share_string(content), this);
+}
+
+Node *SimpleDocument::createPI(char const *target, char const *content) {
+ return new PINode(g_quark_from_string(target), Util::share_string(content), this);
+}
+
+void SimpleDocument::notifyChildAdded(Node &parent,
+ Node &child,
+ Node *prev)
+{
+ if (_in_transaction) {
+ _log_builder.addChild(parent, child, prev);
+ }
+}
+
+void SimpleDocument::notifyChildRemoved(Node &parent,
+ Node &child,
+ Node *prev)
+{
+ if (_in_transaction) {
+ _log_builder.removeChild(parent, child, prev);
+ }
+}
+
+void SimpleDocument::notifyChildOrderChanged(Node &parent,
+ Node &child,
+ Node *old_prev,
+ Node *new_prev)
+{
+ if (_in_transaction) {
+ _log_builder.setChildOrder(parent, child, old_prev, new_prev);
+ }
+}
+
+void SimpleDocument::notifyContentChanged(Node &node,
+ Util::ptr_shared old_content,
+ Util::ptr_shared new_content)
+{
+ if (_in_transaction) {
+ _log_builder.setContent(node, old_content, new_content);
+ }
+}
+
+void SimpleDocument::notifyAttributeChanged(Node &node,
+ GQuark name,
+ Util::ptr_shared old_value,
+ Util::ptr_shared new_value)
+{
+ if (_in_transaction) {
+ _log_builder.setAttribute(node, name, old_value, new_value);
+ }
+}
+
+void SimpleDocument::notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name)
+{
+ if (_in_transaction) {
+ _log_builder.setElementName(node, old_name, new_name);
+ }
+}
+
+} // end namespace XML
+} // end namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/simple-document.h b/src/xml/simple-document.h
new file mode 100644
index 0000000..5dc1750
--- /dev/null
+++ b/src/xml/simple-document.h
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Inkscape::XML::SimpleDocument - generic XML document implementation
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright 2004-2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_SIMPLE_DOCUMENT_H
+#define SEEN_INKSCAPE_XML_SIMPLE_DOCUMENT_H
+
+#include "xml/document.h"
+#include "xml/simple-node.h"
+#include "xml/node-observer.h"
+#include "xml/log-builder.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+class SimpleDocument : public SimpleNode,
+ public Document,
+ public NodeObserver
+{
+public:
+ explicit SimpleDocument()
+ : SimpleNode(g_quark_from_static_string("xml"), this),
+ _in_transaction(false), _is_CData(false) {}
+
+ NodeType type() const override { return Inkscape::XML::DOCUMENT_NODE; }
+
+ bool inTransaction() override { return _in_transaction; }
+
+ void beginTransaction() override;
+ void rollback() override;
+ void commit() override;
+ Inkscape::XML::Event *commitUndoable() override;
+
+ Node *createElement(char const *name) override;
+ Node *createTextNode(char const *content) override;
+ Node *createTextNode(char const *content, bool const is_CData) override;
+ Node *createComment(char const *content) override;
+ Node *createPI(char const *target, char const *content) override;
+
+ void notifyChildAdded(Node &parent, Node &child, Node *prev) override;
+
+ void notifyChildRemoved(Node &parent, Node &child, Node *prev) override;
+
+ void notifyChildOrderChanged(Node &parent, Node &child,
+ Node *old_prev, Node *new_prev) override;
+
+ void notifyContentChanged(Node &node,
+ Util::ptr_shared old_content,
+ Util::ptr_shared new_content) override;
+
+ void notifyAttributeChanged(Node &node, GQuark name,
+ Util::ptr_shared old_value,
+ Util::ptr_shared new_value) override;
+
+ void notifyElementNameChanged(Node& node, GQuark old_name, GQuark new_name) override;
+
+protected:
+ SimpleDocument(SimpleDocument const &doc)
+ : Node(), SimpleNode(doc), Document(), NodeObserver(),
+ _in_transaction(false),
+ _is_CData(false){}
+
+ SimpleNode *_duplicate(Document* /*doc*/) const override
+ {
+ return new SimpleDocument(*this);
+ }
+ NodeObserver *logger() override { return this; }
+
+private:
+ bool _in_transaction;
+ LogBuilder _log_builder;
+ bool _is_CData;
+};
+
+}
+
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/simple-node.cpp b/src/xml/simple-node.cpp
new file mode 100644
index 0000000..d6b01f3
--- /dev/null
+++ b/src/xml/simple-node.cpp
@@ -0,0 +1,825 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Garbage collected XML node implementation
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright 2003-2005 MenTaLguY <mental@rydia.net>
+ * Copyright 2003 Nathan Hurst
+ * Copyright 1999-2003 Lauris Kaplinski
+ * Copyright 2000-2002 Ximian Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <algorithm>
+#include <cstring>
+#include <string>
+
+#include <glib.h>
+
+#include "preferences.h"
+
+#include "xml/simple-node.h"
+#include "xml/node-event-vector.h"
+#include "xml/node-fns.h"
+#include "debug/event-tracker.h"
+#include "debug/simple-event.h"
+#include "util/format.h"
+
+#include "attribute-rel-util.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+namespace {
+
+std::shared_ptr<std::string> stringify_node(Node const &node) {
+ gchar *string;
+ switch (node.type()) {
+ case ELEMENT_NODE: {
+ char const *id=node.attribute("id");
+ if (id) {
+ string = g_strdup_printf("element(%p)=%s(#%s)", &node, node.name(), id);
+ } else {
+ string = g_strdup_printf("element(%p)=%s", &node, node.name());
+ }
+ } break;
+ case TEXT_NODE:
+ string = g_strdup_printf("text(%p)=%s", &node, node.content());
+ break;
+ case COMMENT_NODE:
+ string = g_strdup_printf("comment(%p)=<!--%s-->", &node, node.content());
+ break;
+ case DOCUMENT_NODE:
+ string = g_strdup_printf("document(%p)", &node);
+ break;
+ default:
+ string = g_strdup_printf("unknown(%p)", &node);
+ }
+ std::shared_ptr<std::string> result = std::make_shared<std::string>(string);
+ g_free(string);
+ return result;
+}
+
+typedef Debug::SimpleEvent<Debug::Event::XML> DebugXML;
+
+class DebugXMLNode : public DebugXML {
+public:
+ DebugXMLNode(Node const &node, char const *name)
+ : DebugXML(name)
+ {
+ _addProperty("node", stringify_node(node));
+ }
+};
+
+class DebugAddChild : public DebugXMLNode {
+public:
+ DebugAddChild(Node const &node, Node const &child, Node const *prev)
+ : DebugXMLNode(node, "add-child")
+ {
+ _addProperty("child", stringify_node(child));
+ _addProperty("position", prev ? prev->position() + 1 : 0 );
+ }
+};
+
+class DebugRemoveChild : public DebugXMLNode {
+public:
+ DebugRemoveChild(Node const &node, Node const &child)
+ : DebugXMLNode(node, "remove-child")
+ {
+ _addProperty("child", stringify_node(child));
+ }
+};
+
+class DebugSetChildPosition : public DebugXMLNode {
+public:
+ DebugSetChildPosition(Node const &node, Node const &child,
+ Node const *old_prev, Node const *new_prev)
+ : DebugXMLNode(node, "set-child-position")
+ {
+ _addProperty("child", stringify_node(child));
+
+ unsigned old_position = ( old_prev ? old_prev->position() : 0 );
+ unsigned position = ( new_prev ? new_prev->position() : 0 );
+ if ( position > old_position ) {
+ --position;
+ }
+
+ _addProperty("position", position);
+ }
+};
+
+class DebugSetContent : public DebugXMLNode {
+public:
+ DebugSetContent(Node const &node,
+ Util::ptr_shared content)
+ : DebugXMLNode(node, "set-content")
+ {
+ _addProperty("content", content.pointer());
+ }
+};
+
+class DebugClearContent : public DebugXMLNode {
+public:
+ DebugClearContent(Node const &node)
+ : DebugXMLNode(node, "clear-content")
+ {}
+};
+
+class DebugSetAttribute : public DebugXMLNode {
+public:
+ DebugSetAttribute(Node const &node,
+ GQuark name,
+ Util::ptr_shared value)
+ : DebugXMLNode(node, "set-attribute")
+ {
+ _addProperty("name", g_quark_to_string(name));
+ _addProperty("value", value.pointer());
+ }
+};
+
+class DebugClearAttribute : public DebugXMLNode {
+public:
+ DebugClearAttribute(Node const &node, GQuark name)
+ : DebugXMLNode(node, "clear-attribute")
+ {
+ _addProperty("name", g_quark_to_string(name));
+ }
+};
+
+class DebugSetElementName : public DebugXMLNode {
+public:
+ DebugSetElementName(Node const& node, GQuark name)
+ : DebugXMLNode(node, "set-name")
+ {
+ _addProperty("name", g_quark_to_string(name));
+ }
+};
+
+}
+
+using Util::ptr_shared;
+using Util::share_string;
+using Util::share_unsafe;
+using Util::List;
+using Util::MutableList;
+using Util::cons;
+using Util::rest;
+using Util::set_rest;
+
+SimpleNode::SimpleNode(int code, Document *document)
+: Node(), _name(code), _attributes(), _child_count(0),
+ _cached_positions_valid(false)
+{
+ g_assert(document != nullptr);
+
+ this->_document = document;
+ this->_parent = this->_next = this->_prev = nullptr;
+ this->_first_child = this->_last_child = nullptr;
+
+ _observers.add(_subtree_observers);
+}
+
+SimpleNode::SimpleNode(SimpleNode const &node, Document *document)
+: Node(),
+ _cached_position(node._cached_position),
+ _name(node._name), _attributes(), _content(node._content),
+ _child_count(node._child_count),
+ _cached_positions_valid(node._cached_positions_valid)
+{
+ g_assert(document != nullptr);
+
+ _document = document;
+ _parent = _next = _prev = nullptr;
+ _first_child = _last_child = nullptr;
+
+ for ( SimpleNode *child = node._first_child ;
+ child != nullptr ; child = child->_next )
+ {
+ SimpleNode *child_copy=dynamic_cast<SimpleNode *>(child->duplicate(document));
+
+ child_copy->_setParent(this);
+ if (_last_child) { // not the first iteration
+ _last_child->_next = child_copy;
+ child_copy->_prev = _last_child;
+ } else {
+ _first_child = child_copy;
+ }
+ _last_child = child_copy;
+
+ child_copy->release(); // release to avoid a leak
+ }
+
+ // We need to keep the order of the attributes that we duplicate
+ // and for now, we do that by duplicating the list twice.
+ List<AttributeRecord const> _temp;
+ for ( List<AttributeRecord const> iter = node._attributes ;
+ iter ; ++iter )
+ {
+ _temp = cons(*iter, _temp);
+ }
+ // At this point temp is an up-sidedown list of attributes, put them
+ // back in the right way now.
+ for ( List<AttributeRecord const> iter = _temp ;
+ iter ; ++iter )
+ {
+ _attributes = cons(*iter, _attributes);
+ }
+
+ _observers.add(_subtree_observers);
+}
+
+gchar const *SimpleNode::name() const {
+ return g_quark_to_string(_name);
+}
+
+gchar const *SimpleNode::content() const {
+ return this->_content;
+}
+
+gchar const *SimpleNode::attribute(gchar const *name) const {
+ g_return_val_if_fail(name != nullptr, NULL);
+
+ GQuark const key = g_quark_from_string(name);
+
+ for ( List<AttributeRecord const> iter = _attributes ;
+ iter ; ++iter )
+ {
+ if ( iter->key == key ) {
+ return iter->value;
+ }
+ }
+
+ return nullptr;
+}
+
+unsigned SimpleNode::position() const {
+ g_return_val_if_fail(_parent != nullptr, 0);
+ return _parent->_childPosition(*this);
+}
+
+unsigned SimpleNode::_childPosition(SimpleNode const &child) const {
+ if (!_cached_positions_valid) {
+ unsigned position=0;
+ for ( SimpleNode *sibling = _first_child ;
+ sibling ; sibling = sibling->_next )
+ {
+ sibling->_cached_position = position;
+ position++;
+ }
+ _cached_positions_valid = true;
+ }
+ return child._cached_position;
+}
+
+Node *SimpleNode::nthChild(unsigned index) {
+ SimpleNode *child = _first_child;
+ for ( ; index > 0 && child ; child = child->_next ) {
+ index--;
+ }
+ return child;
+}
+
+bool SimpleNode::matchAttributeName(gchar const *partial_name) const {
+ g_return_val_if_fail(partial_name != nullptr, false);
+
+ for ( List<AttributeRecord const> iter = _attributes ;
+ iter ; ++iter )
+ {
+ gchar const *name = g_quark_to_string(iter->key);
+ if (std::strstr(name, partial_name)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void SimpleNode::_setParent(SimpleNode *parent) {
+ if (_parent) {
+ _subtree_observers.remove(_parent->_subtree_observers);
+ }
+ _parent = parent;
+ if (parent) {
+ _subtree_observers.add(parent->_subtree_observers);
+ }
+}
+
+void SimpleNode::setContent(gchar const *content) {
+ ptr_shared old_content=_content;
+ ptr_shared new_content = ( content ? share_string(content) : ptr_shared() );
+
+ Debug::EventTracker<> tracker;
+ if (new_content) {
+ tracker.set<DebugSetContent>(*this, new_content);
+ } else {
+ tracker.set<DebugClearContent>(*this);
+ }
+
+ _content = new_content;
+
+ if ( _content != old_content ) {
+ _document->logger()->notifyContentChanged(*this, old_content, _content);
+ _observers.notifyContentChanged(*this, old_content, _content);
+ }
+}
+
+void
+SimpleNode::setAttributeImpl(gchar const *name, gchar const *value, bool is_interactive)
+{
+ g_return_if_fail(name && *name);
+
+ // sanity check: `name` must not contain whitespace
+ g_assert(std::none_of(name, name + strlen(name), [](char c) { return g_ascii_isspace(c); }));
+
+ // Check usefulness of attributes on elements in the svg namespace, optionally don't add them to tree.
+ Glib::ustring element = g_quark_to_string(_name);
+ //g_message("setAttribute: %s: %s: %s", element.c_str(), name, value);
+ gchar* cleaned_value = g_strdup( value );
+
+ // Only check elements in SVG name space and don't block setting attribute to NULL.
+ if( element.substr(0,4) == "svg:" && value != nullptr) {
+
+ Inkscape::Preferences *prefs = Inkscape::Preferences::get();
+ if( prefs->getBool("/options/svgoutput/check_on_editing") ) {
+
+ gchar const *id_char = attribute("id");
+ Glib::ustring id = (id_char == nullptr ? "" : id_char );
+ unsigned int flags = sp_attribute_clean_get_prefs();
+ bool attr_warn = flags & SP_ATTR_CLEAN_ATTR_WARN;
+ bool attr_remove = flags & SP_ATTR_CLEAN_ATTR_REMOVE;
+
+ // Check attributes
+ if( (attr_warn || attr_remove) && value != nullptr ) {
+ bool is_useful = sp_attribute_check_attribute( element, id, name, attr_warn );
+ if( !is_useful && attr_remove ) {
+ g_free( cleaned_value );
+ return; // Don't add to tree.
+ }
+ }
+
+ // Check style properties -- Note: if element is not yet inserted into
+ // tree (and thus has no parent), default values will not be tested.
+ if( !strcmp( name, "style" ) && (flags >= SP_ATTR_CLEAN_STYLE_WARN) ) {
+ g_free( cleaned_value );
+ cleaned_value = g_strdup( sp_attribute_clean_style( this, value, flags ).c_str() );
+ // if( g_strcmp0( value, cleaned_value ) ) {
+ // g_warning( "SimpleNode::setAttribute: %s", id.c_str() );
+ // g_warning( " original: %s", value);
+ // g_warning( " cleaned: %s", cleaned_value);
+ // }
+ }
+ }
+ }
+
+ GQuark const key = g_quark_from_string(name);
+
+ MutableList<AttributeRecord> ref;
+ MutableList<AttributeRecord> existing;
+ for ( existing = _attributes ; existing ; ++existing ) {
+ if ( existing->key == key ) {
+ break;
+ }
+ ref = existing;
+ }
+ Debug::EventTracker<> tracker;
+
+ ptr_shared old_value=( existing ? existing->value : ptr_shared() );
+
+ ptr_shared new_value=ptr_shared();
+ if (cleaned_value) {
+ new_value = share_string(cleaned_value);
+ tracker.set<DebugSetAttribute>(*this, key, new_value);
+ if (!existing) {
+ if (ref) {
+ set_rest(ref, MutableList<AttributeRecord>(AttributeRecord(key, new_value)));
+ } else {
+ _attributes = MutableList<AttributeRecord>(AttributeRecord(key, new_value));
+ }
+ } else {
+ existing->value = new_value;
+ }
+ } else {
+ tracker.set<DebugClearAttribute>(*this, key);
+ if (existing) {
+ if (ref) {
+ set_rest(ref, rest(existing));
+ } else {
+ _attributes = rest(existing);
+ }
+ set_rest(existing, MutableList<AttributeRecord>());
+ }
+ }
+
+ if ( new_value != old_value && (!old_value || !new_value || strcmp(old_value, new_value))) {
+ _document->logger()->notifyAttributeChanged(*this, key, old_value, new_value);
+ _observers.notifyAttributeChanged(*this, key, old_value, new_value);
+ //g_warning( "setAttribute notified: %s: %s: %s: %s", name, element.c_str(), old_value, new_value );
+ }
+ g_free( cleaned_value );
+}
+
+void SimpleNode::setCodeUnsafe(int code) {
+ GQuark old_code = static_cast<GQuark>(_name);
+ GQuark new_code = static_cast<GQuark>(code);
+
+ Debug::EventTracker<> tracker;
+ tracker.set<DebugSetElementName>(*this, new_code);
+
+ _name = static_cast<int>(new_code);
+
+ if (new_code != old_code) {
+ _document->logger()->notifyElementNameChanged(*this, old_code, new_code);
+ _observers.notifyElementNameChanged(*this, old_code, new_code);
+ }
+}
+
+void SimpleNode::addChild(Node *generic_child, Node *generic_ref) {
+ g_assert(generic_child);
+ g_assert(generic_child->document() == _document);
+ g_assert(!generic_ref || generic_ref->document() == _document);
+
+ SimpleNode *child=dynamic_cast<SimpleNode *>(generic_child);
+ SimpleNode *ref=dynamic_cast<SimpleNode *>(generic_ref);
+
+ g_assert(!ref || ref->_parent == this);
+ g_assert(!child->_parent);
+
+ Debug::EventTracker<DebugAddChild> tracker(*this, *child, ref);
+
+ SimpleNode *next;
+ if (ref) {
+ next = ref->_next;
+ ref->_next = child;
+
+ child->_prev = ref;
+ } else {
+ if(_first_child) _first_child->_prev = child;
+ next = _first_child;
+ _first_child = child;
+ }
+
+ if (!next) { // appending?
+ _last_child = child;
+ // set cached position if possible when appending
+ if (!ref) {
+ // if !next && !ref, child is sole child
+ child->_cached_position = 0;
+ _cached_positions_valid = true;
+ } else if (_cached_positions_valid) {
+ child->_cached_position = ref->_cached_position + 1;
+ }
+ } else {
+ next->_prev = child;
+ // invalidate cached positions otherwise
+ _cached_positions_valid = false;
+ }
+
+ child->_setParent(this);
+ child->_next = next;
+ _child_count++;
+
+ _document->logger()->notifyChildAdded(*this, *child, ref);
+ _observers.notifyChildAdded(*this, *child, ref);
+}
+
+void SimpleNode::removeChild(Node *generic_child) {
+ g_assert(generic_child);
+ g_assert(generic_child->document() == _document);
+
+ SimpleNode *child=dynamic_cast<SimpleNode *>(generic_child);
+ SimpleNode *ref=child->_prev;
+ SimpleNode *next = child->_next;
+
+ g_assert(child->_parent == this);
+
+ Debug::EventTracker<DebugRemoveChild> tracker(*this, *child);
+
+ if (ref) {
+ ref->_next = next;
+ } else {
+ _first_child = next;
+ }
+ if (next) { // removing the last child?
+ next->_prev = ref;
+ } else {
+ // removing any other child invalidates the cached positions
+ _last_child = ref;
+ _cached_positions_valid = false;
+ }
+
+ child->_next = nullptr;
+ child->_prev = nullptr;
+ child->_setParent(nullptr);
+ _child_count--;
+
+ _document->logger()->notifyChildRemoved(*this, *child, ref);
+ _observers.notifyChildRemoved(*this, *child, ref);
+}
+
+void SimpleNode::changeOrder(Node *generic_child, Node *generic_ref) {
+ g_assert(generic_child);
+ g_assert(generic_child->document() == this->_document);
+ g_assert(!generic_ref || generic_ref->document() == this->_document);
+
+ SimpleNode *const child=dynamic_cast<SimpleNode *>(generic_child);
+ SimpleNode *const ref=dynamic_cast<SimpleNode *>(generic_ref);
+
+ g_return_if_fail(child->parent() == this);
+ g_return_if_fail(child != ref);
+ g_return_if_fail(!ref || ref->parent() == this);
+
+ SimpleNode *const prev= child->_prev;
+
+ Debug::EventTracker<DebugSetChildPosition> tracker(*this, *child, prev, ref);
+
+ if (prev == ref) { return; }
+
+ SimpleNode *next;
+
+ /* Remove from old position. */
+ next = child->_next;
+ if (prev) {
+ prev->_next = next;
+ } else {
+ _first_child = next;
+ }
+ if (next) {
+ next->_prev = prev;
+ } else {
+ _last_child = prev;
+ }
+
+ /* Insert at new position. */
+ if (ref) {
+ next = ref->_next;
+ ref->_next = child;
+ } else {
+ next = _first_child;
+ _first_child = child;
+ }
+
+ child->_prev = ref;
+ child->_next = next;
+
+ if (next) {
+ next->_prev = child;
+ } else {
+ _last_child = child;
+ }
+
+ _cached_positions_valid = false;
+
+ _document->logger()->notifyChildOrderChanged(*this, *child, prev, ref);
+ _observers.notifyChildOrderChanged(*this, *child, prev, ref);
+}
+
+void SimpleNode::setPosition(int pos) {
+ g_return_if_fail(_parent != nullptr);
+
+ // a position beyond the end of the list means the end of the list;
+ // a negative position is the same as an infinitely large position
+
+ SimpleNode *ref=nullptr;
+ for ( SimpleNode *sibling = _parent->_first_child ;
+ sibling && pos ; sibling = sibling->_next )
+ {
+ if ( sibling != this ) {
+ ref = sibling;
+ pos--;
+ }
+ }
+
+ _parent->changeOrder(this, ref);
+}
+
+namespace {
+
+void child_added(Node *node, Node *child, Node *ref, void *data) {
+ reinterpret_cast<NodeObserver *>(data)->notifyChildAdded(*node, *child, ref);
+}
+
+void child_removed(Node *node, Node *child, Node *ref, void *data) {
+ reinterpret_cast<NodeObserver *>(data)->notifyChildRemoved(*node, *child, ref);
+}
+
+void content_changed(Node *node, gchar const *old_content, gchar const *new_content, void *data) {
+ reinterpret_cast<NodeObserver *>(data)->notifyContentChanged(*node, Util::share_unsafe((const char *)old_content), Util::share_unsafe((const char *)new_content));
+}
+
+void attr_changed(Node *node, gchar const *name, gchar const *old_value, gchar const *new_value, bool /*is_interactive*/, void *data) {
+ reinterpret_cast<NodeObserver *>(data)->notifyAttributeChanged(*node, g_quark_from_string(name), Util::share_unsafe((const char *)old_value), Util::share_unsafe((const char *)new_value));
+}
+
+void order_changed(Node *node, Node *child, Node *old_ref, Node *new_ref, void *data) {
+ reinterpret_cast<NodeObserver *>(data)->notifyChildOrderChanged(*node, *child, old_ref, new_ref);
+}
+
+const NodeEventVector OBSERVER_EVENT_VECTOR = {
+ &child_added,
+ &child_removed,
+ &attr_changed,
+ &content_changed,
+ &order_changed
+};
+
+};
+
+void SimpleNode::synthesizeEvents(NodeEventVector const *vector, void *data) {
+ if (vector->attr_changed) {
+ for ( List<AttributeRecord const> iter = _attributes ;
+ iter ; ++iter )
+ {
+ vector->attr_changed(this, g_quark_to_string(iter->key), nullptr, iter->value, false, data);
+ }
+ }
+ if (vector->child_added) {
+ SimpleNode *ref = nullptr;
+ for ( SimpleNode *child = this->_first_child ;
+ child ; child = child->_next )
+ {
+ vector->child_added(this, child, ref, data);
+ ref = child;
+ }
+ }
+ if (vector->content_changed) {
+ vector->content_changed(this, nullptr, this->_content, data);
+ }
+}
+
+void SimpleNode::synthesizeEvents(NodeObserver &observer) {
+ synthesizeEvents(&OBSERVER_EVENT_VECTOR, &observer);
+}
+
+void SimpleNode::recursivePrintTree(unsigned level) {
+
+ if (level == 0) {
+ std::cout << "XML Node Tree" << std::endl;
+ }
+ std::cout << "XML: ";
+ for (unsigned i = 0; i < level; ++i) {
+ std::cout << " ";
+ }
+ char const *id=attribute("id");
+ if (id) {
+ std::cout << id << std::endl;
+ } else {
+ std::cout << name() << std::endl;
+ }
+ for (SimpleNode *child = _first_child; child != nullptr; child = child->_next) {
+ child->recursivePrintTree( level+1 );
+ }
+}
+
+Node *SimpleNode::root() {
+ Node *parent=this;
+ while (parent->parent()) {
+ parent = parent->parent();
+ }
+
+ if ( parent->type() == DOCUMENT_NODE ) {
+ for ( Node *child = _document->firstChild() ;
+ child ; child = child->next() )
+ {
+ if ( child->type() == ELEMENT_NODE ) {
+ return child;
+ }
+ }
+ return nullptr;
+ } else if ( parent->type() == ELEMENT_NODE ) {
+ return parent;
+ } else {
+ return nullptr;
+ }
+}
+
+void SimpleNode::cleanOriginal(Node *src, gchar const *key){
+ std::vector<Node *> to_delete;
+ for ( Node *child = this->firstChild() ; child != nullptr ; child = child->next() )
+ {
+ gchar const *id = child->attribute(key);
+ if (id) {
+ Node *rch = sp_repr_lookup_child(src, key, id);
+ if (rch) {
+ child->cleanOriginal(rch, key);
+ } else {
+ to_delete.push_back(child);
+ }
+ } else {
+ to_delete.push_back(child);
+ }
+ }
+ for (auto & i : to_delete) {
+ removeChild(i);
+ }
+}
+
+bool SimpleNode::equal(Node const *other, bool recursive) {
+ if(strcmp(name(),other->name())!= 0){
+ return false;
+ }
+ if (!(strcmp("sodipodi:namedview", name()))) {
+ return true;
+ }
+ guint orig_length = 0;
+ guint other_length = 0;
+
+ if(content() && other->content() && strcmp(content(), other->content()) != 0){
+ return false;
+ }
+ for (List<AttributeRecord const> orig_attr = attributeList(); orig_attr; ++orig_attr) {
+ for (List<AttributeRecord const> other_attr = other->attributeList(); other_attr; ++other_attr) {
+ const gchar * key_orig = g_quark_to_string(orig_attr->key);
+ const gchar * key_other = g_quark_to_string(other_attr->key);
+ if (!strcmp(key_orig, key_other) &&
+ !strcmp(orig_attr->value, other_attr->value))
+ {
+ other_length++;
+ break;
+ }
+ }
+ orig_length++;
+ }
+ if (orig_length != other_length) {
+ return false;
+ }
+ if (recursive) {
+ //NOTE: for faster the childs need to be in the same order
+ Node const *other_child = other->firstChild();
+ for ( Node *child = firstChild();
+ child;
+ child = child->next())
+ {
+ if (!child->equal(other_child, recursive)) {
+ return false;
+ }
+ other_child = other_child->next();
+ if(!other_child) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+void SimpleNode::mergeFrom(Node const *src, gchar const *key, bool extension, bool clean) {
+ g_return_if_fail(src != nullptr);
+ g_return_if_fail(key != nullptr);
+ g_assert(src != this);
+
+ setContent(src->content());
+ if(_parent) {
+ setPosition(src->position());
+ }
+
+ if (clean) {
+ Node * srcp = const_cast<Node *>(src);
+ cleanOriginal(srcp, key);
+ }
+
+ for ( Node const *child = src->firstChild() ; child != nullptr ; child = child->next() )
+ {
+ gchar const *id = child->attribute(key);
+ if (id) {
+ Node *rch=sp_repr_lookup_child(this, key, id);
+ if (rch && (!extension || rch->equal(child, false))) {
+ rch->mergeFrom(child, key, extension);
+ continue;
+ } else {
+ if(rch) {
+ removeChild(rch);
+ }
+ }
+ }
+ {
+ guint pos = child->position();
+ Node *rch=child->duplicate(_document);
+ addChildAtPos(rch, pos);
+ rch->release();
+ }
+ }
+
+ for ( List<AttributeRecord const> iter = src->attributeList() ;
+ iter ; ++iter )
+ {
+ setAttribute(g_quark_to_string(iter->key), iter->value);
+ }
+}
+
+}
+
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/simple-node.h b/src/xml/simple-node.h
new file mode 100644
index 0000000..690bdaf
--- /dev/null
+++ b/src/xml/simple-node.h
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * GC-managed XML node implementation
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_NODE_H
+#error You have included xml/simple-node.h in your document, which is an implementation. Chances are that you want xml/node.h. Please fix that.
+#endif
+
+#ifndef SEEN_INKSCAPE_XML_SIMPLE_NODE_H
+#define SEEN_INKSCAPE_XML_SIMPLE_NODE_H
+
+#include <cassert>
+#include <iostream>
+
+#include "xml/node.h"
+#include "xml/attribute-record.h"
+#include "xml/composite-node-observer.h"
+#include "util/list-container.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+/**
+ * @brief Default implementation of the XML node stored in memory.
+ *
+ * @see Inkscape::XML::Node
+ */
+class SimpleNode
+: virtual public Node, public Inkscape::GC::Managed<>
+{
+public:
+ char const *name() const override;
+ int code() const override { return _name; }
+ void setCodeUnsafe(int code) override;
+
+ Document *document() override { return _document; }
+ Document const *document() const override {
+ return const_cast<SimpleNode *>(this)->document();
+ }
+
+ Node *duplicate(Document* doc) const override { return _duplicate(doc); }
+
+ Node *root() override;
+ Node const *root() const override {
+ return const_cast<SimpleNode *>(this)->root();
+ }
+
+ Node *parent() override { return _parent; }
+ Node const *parent() const override { return _parent; }
+
+ Node *next() override { return _next; }
+ Node const *next() const override { return _next; }
+ Node *prev() override { return _prev; }
+ Node const *prev() const override { return _prev; }
+
+ Node *firstChild() override { return _first_child; }
+ Node const *firstChild() const override { return _first_child; }
+ Node *lastChild() override { return _last_child; }
+ Node const *lastChild() const override { return _last_child; }
+
+ unsigned childCount() const override { return _child_count; }
+ Node *nthChild(unsigned index) override;
+ Node const *nthChild(unsigned index) const override {
+ return const_cast<SimpleNode *>(this)->nthChild(index);
+ }
+
+ void addChild(Node *child, Node *ref) override;
+ void appendChild(Node *child) override {
+ SimpleNode::addChild(child, _last_child);
+ }
+ void removeChild(Node *child) override;
+ void changeOrder(Node *child, Node *ref) override;
+
+ unsigned position() const override;
+ void setPosition(int pos) override;
+
+ char const *attribute(char const *key) const override;
+ bool matchAttributeName(char const *partial_name) const override;
+
+ char const *content() const override;
+ void setContent(char const *value) override;
+
+ void cleanOriginal(Node *src, gchar const *key) override;
+ bool equal(Node const *other, bool recursive) override;
+ void mergeFrom(Node const *src, char const *key, bool extension = false, bool clean = false) override;
+
+ Inkscape::Util::List<AttributeRecord const> attributeList() const override {
+ return _attributes;
+ }
+
+ void synthesizeEvents(NodeEventVector const *vector, void *data) override;
+ void synthesizeEvents(NodeObserver &observer) override;
+
+ void addListener(NodeEventVector const *vector, void *data) override {
+ assert(vector != NULL);
+ _observers.addListener(*vector, data);
+ }
+ void addObserver(NodeObserver &observer) override {
+ _observers.add(observer);
+ }
+ void removeListenerByData(void *data) override {
+ _observers.removeListenerByData(data);
+ }
+ void removeObserver(NodeObserver &observer) override {
+ _observers.remove(observer);
+ }
+
+ void addSubtreeObserver(NodeObserver &observer) override {
+ _subtree_observers.add(observer);
+ }
+ void removeSubtreeObserver(NodeObserver &observer) override {
+ _subtree_observers.remove(observer);
+ }
+
+ void recursivePrintTree(unsigned level = 0) override;
+
+protected:
+ SimpleNode(int code, Document *document);
+ SimpleNode(SimpleNode const &repr, Document *document);
+
+ virtual SimpleNode *_duplicate(Document *doc) const=0;
+ void setAttributeImpl(char const *key, char const *value, bool is_interactive) override;
+
+private:
+ void operator=(Node const &); // no assign
+
+ void _setParent(SimpleNode *parent);
+ unsigned _childPosition(SimpleNode const &child) const;
+
+ SimpleNode *_parent;
+ SimpleNode *_next;
+ SimpleNode *_prev;
+ Document *_document;
+ mutable unsigned _cached_position;
+
+ int _name;
+
+ Inkscape::Util::MutableList<AttributeRecord> _attributes;
+
+ Inkscape::Util::ptr_shared _content;
+
+ unsigned _child_count;
+ mutable bool _cached_positions_valid;
+ SimpleNode *_first_child;
+ SimpleNode *_last_child;
+
+ CompositeNodeObserver _observers;
+ CompositeNodeObserver _subtree_observers;
+};
+
+}
+
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/sp-css-attr.h b/src/xml/sp-css-attr.h
new file mode 100644
index 0000000..07db9ca
--- /dev/null
+++ b/src/xml/sp-css-attr.h
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * SPCSSAttr - interface for CSS Attributes
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2010 Authors
+ * Copyright 2005 Kees Cook <kees@outflux.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_SP_SPCSSATTR_H
+#define SEEN_INKSCAPE_XML_SP_SPCSSATTR_H
+
+#include "xml/node.h"
+
+class SPCSSAttr : virtual public Inkscape::XML::Node {
+};
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/subtree.cpp b/src/xml/subtree.cpp
new file mode 100644
index 0000000..5340c2a
--- /dev/null
+++ b/src/xml/subtree.cpp
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * XML::Subtree - proxy for an XML subtree
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Copyright 2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "xml/node.h"
+#include "xml/subtree.h"
+#include "xml/node-iterators.h"
+
+namespace Inkscape {
+namespace XML {
+
+Subtree::Subtree(Node &root) : _root(root) {
+ _root.addSubtreeObserver(_observers);
+}
+
+Subtree::~Subtree() {
+ _root.removeSubtreeObserver(_observers);
+}
+
+namespace {
+
+void synthesize_events_recursive(Node &node, NodeObserver &observer) {
+ node.synthesizeEvents(observer);
+ for ( NodeSiblingIterator iter = node.firstChild() ; iter ; ++iter ) {
+ synthesize_events_recursive(*iter, observer);
+ }
+}
+
+}
+
+void Subtree::synthesizeEvents(NodeObserver &observer) {
+ synthesize_events_recursive(_root, observer);
+}
+
+void Subtree::addObserver(NodeObserver &observer) {
+ _observers.add(observer);
+}
+
+void Subtree::removeObserver(NodeObserver &observer) {
+ _observers.remove(observer);
+}
+
+}
+}
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/subtree.h b/src/xml/subtree.h
new file mode 100644
index 0000000..ef4002a
--- /dev/null
+++ b/src/xml/subtree.h
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * @brief Object representing a subtree of the XML document
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2015 Authors
+ * Copyright 2005 MenTaLguY <mental@rydia.net>
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_SUBTREE_H
+#define SEEN_INKSCAPE_XML_SUBTREE_H
+
+#include "inkgc/gc-managed.h"
+#include "xml/composite-node-observer.h"
+
+namespace Inkscape {
+namespace XML {
+
+/**
+ * @brief Represents a node and all its descendants
+ *
+ * This is a convenience object for node operations that affect all of the node's descendants.
+ * Currently the only such operations are adding and removing subtree observers
+ * and synthesizing events for the entire subtree.
+ */
+class Subtree : public GC::Managed<GC::SCANNED, GC::MANUAL> {
+public:
+ Subtree(Node &root);
+ ~Subtree();
+
+ /**
+ * @brief Synthesize events for the entire subtree
+ *
+ * This method notifies the specified observer of node changes equivalent to creating
+ * this subtree from scratch. The notifications recurse into the tree depth-first.
+ * Currently this is the only method that provides extra functionality compared to
+ * the public methods of Node.
+ */
+ void synthesizeEvents(NodeObserver &observer);
+ /**
+ * @brief Add an observer watching for subtree changes
+ *
+ * Equivalent to Node::addSubtreeObserver().
+ */
+ void addObserver(NodeObserver &observer);
+ /**
+ * @brief Add an observer watching for subtree changes
+ *
+ * Equivalent to Node::removeSubtreeObserver().
+ */
+ void removeObserver(NodeObserver &observer);
+
+private:
+ Node &_root;
+ CompositeNodeObserver _observers;
+};
+
+}
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
diff --git a/src/xml/text-node.h b/src/xml/text-node.h
new file mode 100644
index 0000000..58b12f3
--- /dev/null
+++ b/src/xml/text-node.h
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file
+ * Text node implementation
+ *//*
+ * Authors: see git history
+ *
+ * Copyright (C) 2018 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef SEEN_INKSCAPE_XML_TEXT_NODE_H
+#define SEEN_INKSCAPE_XML_TEXT_NODE_H
+
+#include <glib.h>
+#include "xml/simple-node.h"
+
+namespace Inkscape {
+
+namespace XML {
+
+/**
+ * @brief Text node, e.g. "Some text" in &lt;group&gt;Some text&lt;/group&gt;
+ */
+struct TextNode : public SimpleNode {
+ TextNode(Util::ptr_shared content, Document *doc)
+ : SimpleNode(g_quark_from_static_string("string"), doc)
+ {
+ setContent(content);
+ _is_CData = false;
+ }
+ TextNode(Util::ptr_shared content, Document *doc, bool is_CData)
+ : SimpleNode(g_quark_from_static_string("string"), doc)
+ {
+ setContent(content);
+ _is_CData = is_CData;
+ }
+ TextNode(TextNode const &other, Document *doc)
+ : SimpleNode(other, doc) {
+ _is_CData = other._is_CData;
+ }
+
+ Inkscape::XML::NodeType type() const override { return Inkscape::XML::TEXT_NODE; }
+ bool is_CData() const { return _is_CData; }
+
+protected:
+ SimpleNode *_duplicate(Document* doc) const override { return new TextNode(*this, doc); }
+ bool _is_CData;
+};
+
+}
+
+}
+
+#endif
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :