diff options
Diffstat (limited to '')
-rw-r--r-- | src/xml/repr-io.cpp | 1075 |
1 files changed, 1075 insertions, 0 deletions
diff --git a/src/xml/repr-io.cpp b/src/xml/repr-io.cpp new file mode 100644 index 0000000..d891f07 --- /dev/null +++ b/src/xml/repr-io.cpp @@ -0,0 +1,1075 @@ +// 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), + LoadEntities(false), + cachedData(), + cachedPos(0), + instr(nullptr), + gzin(nullptr) + { + for (unsigned char & k : firstFew) + { + k=0; + } + } + virtual ~XmlSource() + { + close(); + if ( encoding ) { + g_free(encoding); + encoding = nullptr; + } + } + + int setFile( char const * filename, bool load_entities ); + + xmlDocPtr readXml(); + + static int readCb( void * context, char * buffer, int len ); + static int closeCb( void * context ); + + char const* getEncoding() const { return encoding; } + int read( char * buffer, int len ); + int close(); +private: + const char* filename; + char* encoding; + FILE* fp; + unsigned char firstFew[4]; + int firstFewLen; + bool LoadEntities; // Checks for SYSTEM Entities (requires cached data) + std::string cachedData; + unsigned int cachedPos; + Inkscape::IO::FileInputStream* instr; + Inkscape::IO::GzipInputStream* gzin; +}; + +int XmlSource::setFile(char const *filename, bool load_entities=false) +{ + int retVal = -1; + + this->filename = filename; + + fp = Inkscape::IO::fopen_utf8name(filename, "r"); + if ( fp ) { + // First peek in the file to see what it is + memset( firstFew, 0, sizeof(firstFew) ); + + size_t some = fread( firstFew, 1, 4, fp ); + if ( fp ) { + // first check for compression + if ( (some >= 2) && (firstFew[0] == 0x1f) && (firstFew[1] == 0x8b) ) { + //g_message(" the file being read is gzip'd. extract it"); + fclose(fp); + fp = nullptr; + fp = Inkscape::IO::fopen_utf8name(filename, "r"); + instr = new Inkscape::IO::FileInputStream(fp); + gzin = new Inkscape::IO::GzipInputStream(*instr); + + memset( firstFew, 0, sizeof(firstFew) ); + some = 0; + int single = 0; + while ( some < 4 && single >= 0 ) + { + single = gzin->get(); + if ( single >= 0 ) { + firstFew[some++] = 0x0ff & single; + } else { + break; + } + } + } + + int encSkip = 0; + if ( (some >= 2) &&(firstFew[0] == 0xfe) && (firstFew[1] == 0xff) ) { + encoding = g_strdup("UTF-16BE"); + encSkip = 2; + } else if ( (some >= 2) && (firstFew[0] == 0xff) && (firstFew[1] == 0xfe) ) { + encoding = g_strdup("UTF-16LE"); + encSkip = 2; + } else if ( (some >= 3) && (firstFew[0] == 0xef) && (firstFew[1] == 0xbb) && (firstFew[2] == 0xbf) ) { + encoding = g_strdup("UTF-8"); + encSkip = 3; + } + + if ( encSkip ) { + memmove( firstFew, firstFew + encSkip, (some - encSkip) ); + some -= encSkip; + } + + firstFewLen = some; + retVal = 0; // no error + } + } + if(load_entities) { + this->cachedData = std::string(""); + this->cachedPos = 0; + + // First get data from file in typical way (cache it all) + char *buffer = new char [4096]; + while(true) { + int len = this->read(buffer, 4096); + if(len <= 0) break; + buffer[len] = 0; + this->cachedData += buffer; + } + delete[] buffer; + + // Check for SYSTEM or PUBLIC entities and remove them from the cache + GMatchInfo *info; + gint start, end; + + GRegex *regex = g_regex_new( + "<!ENTITY\\s+[^>\\s]+\\s+(SYSTEM|PUBLIC\\s+\"[^>\"]+\")\\s+\"[^>\"]+\"\\s*>", + G_REGEX_CASELESS, G_REGEX_MATCH_NEWLINE_ANY, nullptr); + + g_regex_match (regex, this->cachedData.c_str(), G_REGEX_MATCH_NEWLINE_ANY, &info); + + while (g_match_info_matches (info)) { + if (g_match_info_fetch_pos (info, 1, &start, &end)) + this->cachedData.erase(start, end - start); + g_match_info_next (info, nullptr); + } + g_match_info_free(info); + g_regex_unref(regex); + } + // Do this after loading cache, so reads don't return cache to fill cache. + this->LoadEntities = load_entities; + return retVal; +} + +xmlDocPtr XmlSource::readXml() +{ + int parse_options = XML_PARSE_HUGE | XML_PARSE_RECOVER; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool allowNetAccess = prefs->getBool("/options/externalresources/xml/allow_net_access", false); + if (!allowNetAccess) parse_options |= XML_PARSE_NONET; + + // Allow NOENT only if we're filtering out SYSTEM and PUBLIC entities + if (LoadEntities) parse_options |= XML_PARSE_NOENT; + + auto doc = xmlReadIO( readCb, closeCb, this, + filename, getEncoding(), parse_options); + + if (doc && doc->properties && xmlXIncludeProcessFlags(doc, XML_PARSE_NOXINCNODE) < 0) { + g_warning("XInclude processing failed for %s", filename); + } + + return doc; +} + +int XmlSource::readCb( void * context, char * buffer, int len ) +{ + int retVal = -1; + + if ( context ) { + XmlSource* self = static_cast<XmlSource*>(context); + retVal = self->read( buffer, len ); + } + return retVal; +} + +int XmlSource::closeCb(void * context) +{ + if ( context ) { + XmlSource* self = static_cast<XmlSource*>(context); + self->close(); + } + return 0; +} + +int XmlSource::read( char *buffer, int len ) +{ + int retVal = 0; + size_t got = 0; + + if ( LoadEntities ) { + if (cachedPos >= cachedData.length()) { + return -1; + } else { + retVal = cachedData.copy(buffer, len, cachedPos); + cachedPos += retVal; + return retVal; // Do NOT continue. + } + } else if ( firstFewLen > 0 ) { + int some = (len < firstFewLen) ? len : firstFewLen; + memcpy( buffer, firstFew, some ); + if ( len < firstFewLen ) { + memmove( firstFew, firstFew + some, (firstFewLen - some) ); + } + firstFewLen -= some; + got = some; + } else if ( gzin ) { + int single = 0; + while ( (static_cast<int>(got) < len) && (single >= 0) ) + { + single = gzin->get(); + if ( single >= 0 ) { + buffer[got++] = 0x0ff & single; + } else { + break; + } + } + } else { + got = fread( buffer, 1, len, fp ); + } + + if ( feof(fp) ) { + retVal = got; + } else if ( ferror(fp) ) { + retVal = -1; + } else { + retVal = got; + } + + return retVal; +} + +int XmlSource::close() +{ + if ( gzin ) { + gzin->close(); + delete gzin; + gzin = nullptr; + } + if ( instr ) { + instr->close(); + fp = nullptr; + delete instr; + instr = nullptr; + } + if ( fp ) { + fclose(fp); + fp = nullptr; + } + return 0; +} + +/** + * Reads XML from a file, and returns the Document. + * The default namespace can also be specified, if desired. + */ +Document *sp_repr_read_file (const gchar * filename, const gchar *default_ns) +{ + xmlDocPtr doc = nullptr; + Document * rdoc = nullptr; + + xmlSubstituteEntitiesDefault(1); + + g_return_val_if_fail(filename != nullptr, NULL); + if (!Inkscape::IO::file_test(filename, G_FILE_TEST_EXISTS)) { + g_warning("Can't open file: %s (doesn't exist)", filename); + return nullptr; + } + /* fixme: A file can disappear at any time, including between now and when we actually try to + * open it. Get rid of the above test once we're sure that we correctly handle + * non-existence. */ + + // TODO: bulia, please look over + gsize bytesRead = 0; + gsize bytesWritten = 0; + GError* error = nullptr; + // TODO: need to replace with our own fopen and reading + gchar* localFilename = g_filename_from_utf8(filename, -1, &bytesRead, &bytesWritten, &error); + g_return_val_if_fail(localFilename != nullptr, NULL); + + Inkscape::IO::dump_fopen_call(filename, "N"); + + XmlSource src; + + if (src.setFile(filename) == 0) { + doc = src.readXml(); + rdoc = sp_repr_do_read(doc, default_ns); + // For some reason, failed ns loading results in this + // We try a system check version of load with NOENT for adobe + if (rdoc && strcmp(rdoc->root()->name(), "ns:svg") == 0) { + xmlFreeDoc(doc); + src.setFile(filename, true); + doc = src.readXml(); + rdoc = sp_repr_do_read(doc, default_ns); + } + } + + if (doc) { + xmlFreeDoc(doc); + } + + if (localFilename) { + g_free(localFilename); + } + + return rdoc; +} + +/** + * Reads and parses XML from a buffer, returning it as an Document + */ +Document *sp_repr_read_mem (const gchar * buffer, gint length, const gchar *default_ns) +{ + xmlDocPtr doc; + Document * rdoc; + + xmlSubstituteEntitiesDefault(1); + + g_return_val_if_fail (buffer != nullptr, NULL); + + int parser_options = XML_PARSE_HUGE | XML_PARSE_RECOVER; + parser_options |= XML_PARSE_NONET; // TODO: should we allow network access? + // proper solution would be to check the preference "/options/externalresources/xml/allow_net_access" + // as done in XmlSource::readXml which gets called by the analogous sp_repr_read_file() + // but sp_repr_read_mem() seems to be called in locations where Inkscape::Preferences::get() fails badly + doc = xmlReadMemory (const_cast<gchar *>(buffer), length, nullptr, nullptr, parser_options); + + rdoc = sp_repr_do_read (doc, default_ns); + if (doc) { + xmlFreeDoc (doc); + } + return rdoc; +} + +/** + * Reads and parses XML from a buffer, returning it as an Document + */ +Document *sp_repr_read_buf (const Glib::ustring &buf, const gchar *default_ns) +{ + return sp_repr_read_mem(buf.c_str(), buf.size(), default_ns); +} + + +namespace Inkscape { + +struct compare_quark_ids { + bool operator()(Glib::QueryQuark const &a, Glib::QueryQuark const &b) const { + return a.id() < b.id(); + } +}; + +} + +namespace { + +typedef std::map<Glib::QueryQuark, Glib::QueryQuark, Inkscape::compare_quark_ids> PrefixMap; + +Glib::QueryQuark qname_prefix(Glib::QueryQuark qname) { + static PrefixMap prefix_map; + PrefixMap::iterator iter = prefix_map.find(qname); + if ( iter != prefix_map.end() ) { + return (*iter).second; + } else { + gchar const *name_string=g_quark_to_string(qname); + gchar const *prefix_end=strchr(name_string, ':'); + if (prefix_end) { + Glib::Quark prefix=Glib::ustring(name_string, prefix_end); + prefix_map.insert(PrefixMap::value_type(qname, prefix)); + return prefix; + } else { + return GQuark(0); + } + } +} + +} + +namespace { + +void promote_to_namespace(Node *repr, const gchar *prefix) { + if ( repr->type() == Inkscape::XML::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); + } + } +} + +} + +/** + * Reads in a XML file to create a Document + */ +Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns) +{ + if (doc == nullptr) { + return nullptr; + } + xmlNodePtr node=xmlDocGetRootElement (doc); + if (node == nullptr) { + return nullptr; + } + + std::map<std::string, std::string> prefix_map; + + Document *rdoc = new Inkscape::XML::SimpleDocument(); + + Node *root=nullptr; + for ( node = doc->children ; node != nullptr ; node = node->next ) { + if (node->type == XML_ELEMENT_NODE) { + Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map); + rdoc->appendChild(repr); + Inkscape::GC::release(repr); + + if (!root) { + root = repr; + } else { + root = nullptr; + break; + } + } else if ( node->type == XML_COMMENT_NODE || node->type == XML_PI_NODE ) { + Node *repr=sp_repr_svg_read_node(rdoc, node, default_ns, prefix_map); + rdoc->appendChild(repr); + Inkscape::GC::release(repr); + } + } + + if (root != nullptr) { + /* promote elements of some XML documents that don't use namespaces + * into their default namespace */ + if ( default_ns && !strchr(root->name(), ':') ) { + if ( !strcmp(default_ns, SP_SVG_NS_URI) ) { + promote_to_namespace(root, "svg"); + } + if ( !strcmp(default_ns, INKSCAPE_EXTENSION_URI) ) { + promote_to_namespace(root, INKSCAPE_EXTENSION_NS_NC); + } + } + + + // Clean unnecessary attributes and style properties from SVG documents. (Controlled by + // preferences.) Note: internal Inkscape svg files will also be cleaned (filters.svg, + // icons.svg). How can one tell if a file is internal? + if ( !strcmp(root->name(), "svg:svg" ) ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool clean = prefs->getBool("/options/svgoutput/check_on_reading"); + if( clean ) { + sp_attribute_clean_tree( root ); + } + } + } + + return rdoc; +} + +gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar */*default_ns*/, std::map<std::string, std::string> &prefix_map) +{ + const xmlChar *prefix; + if (ns){ + if (ns->href ) { + prefix = reinterpret_cast<const xmlChar*>( sp_xml_ns_uri_prefix(reinterpret_cast<const gchar*>(ns->href), + reinterpret_cast<const char*>(ns->prefix)) ); + prefix_map[reinterpret_cast<const char*>(prefix)] = reinterpret_cast<const char*>(ns->href); + } + else { + prefix = nullptr; + } + } + else { + prefix = nullptr; + } + + if (prefix) { + return g_snprintf (p, len, "%s:%s", reinterpret_cast<const gchar*>(prefix), name); + } else { + return g_snprintf (p, len, "%s", name); + } +} + +static Node *sp_repr_svg_read_node (Document *xml_doc, xmlNodePtr node, const gchar *default_ns, std::map<std::string, std::string> &prefix_map) +{ + xmlAttrPtr prop; + xmlNodePtr child; + gchar c[256]; + + if (node->type == XML_TEXT_NODE || node->type == XML_CDATA_SECTION_NODE) { + + if (node->content == nullptr || *(node->content) == '\0') { + return nullptr; // empty text node + } + + // Since libxml2 2.9.0, only element nodes are checked, thus check parent. + // Note: this only handles XML's rules for white space. SVG's specific rules + // are handled in sp-string.cpp. + bool preserve = (xmlNodeGetSpacePreserve (node->parent) == 1); + + xmlChar *p; + for (p = node->content; *p && g_ascii_isspace (*p) && !preserve; p++) + ; // skip all whitespace + + if (!(*p)) { // this is an all-whitespace node, and preserve == default + return nullptr; // we do not preserve all-whitespace nodes unless we are asked to + } + + // We keep track of original node type so that CDATA sections are preserved on output. + return xml_doc->createTextNode(reinterpret_cast<gchar *>(node->content), + node->type == XML_CDATA_SECTION_NODE ); + } + + if (node->type == XML_COMMENT_NODE) { + return xml_doc->createComment(reinterpret_cast<gchar *>(node->content)); + } + + if (node->type == XML_PI_NODE) { + return xml_doc->createPI(reinterpret_cast<const gchar *>(node->name), + reinterpret_cast<const gchar *>(node->content)); + } + + if (node->type == XML_ENTITY_DECL) { + return nullptr; + } + + sp_repr_qualified_name (c, 256, node->ns, node->name, default_ns, prefix_map); + Node *repr = xml_doc->createElement(c); + /* TODO remember node->ns->prefix if node->ns != NULL */ + + for (prop = node->properties; prop != nullptr; prop = prop->next) { + if (prop->children) { + sp_repr_qualified_name (c, 256, prop->ns, prop->name, default_ns, prefix_map); + repr->setAttribute(c, reinterpret_cast<gchar*>(prop->children->content)); + /* TODO remember prop->ns->prefix if prop->ns != NULL */ + } + } + + if (node->content) { + repr->setContent(reinterpret_cast<gchar*>(node->content)); + } + + for (child = node->xmlChildrenNode; child != nullptr; child = child->next) { + Node *crepr = sp_repr_svg_read_node (xml_doc, child, default_ns, prefix_map); + if (crepr) { + repr->appendChild(crepr); + Inkscape::GC::release(crepr); + } + } + + return repr; +} + + +static void sp_repr_save_writer(Document *doc, Inkscape::IO::Writer *out, + gchar const *default_ns, + gchar const *old_href_abs_base, + gchar const *new_href_abs_base) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool inlineattrs = prefs->getBool("/options/svgoutput/inlineattrs"); + int indent = prefs->getInt("/options/svgoutput/indent", 2); + + /* fixme: do this The Right Way */ + out->writeString( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" ); + + const gchar *str = static_cast<Node *>(doc)->attribute("doctype"); + if (str) { + out->writeString( str ); + } + + for (Node *repr = sp_repr_document_first_child(doc); + repr; repr = repr->next()) + { + Inkscape::XML::NodeType const node_type = repr->type(); + if ( node_type == Inkscape::XML::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; + 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 : |