diff options
Diffstat (limited to 'src/xml')
49 files changed, 7626 insertions, 0 deletions
diff --git a/src/xml/CMakeLists.txt b/src/xml/CMakeLists.txt new file mode 100644 index 0000000..a9cd74b --- /dev/null +++ b/src/xml/CMakeLists.txt @@ -0,0 +1,58 @@ +# 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 + node.cpp + node-iterators.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 + href-attribute-helper.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-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 + href-attribute-helper.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..233016c --- /dev/null +++ b/src/xml/attribute-record.h @@ -0,0 +1,62 @@ +// 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. + */ +class AttributeRecord : public Inkscape::GC::Managed<> { + public: + + 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; + bool operator== (const AttributeRecord &o) const {return key==o.key && value==o.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..76f082e --- /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::NodeType::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..541f289 --- /dev/null +++ b/src/xml/composite-node-observer.cpp @@ -0,0 +1,237 @@ +// 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 <algorithm> +#include <cstring> +#include <glib.h> + +#include "xml/composite-node-observer.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.emplace_back(&observer); + } else { + _active.emplace_back(&observer); + } +} + +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) +{ + auto found = std::find_if( + observers.begin(), observers.end(), + unmarked_record_satisfying<Predicate>(p) + ); + + if ( found != observers.end() ) { + ++marked_count; + found->marked = true; + return true; + } else { + return false; + } +} + +template <typename Predicate> +bool remove_one(ObserverRecordList &observers, unsigned &/*marked_count*/, + Predicate p) +{ + auto found = std::find_if( + observers.begin(), observers.end(), + unmarked_record_satisfying<Predicate>(p) + ); + + if ( found != observers.end() ) { + // for O(1) removal + if (observers.size() > 3) { + *found = std::move(observers.back()); + observers.pop_back(); + } else { + observers.erase(found); + } + return true; + } else { + return false; + } +} + +bool is_marked(ObserverRecord const &record) { return record.marked; } + +void remove_all_marked(ObserverRecordList &observers, unsigned &marked_count) +{ + if (marked_count) { + g_assert(!observers.empty()); + + auto newEnd = std::remove_if(observers.begin(), observers.end(), is_marked); + observers.erase(newEnd, observers.end()); + marked_count = 0; + } +} + +} + +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(); + + g_assert(_pending.empty()); + } +} + +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 XML +} // 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/composite-node-observer.h b/src/xml/composite-node-observer.h new file mode 100644 index 0000000..212d0ad --- /dev/null +++ b/src/xml/composite-node-observer.h @@ -0,0 +1,100 @@ +// 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-alloc.h" +#include "inkgc/gc-managed.h" +#include "xml/node-observer.h" + +#include <vector> + +namespace Inkscape { + +namespace XML { + +/** + * @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 + { + explicit ObserverRecord(NodeObserver *o) : observer(o), marked(false) {} + + NodeObserver *observer; + bool marked; //< if marked for removal + }; + using ObserverRecordList = std::vector<ObserverRecord, Inkscape::GC::Alloc<ObserverRecord, Inkscape::GC::ATOMIC>>; + + 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); + + 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..d3b508a --- /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() == NodeType::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..fd837f5 --- /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/src/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..6002119 --- /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::NodeType::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..6aab5c5 --- /dev/null +++ b/src/xml/event.cpp @@ -0,0 +1,529 @@ +// 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 "xml/document.h" +#include "xml/node-observer.h" +#include "debug/event-tracker.h" +#include "debug/simple-event.h" + +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 +) { + std::vector<Inkscape::XML::Event const *> r; + while (log) { + r.push_back(log); + log = log->next; + } + for ( auto reversed = r.rbegin(); reversed != r.rend(); ++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->old_name, this->new_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::NodeType::DOCUMENT_NODE: + type_name = "Document"; + break; + case Inkscape::XML::NodeType::ELEMENT_NODE: + type_name = "Element"; + break; + case Inkscape::XML::NodeType::TEXT_NODE: + type_name = "Text"; + break; + case Inkscape::XML::NodeType::COMMENT_NODE: + type_name = "Comment"; + break; + default: + g_assert_not_reached(); + } + char buffer[40]; + result.append("#"); + if (node.attribute("id")) { + result.append(node.attribute("id")); + } + 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..0514a24 --- /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..a1c2d7a --- /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/href-attribute-helper.cpp b/src/xml/href-attribute-helper.cpp new file mode 100644 index 0000000..55f2b93 --- /dev/null +++ b/src/xml/href-attribute-helper.cpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Thomas Holder + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "href-attribute-helper.h" + +namespace Inkscape { + +/** + * Get the 'href' or 'xlink:href' (fallback) attribute from an XML node. + * + * @return The observed key and its value + */ +std::pair<char const *, char const *> getHrefAttribute(XML::Node const &node) +{ + if (auto value = node.attribute("href")) { + return {"href", value}; + } + + if (auto value = node.attribute("xlink:href")) { + return {"xlink:href", value}; + } + + return {"xlink:href", nullptr}; +} + +/** + * If the 'href' attribute already exists for the given node, then set a new + * value for it. Otherwise set the value for 'xlink:href'. + */ +void setHrefAttribute(XML::Node &node, Util::const_char_ptr value) +{ + if (node.attribute("href")) { + node.setAttribute("href", value); + } else { + node.setAttribute("xlink:href", value); + } +} + +} // namespace Inkscape diff --git a/src/xml/href-attribute-helper.h b/src/xml/href-attribute-helper.h new file mode 100644 index 0000000..c21cce2 --- /dev/null +++ b/src/xml/href-attribute-helper.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Thomas Holder + * + * Copyright (C) 2022 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <utility> + +#include "xml/node.h" + +namespace Inkscape { +std::pair<char const *, char const *> getHrefAttribute(XML::Node const &); +void setHrefAttribute(XML::Node &, Util::const_char_ptr); +} // namespace Inkscape 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-fns.cpp b/src/xml/node-fns.cpp new file mode 100644 index 0000000..86a5339 --- /dev/null +++ b/src/xml/node-fns.cpp @@ -0,0 +1,85 @@ +// 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 "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() != NodeType::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(); +} + +} +} + +/* + 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.cpp b/src/xml/node-iterators.cpp new file mode 100644 index 0000000..da43742 --- /dev/null +++ b/src/xml/node-iterators.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Node iterators + * + * Copyright (C) 2004 MenTaLguY + */ + +#include "node-iterators.h" +#include "xml/node.h" + +namespace Inkscape { +namespace XML { + +Node const *NodeSiblingIteratorStrategy::next(Node const *node) +{ + return node ? node->next() : nullptr; +} + +Node const *NodeParentIteratorStrategy::next(Node const *node) +{ + return node ? node->parent() : nullptr; +} + +} // namespace XML +} // namespace Inkscape + +// vim: expandtab:shiftwidth=4: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..04a22a8 --- /dev/null +++ b/src/xml/node-iterators.h @@ -0,0 +1,60 @@ +// 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" + +namespace Inkscape { +namespace XML { + +class Node; + +struct NodeSiblingIteratorStrategy { + static Node const *next(Node const *node); +}; + +struct NodeParentIteratorStrategy { + static Node const *next(Node const *node); +}; + +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.cpp b/src/xml/node.cpp new file mode 100644 index 0000000..8d4ec48 --- /dev/null +++ b/src/xml/node.cpp @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * @brief + * + * Authors: see git history + * Osama Ahmad + * + * Copyright (c) 2021 Osama Ahmad, Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> + +#include "node.h" +#include "svg/stringstream.h" +#include "svg/css-ostringstream.h" +#include "svg/svg-length.h" + +namespace Inkscape{ +namespace XML { + +void Node::setAttribute(Util::const_char_ptr key, Util::const_char_ptr value) +{ + this->setAttributeImpl(key.data(), value.data()); +} + +bool Node::copyAttribute(Util::const_char_ptr key, Node const *source_node, bool remove_if_empty) +{ + if (source_node) { + if (auto const value = source_node->attribute(key.data())) { + if (*value || !remove_if_empty) { + setAttribute(key, value); + } + return true; + } + + if (remove_if_empty) { + removeAttribute(key); + return true; + } + } + + return false; +} + +bool Node::getAttributeBoolean(Util::const_char_ptr key, bool default_value) const +{ + auto v = this->attribute(key.data()); + if (v == nullptr) { + return default_value; + } + + if (!g_ascii_strcasecmp(v, "true") || + !g_ascii_strcasecmp(v, "yes") || + !g_ascii_strcasecmp(v, "y") || + (atoi(v) != 0)) + { + return true; + } else { + return false; + } +} + +int Node::getAttributeInt(Util::const_char_ptr key, int default_value) const +{ + auto v = this->attribute(key.data()); + if (v == nullptr) { + return default_value; + } + return atoi(v); +} + +double Node::getAttributeDouble(Util::const_char_ptr key, double default_value) const +{ + auto v = this->attribute(key.data()); + if (v == nullptr) { + return default_value; + } + + return g_ascii_strtod(v, nullptr); +} + +bool Node::setAttributeBoolean(Util::const_char_ptr key, bool val) +{ + this->setAttribute(key, (val) ? "true" : "false"); + return true; +} + +bool Node::setAttributeInt(Util::const_char_ptr key, int val) +{ + gchar c[32]; + + g_snprintf(c, 32, "%d", val); + + this->setAttribute(key, c); + return true; +} + +bool Node::setAttributeCssDouble(Util::const_char_ptr key, double val) +{ + Inkscape::CSSOStringStream os; + os << val; + + this->setAttribute(key, os.str()); + return true; +} + +bool Node::setAttributeSvgDouble(Util::const_char_ptr key, double val) +{ + g_return_val_if_fail(val == val, false); // tests for nan + + Inkscape::SVGOStringStream os; + os << val; + + this->setAttribute(key, os.str()); + return true; +} + +bool Node::setAttributeSvgNonDefaultDouble(Util::const_char_ptr key, double val, double default_value) +{ + if (val == default_value) { + this->removeAttribute(key); + return true; + } + return this->setAttributeSvgDouble(key, val); +} + +bool Node::setAttributeSvgLength(Util::const_char_ptr key, SVGLength const &val) +{ + this->setAttribute(key, val.write()); + return true; +} + +bool Node::setAttributePoint(Util::const_char_ptr key, Geom::Point const &val) +{ + Inkscape::SVGOStringStream os; + os << val[Geom::X] << "," << val[Geom::Y]; + + this->setAttribute(key, os.str()); + return true; +} + +Geom::Point Node::getAttributePoint(Util::const_char_ptr key, Geom::Point default_value) const +{ + auto v = this->attribute(key.data()); + if (v == nullptr) { + return default_value; + } + + 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); + return Geom::Point(newx, newy); + } + + g_strfreev(strarray); + return default_value; +} + +void Node::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()); +} + +} // namespace XML +} // namespace Inkscape diff --git a/src/xml/node.h b/src/xml/node.h new file mode 100644 index 0000000..f7bd875 --- /dev/null +++ b/src/xml/node.h @@ -0,0 +1,622 @@ +// 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 <cassert> +#include <vector> +#include <list> +#include <2geom/point.h> + +#include "gc-anchored.h" +#include "inkgc/gc-alloc.h" +#include "node-iterators.h" +#include "util/const_char_ptr.h" +#include "svg/svg-length.h" + +namespace Inkscape { +namespace XML { + +class AttributeRecord; +struct Document; +class Event; +class NodeObserver; + +using AttributeVector = std::vector<AttributeRecord, Inkscape::GC::Alloc<AttributeRecord>>; + +/** + * @brief Enumeration containing all supported node types. + */ +enum class 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 are 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. + * + * 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 const AttributeVector & 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 + */ + + void setAttribute(Util::const_char_ptr key, Util::const_char_ptr value); + + /** + * @brief Copy attribute value from another node to this node + * + * @param key Name of the attribute to change + * @param source_node Node from which to take the attribute value + * @param remove_if_empty + * If true, and the source node has no such attribute, or the source + * node's value for the attribute is an empty string, the attribute + * will be removed (if present) from this node. + * + * @return true if the attribute was set, false otherwise. + */ + + bool copyAttribute(Util::const_char_ptr key, Node const *source_node, bool remove_if_empty = 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. + */ + bool getAttributeBoolean(Util::const_char_ptr key, bool default_value = false) const; + + int getAttributeInt(Util::const_char_ptr key, int default_value = 0) const; + + double getAttributeDouble(Util::const_char_ptr key, double default_value = 0.0) const; + + bool setAttributeBoolean(Util::const_char_ptr key, bool val); + + bool setAttributeInt(Util::const_char_ptr key, int val); + + /** + * Set a property attribute to \a val [slightly rounded], in the format + * required for CSS properties: in particular, it never uses exponent + * notation. + */ + bool setAttributeCssDouble(Util::const_char_ptr key, double val); + + /** + * For attributes where an exponent is allowed. + * + * Not suitable for property attributes (fill-opacity, font-size etc.). + */ + bool setAttributeSvgDouble(Util::const_char_ptr key, double val); + + bool setAttributeSvgNonDefaultDouble(Util::const_char_ptr key, + double val, double default_value); + + bool setAttributeSvgLength(Util::const_char_ptr key, SVGLength const &val); + + bool setAttributePoint(Util::const_char_ptr key, Geom::Point const &val); + + Geom::Point getAttributePoint(Util::const_char_ptr key, Geom::Point default_value = {}) const; + + /** + * @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); + + /** + * @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); } + + //@} + /** + * @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, bool skip_ids = false) = 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; + + virtual void recursivePrintTree(unsigned level) = 0; + + /*@}*/ + + using iterator = Inkscape::XML::NodeSiblingIterator; + + /** @brief Iterator over children */ + iterator begin() { return iterator(this->firstChild()); } + /** @brief Helper to use the standard lib container functions */ + iterator end() { return iterator(nullptr); } + + /** @brief Compare a node by looking at its name to a string */ + bool operator==(const std::string &name) const { return this->name() == name; } + + /** @brief depth first search to find a node + * + * This function takes any list structure you want and uses that + * to compare Node's down the child tree. It will do depth first + * searching into the tree. The key part is that since it is a template + * you have flexibility on the container, and the comparison that is + * being used. Typically it will be used with something like a + * std::list<std::string> which will compare against the node's name + * but more complex searches could be imagined. + */ + template <typename T> + Node *findChildPath(T list) + { + return findChildPath(list.cbegin(), list.cend()); + } + + /** @brief template reshuffling to make the more useful findChildPath cleaner */ + template <typename iterT> + Node *findChildPath(iterT itr, iterT end) + { + if (itr == end) { + return this; + } + + for (auto &child : *this) { + if (child == *itr) { + auto found = child.findChildPath(std::next(itr), end); + if (found != nullptr) { + return found; + } + } + } + + return nullptr; + } + +protected: + Node(Node const &) + : Anchored() + {} + + virtual void setAttributeImpl(char const *key, char const *value) = 0; +}; + +} // 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/pi-node.h b/src/xml/pi-node.h new file mode 100644 index 0000000..b7f8e09 --- /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::NodeType::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..b74e9e6 --- /dev/null +++ b/src/xml/rebase-hrefs.cpp @@ -0,0 +1,193 @@ +// 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 <glibmm/utility.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/repr.h" +#include "xml/rebase-hrefs.h" +#include "xml/href-attribute-helper.h" + +using Inkscape::XML::AttributeRecord; +using Inkscape::XML::AttributeVector; + +/** + * Determine if a href needs rebasing. + */ +static bool href_needs_rebasing(char const *href) +{ + // RFC 3986 defines empty string relative URL as referring to the + // containing document, rather than referring to the base URI. + if (!href[0] || href[0] == '#') { + return false; + } + + // skip document-local queries + if (href[0] == '?') { + return false; + } + + // skip absolute-path and network-path references + if (href[0] == '/') { + return false; + } + + // Don't change non-file URIs (like data or http) + auto scheme = Glib::make_unique_ptr_gfree(g_uri_parse_scheme(href)); + return !scheme || g_str_equal(scheme.get(), "file"); +} + +AttributeVector +Inkscape::XML::rebase_href_attrs(gchar const *const old_abs_base, + gchar const *const new_abs_base, + const AttributeVector &attributes) +{ + using Inkscape::Util::share_string; + + auto ret = attributes; // copy + + if (old_abs_base == new_abs_base) { + return ret; + } + + static GQuark const href_key = g_quark_from_static_string("href"); + static GQuark const xlink_href_key = g_quark_from_static_string("xlink:href"); + static GQuark const absref_key = g_quark_from_static_string("sodipodi:absref"); + + auto const find_record = [&ret](GQuark const key) { + return find_if(ret.begin(), ret.end(), [key](auto const &attr) { return attr.key == key; }); + }; + + auto href_it = find_record(href_key); + if (href_it == ret.end()) { + href_it = find_record(xlink_href_key); + } + if (href_it == ret.end() || !href_needs_rebasing(href_it->value.pointer())) { + return ret; + } + + auto uri = URI::from_href_and_basedir(href_it->value.pointer(), old_abs_base); + auto abs_href = uri.toNativeFilename(); + + auto absref_it = find_record(absref_key); + if (absref_it != ret.end()) { + if (g_file_test(abs_href.c_str(), G_FILE_TEST_EXISTS)) { + if (!streq(abs_href.c_str(), absref_it->value.pointer())) { + absref_it->value = share_string(abs_href.c_str()); + } + } else if (g_file_test(absref_it->value.pointer(), G_FILE_TEST_EXISTS)) { + uri = URI::from_native_filename(absref_it->value.pointer()); + } + } + + 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()); + href_it->value = share_string(new_href.c_str()); + + return ret; +} + +static void rebase_image_href(Inkscape::XML::Node *ir, std::string const &old_base_url_str, std::string const &new_base_url_str, bool const spns) { + + using Inkscape::URI; + + auto [href_key, href_cstr] = Inkscape::getHrefAttribute(*ir); + if (!href_cstr) { + return; + } + + if (!href_needs_rebasing(href_cstr)) { + return; + } + + // make absolute + URI url; + try { + url = URI(href_cstr, old_base_url_str.c_str()); + } catch (...) { + return; + } + + // skip non-file URLs + if (!url.hasScheme("file")) { + return; + } + + // 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(href_key, href_str); +} + +void Inkscape::XML::rebase_hrefs(Inkscape::XML::Node *rootxml, gchar const *const old_base, gchar const *const new_base, bool const spns) +{ + using Inkscape::URI; + + std::string old_base_url_str = URI::from_dirname(old_base).str(); + std::string new_base_url_str; + + if (new_base) { + new_base_url_str = URI::from_dirname(new_base).str(); + } + sp_repr_visit_descendants(rootxml, [&](Inkscape::XML::Node *ir) { + if (!strcmp("svg:image", ir->name())) { + rebase_image_href(ir, old_base_url_str, new_base_url_str, spns); + } + return true; + }); +} + +void Inkscape::XML::rebase_hrefs(SPDocument *const doc, gchar const *const new_base, bool const spns) +{ + rebase_hrefs(doc->getReprRoot(), doc->getDocumentBase(), new_base, spns); + 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..9d171ce --- /dev/null +++ b/src/xml/rebase-hrefs.h @@ -0,0 +1,73 @@ +// 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 <vector> +#include "xml/attribute-record.h" +#include "xml/node.h" + +class SPDocument; + +namespace Inkscape { +namespace XML { + +/** + * Change relative hrefs in current root XML node (faster than full document generation) + * + * 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(Inkscape::XML::Node *rootxml, gchar const *const old_base, gchar const *const new_base, bool const spns); +/** + * 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. + */ +AttributeVector rebase_href_attrs( + char const *old_abs_base, + char const *new_abs_base, + const AttributeVector & 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..b321b62 --- /dev/null +++ b/src/xml/repr-css.cpp @@ -0,0 +1,400 @@ +// 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/src/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::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::NodeType::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; +} + + +/** + * 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); + + return css->getAttributeDouble(name, defval); +} + +/** + * Set a style property to a new float value (e.g. opacity to 0.5). + */ +void sp_repr_css_set_property_double(SPCSSAttr *css, gchar const *name, double value) +{ + g_assert(css != nullptr); + g_assert(name != nullptr); + + ((Node *) css)->setAttributeCssDouble(name, value); +} + +/** + * 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 (const auto & iter : css->attributeList()) + { + if (iter.value && !strcmp(iter.value, "inkscape:unset")) { + continue; + } + + if (!str.empty()) { + str.push_back(';'); + } + + str.append(g_quark_to_string(iter.key)); + str.push_back(':'); + str.append(iter.value); // Any necessary quoting to be done by calling routine. + } +} + +/** + * 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 ( const auto & attr: css->attributeList() ) + { + gchar const * key = g_quark_to_string(attr.key); + gchar const * val = attr.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); + css->setAttribute(decl->property->stryng->str, reinterpret_cast<char const *>(str_value_unsigned)); + g_free(str_value_unsigned); +} + +/** + * 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 ( const auto & iter : css->attributeList() ) { + 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..249f62f --- /dev/null +++ b/src/xml/repr-io.cpp @@ -0,0 +1,1048 @@ +// 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 <libxml/xinclude.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 "xml/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::XML::Document; +using Inkscape::XML::SimpleDocument; +using Inkscape::XML::Node; +using Inkscape::XML::AttributeRecord; +using Inkscape::XML::AttributeVector; +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, + const AttributeVector & 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), + 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 ); + + 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; + Inkscape::IO::FileInputStream* instr; + Inkscape::IO::GzipInputStream* gzin; +}; + +int XmlSource::setFile(char const *filename) +{ + 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 + } + } + 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; + + 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 ( 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. + * XIncude is dangerous to support during use-cases like automated file format conversion, so it is off by default. + * + * \param filename The actual file to read from. + * + * \param default_ns Default namespace for the document, can be nullptr. + * + * \param xinclude Process XInclude directives, which is off by default for security. + */ +Document *sp_repr_read_file (const gchar * filename, const gchar *default_ns, bool xinclude) +{ + 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(); + if (xinclude && doc && doc->properties && xmlXIncludeProcessFlags(doc, XML_PARSE_NOXINCNODE) < 0) { + g_warning("XInclude processing failed for %s", filename); + } + 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::NodeType::ELEMENT_NODE ) { + GQuark code = repr->code(); + if (!qname_prefix(code).id()) { + gchar *svg_name = g_strconcat(prefix, ":", g_quark_to_string(code), nullptr); + 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); + } + } +} + +/** + * When an xml document can be parsed, but it's name spaces are not recognised + * we can repair the document and force the use of the svg namespace. + * + * This can help us make use of some older svg files which use xml ENTITIES + * which is a feature that should never be allowed to be used for security. + */ +void repair_namespace(Node *repr, const gchar *prefix) { + if ( repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE ) { + gchar *svg_name = nullptr; + if (strncmp(repr->name(), "ns:", 3) == 0) { + svg_name = g_strconcat(prefix, ":", repr->name() + 3, nullptr); + } else if (strncmp(repr->name(), "svg0:", 5) == 0) { + svg_name = g_strconcat(prefix, ":", repr->name() + 5, nullptr); + } + if (svg_name) { + repr->setCodeUnsafe(g_quark_from_string(svg_name)); + g_free(svg_name); + } + for ( Node *child = repr->firstChild() ; child ; child = child->next() ) { + repair_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 (!strcmp(root->name(), "ns:svg") || !strcmp(root->name(), "svg0:svg")) { + g_warning("Detected broken namespace \"%s\" in the SVG file, attempting to work around it", root->name()); + repair_namespace(root, "svg"); + } else 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::NodeType::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::NodeType::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; + } + + std::string old_href_abs_base; + std::string 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 { + std::string const cwd = Glib::get_current_dir(); + std::string 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; + case '\n': 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::NodeType::ELEMENT_NODE ) { + add_ns_map_entry(ns_map, qname_prefix(repr.code())); + for ( const auto & iter : repr.attributeList() ) + { + 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)); + } + + auto attributes = repr->attributeList(); // copy + + using Inkscape::Util::share_string; + 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 ) { + //repr->setAttribute(share_string("xmlns"), share_string(ns_uri)); + attributes.emplace_back(g_quark_from_static_string("xmlns"), ns_uri); + } + + Glib::ustring attr_name="xmlns:"; + attr_name.append(g_quark_to_string(prefix)); + GQuark key = g_quark_from_string(attr_name.c_str()); + //repr->setAttribute(share_string(attr_name.c_str()), share_string(ns_uri)); + attributes.emplace_back(key, ns_uri); + } + } 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::NodeType::TEXT_NODE: { + auto textnode = dynamic_cast<const Inkscape::XML::TextNode *>(repr); + assert(textnode); + if (textnode->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::NodeType::COMMENT_NODE: { + repr_write_comment( out, repr->content(), add_whitespace, indent_level, indent ); + break; + } + case Inkscape::XML::NodeType::PI_NODE: { + out.printf( "<?%s %s?>", repr->name(), repr->content() ); + break; + } + case Inkscape::XML::NodeType::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::NodeType::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, + const AttributeVector & 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; + } + } + + const auto rbd = rebase_href_attrs(old_href_base, new_href_base, attributes); + for (const auto &iter : rbd) { + 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::NodeType::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..9fd3b22 --- /dev/null +++ b/src/xml/repr-sorting.cpp @@ -0,0 +1,68 @@ +// 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" + +Inkscape::XML::Node const *LCA(Inkscape::XML::Node const *a, Inkscape::XML::Node const *b) +{ + using Inkscape::Algorithms::nearest_common_ancestor; + Inkscape::XML::Node const *ancestor = + nearest_common_ancestor<Inkscape::XML::NodeConstParentIterator>(a, b, nullptr); + bool OK = false; + if (ancestor) { + if (ancestor->type() != Inkscape::XML::NodeType::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..c63cb01 --- /dev/null +++ b/src/xml/repr-util.cpp @@ -0,0 +1,480 @@ +// 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 <glib.h> +#include <glibmm.h> + +#include "svg/svg-length.h" + +#include "xml/repr.h" +#include "xml/repr-sorting.h" + + +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[8]; + + //defaults[7].uri = g_quark_from_static_string("https://inkscape.org/namespaces/deprecated/osb"); + //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; +} + +Glib::ustring sp_repr_lookup_content(Inkscape::XML::Node const *repr, gchar const *name, Glib::ustring otherwise) +{ + if (auto node = sp_repr_lookup_name(repr, name, 1)) { + if (auto ret = node->firstChild()->content()) { + return ret; + } + } + return otherwise; +} + +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::NodeType::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; +} + +/* + 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..9cfc687 --- /dev/null +++ b/src/xml/repr.h @@ -0,0 +1,236 @@ +// 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, bool xinclude = false); +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_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_set_property_double(SPCSSAttr *css, char const *name, double value); + +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); + +//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); + +Glib::ustring sp_repr_lookup_content(Inkscape::XML::Node const *repr, gchar const *name, Glib::ustring otherwise = {}); + +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..5c661a2 --- /dev/null +++ b/src/xml/simple-document.h @@ -0,0 +1,97 @@ +// 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) {} + + NodeType type() const override { return Inkscape::XML::NodeType::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) + {} + + SimpleNode *_duplicate(Document* /*doc*/) const override + { + return new SimpleDocument(*this); + } + NodeObserver *logger() override { return this; } + +private: + bool _in_transaction; + LogBuilder _log_builder; +}; + +} + +} + +#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..9dd19f9 --- /dev/null +++ b/src/xml/simple-node.cpp @@ -0,0 +1,756 @@ +// 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. + */ + +// clang-format off +#include "xml/node.h" +#include "xml/simple-node.h" +// clang-format on + +#include <algorithm> +#include <cstring> +#include <string> + +#include <glib.h> + +#include "preferences.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 NodeType::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 NodeType::TEXT_NODE: + string = g_strdup_printf("text(%p)=%s", &node, node.content()); + break; + case NodeType::COMMENT_NODE: + string = g_strdup_printf("comment(%p)=<!--%s-->", &node, node.content()); + break; + case NodeType::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)); + } +}; + +} // namespace + +using Util::ptr_shared; +using Util::share_string; +using Util::share_unsafe; + +SimpleNode::SimpleNode(int code, Document *document) + : _name(code) +{ + 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) + : _cached_position(node._cached_position) + , _name(node._name) + , _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 + } + + _attributes = node._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 (const auto & iter : _attributes) + { + 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 ( const auto & iter : _attributes ) + { + 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) +{ + 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_ATTRCLEAN_ATTR_WARN; + bool attr_remove = flags & SP_ATTRCLEAN_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_ATTRCLEAN_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); + + AttributeRecord *ref = nullptr; + for ( auto & existing : _attributes ) { + if ( existing.key == key ) { + ref = &existing; + break; + } + } + Debug::EventTracker<> tracker; + + ptr_shared old_value=( ref ? ref->value : ptr_shared() ); + + ptr_shared new_value=ptr_shared(); + if (cleaned_value) { // set value of attribute + new_value = share_string(cleaned_value); + tracker.set<DebugSetAttribute>(*this, key, new_value); + if (!ref) { + _attributes.emplace_back(key, new_value); + } else { + ref->value = new_value; + } + } else { //clearing attribute + tracker.set<DebugClearAttribute>(*this, key); + if (ref) { + _attributes.erase(std::find(_attributes.begin(),_attributes.end(),(*ref))); + } + } + + 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); +} + +void SimpleNode::synthesizeEvents(NodeObserver &observer) +{ + for (auto const &iter : _attributes) { + observer.notifyAttributeChanged(*this, iter.key, Util::ptr_shared(), iter.value); + } + + SimpleNode *ref = nullptr; + for (auto child = this->_first_child; child; child = child->_next) { + observer.notifyChildAdded(*this, *child, ref); + ref = child; + } + + observer.notifyContentChanged(*this, Util::ptr_shared(), this->_content); +} + +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() == NodeType::DOCUMENT_NODE ) { + for ( Node *child = _document->firstChild() ; + child ; child = child->next() ) + { + if ( child->type() == NodeType::ELEMENT_NODE ) { + return child; + } + } + return nullptr; + } else if ( parent->type() == NodeType::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 string_equal(const gchar *a,const gchar *b) { + return g_strcmp0(a, b) == 0; +} + +bool SimpleNode::equal(Node const *other, bool recursive, bool skip_ids) { + if (!other || !string_equal(name(), other->name())) { + return false; + } + if (!string_equal(content(), other->content())) { + return false; + } + const AttributeVector & orig_attrs = attributeList(); + const AttributeVector & other_attrs = other->attributeList(); + size_t sizeorig = orig_attrs.size(); + size_t sizeother = other_attrs.size(); + if (sizeother != sizeorig) { + return false; + } + for (size_t i = 0; i < sizeorig; i++) { + const gchar * key_orig = g_quark_to_string(orig_attrs[i].key); + if (skip_ids && string_equal(key_orig, "id")) { + continue; + } + const gchar * key_other = g_quark_to_string(other_attrs[i].key); + if (!string_equal(key_orig, key_other) || + !string_equal(orig_attrs[i].value, other_attrs[i].value) != 0) + { + return false; + } + } + if (recursive) { + //NOTE: for faster the childs need to be in the same order + Node const *other_child = other->firstChild(); + Node *child = firstChild(); + while (child && other_child) { + if (!child->equal(other_child, recursive, skip_ids)) { + return false; + } + child = child->next(); + other_child = other_child->next(); + } + if ((!child && other_child) || (child && !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); + + Node * srcp = const_cast<Node *>(src); + if (srcp->equal(this, true)) { + return; + } + setContent(src->content()); + if(_parent) { + setPosition(src->position()); + } + + if (clean) { + 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 ( const auto & iter : src->attributeList() ) + { + 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..e3f77b8 --- /dev/null +++ b/src/xml/simple-node.h @@ -0,0 +1,164 @@ +// 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 <vector> + +#include "xml/node.h" +#include "xml/attribute-record.h" +#include "xml/composite-node-observer.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, bool skip_ids = false) override; + void mergeFrom(Node const *src, char const *key, bool extension = false, bool clean = false) override; + + const AttributeVector & attributeList() const override { + return _attributes; + } + + void synthesizeEvents(NodeObserver &observer) override; + + void addObserver(NodeObserver &observer) override { + _observers.add(observer); + } + 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) 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; + + AttributeVector _attributes; + + Inkscape::Util::ptr_shared _content; + + unsigned _child_count{0}; + mutable bool _cached_positions_valid{false}; + 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..9aaa1d3 --- /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::NodeType::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 : |