diff options
Diffstat (limited to 'src/xml')
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. <!-- Some comment --> + */ +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. <group /> + */ +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. <group />. + TEXT_NODE, ///< Text node, e.g. "Some text" in <group>Some text</group> is represented by a text node. + COMMENT_NODE, ///< Comment node, e.g. <!-- some comment --> + PI_NODE ///< Processing instruction node, e.g. <?xml version="1.0" encoding="utf-8" standalone="no"?> +}; + +// 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. <?xml version="1.0" encoding="utf-8" standalone="no"?> + */ +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"}, + {"\"", """}, + {"&", "&"}, + {"<", "<"}, + {">", ">"}, + {"a\"b<c>d;!@#$%^*(\\)?", "a"b<c>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>"&<></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; // " + break; + case '&': + len += 5; // & + break; + case '<': + case '>': + len += 4; // < or > + 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, """); + resp += 6; + break; + case '&': + strcpy(resp, "&"); + resp += 5; + break; + case '<': + strcpy(resp, "<"); + resp += 4; + break; + case '>': + strcpy(resp, ">"); + 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( """ ); break; + case '&': out.writeString( "&" ); break; + case '<': out.writeString( "<" ); break; + case '>': out.writeString( ">" ); 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 &, 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 <group>Some text</group> + */ +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 : |