Code

applied patch #1348672 from enchanter
[inkscape.git] / src / xml / repr-io.cpp
1 #define __SP_REPR_IO_C__
3 /*
4  * Dirty DOM-like  tree
5  *
6  * Authors:
7  *   Lauris Kaplinski <lauris@kaplinski.com>
8  *   bulia byak <buliabyak@users.sf.net>
9  *
10  * Copyright (C) 1999-2002 Lauris Kaplinski
11  *
12  * Released under GNU GPL, read the file 'COPYING' for more information
13  */
15 #ifdef HAVE_CONFIG_H
16 # include <config.h>
17 #endif
21 #include "xml/repr.h"
22 #include "xml/attribute-record.h"
24 #include "io/sys.h"
25 #include "io/uristream.h"
26 #include "io/gzipstream.h"
29 using Inkscape::IO::Writer;
30 using Inkscape::Util::List;
31 using Inkscape::Util::cons;
32 using Inkscape::XML::Document;
33 using Inkscape::XML::Node;
34 using Inkscape::XML::AttributeRecord;
36 static Document *sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns);
37 static Node *sp_repr_svg_read_node (xmlNodePtr node, const gchar *default_ns, GHashTable *prefix_map);
38 static gint sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar *default_ns, GHashTable *prefix_map);
39 static void sp_repr_write_stream_root_element (Node *repr, Writer &out, gboolean add_whitespace, gchar const *default_ns);
40 static void sp_repr_write_stream (Node *repr, Writer &out, gint indent_level, gboolean add_whitespace, Glib::QueryQuark elide_prefix);
41 static void sp_repr_write_stream_element (Node *repr, Writer &out, gint indent_level, gboolean add_whitespace, Glib::QueryQuark elide_prefix, List<AttributeRecord const> attributes);
43 #ifdef HAVE_LIBWMF
44 static xmlDocPtr sp_wmf_convert (const char * file_name);
45 static char * sp_wmf_image_name (void * context);
46 #endif /* HAVE_LIBWMF */
49 class XmlSource
50 {
51 public:
52     XmlSource()
53         : filename(0),
54           fp(0),
55           first(false),
56           dummy("x"),
57           instr(0),
58           gzin(0)
59     {
60     }
61     virtual ~XmlSource()
62     {
63         close();
64     }
66     void setFile( char const * filename );
68     static int readCb( void * context, char * buffer, int len);
69     static int closeCb(void * context);
72     int read( char * buffer, int len );
73     int close();
74 private:
75     const char* filename;
76     FILE* fp;
77     bool first;
78     Inkscape::URI dummy;
79     Inkscape::IO::UriInputStream* instr;
80     Inkscape::IO::GzipInputStream* gzin;
81 };
83 void XmlSource::setFile( char const * filename ) {
84     this->filename = filename;
85     fp = Inkscape::IO::fopen_utf8name(filename, "r");
86     first = true;
87 }
90 int XmlSource::readCb( void * context, char * buffer, int len )
91 {
92     int retVal = -1;
93     if ( context ) {
94         XmlSource* self = static_cast<XmlSource*>(context);
95         retVal = self->read( buffer, len );
96     }
97     return retVal;
98 }
100 int XmlSource::closeCb(void * context)
102     if ( context ) {
103         XmlSource* self = static_cast<XmlSource*>(context);
104         self->close();
105     }
106     return 0;
109 int XmlSource::read( char *buffer, int len )
111     int retVal = 0;
112     size_t got = 0;
114     if ( first ) {
115         first = false;
116         char tmp[] = {0,0};
117         size_t some = fread( tmp, 1, 2, fp );
119         if ( (some >= 2) && (tmp[0] == 0x1f) && ((unsigned char)(tmp[1]) == 0x8b) ) {
120             //g_message(" the file being read is gzip'd. extract it");
121             fclose(fp);
122             fp = 0;
123             fp = Inkscape::IO::fopen_utf8name(filename, "r");
124             instr = new Inkscape::IO::UriInputStream(fp, dummy);
125             gzin = new Inkscape::IO::GzipInputStream(*instr);
126             int single = 0;
127             while ( (int)got < len && single >= 0 )
128             {
129                 single = gzin->get();
130                 if ( single >= 0 ) {
131                     buffer[got++] = 0x0ff & single;
132                 } else {
133                     break;
134                 }
135             }
136             //g_message(" extracted %d bytes this pass", got );
137         } else {
138             memcpy( buffer, tmp, some );
139             got = some;
140         }
141     } else if ( gzin ) {
142         int single = 0;
143         while ( (int)got < len && single >= 0 )
144         {
145             single = gzin->get();
146             if ( single >= 0 ) {
147                 buffer[got++] = 0x0ff & single;
148             } else {
149                 break;
150             }
151         }
152         //g_message(" extracted %d bytes this pass  b", got );
153     } else {
154         got = fread( buffer, 1, len, fp );
155     }
157     if ( feof(fp) ) {
158         retVal = got;
159     }
160     else if ( ferror(fp) ) {
161         retVal = -1;
162     }
163     else {
164         retVal = got;
165     }
167     return retVal;
170 int XmlSource::close()
172     if ( gzin ) {
173         gzin->close();
174         delete gzin;
175         gzin = 0;
176     }
177     if ( instr ) {
178         instr->close();
179         fp = 0;
180         delete instr;
181         instr = 0;
182     }
183     if ( fp ) {
184         fclose(fp);
185         fp = 0;
186     }
187     return 0;
190 /**
191  * Reads XML from a file, including WMF files, and returns the Document.
192  * The default namespace can also be specified, if desired.
193  */
194 Document *
195 sp_repr_read_file (const gchar * filename, const gchar *default_ns)
197     xmlDocPtr doc = 0;
198     Document * rdoc = 0;
200     xmlSubstituteEntitiesDefault(1);
202     g_return_val_if_fail (filename != NULL, NULL);
203     g_return_val_if_fail (Inkscape::IO::file_test( filename, G_FILE_TEST_EXISTS ), NULL);
205     // TODO: bulia, please look over
206     gsize bytesRead = 0;
207     gsize bytesWritten = 0;
208     GError* error = NULL;
209     // TODO: need to replace with our own fopen and reading
210     gchar* localFilename = g_filename_from_utf8 ( filename,
211                                  -1,  &bytesRead,  &bytesWritten, &error);
212     g_return_val_if_fail( localFilename != NULL, NULL );
214     Inkscape::IO::dump_fopen_call( filename, "N" );
216     XmlSource src;
217     src.setFile(filename);
219     xmlDocPtr doubleDoc = xmlReadIO( XmlSource::readCb,
220                                      XmlSource::closeCb,
221                                      &src,
222                                      localFilename,
223                                      NULL, //"UTF-8",
224                                      XML_PARSE_NOENT );
227 #ifdef HAVE_LIBWMF
228     if (strlen (localFilename) > 4) {
229         if ( (strcmp (localFilename + strlen (localFilename) - 4,".wmf") == 0)
230           || (strcmp (localFilename + strlen (localFilename) - 4,".WMF") == 0))
231             doc = sp_wmf_convert (localFilename);
232         else
233             doc = xmlParseFile (localFilename);
234     }
235     else {
236         doc = xmlParseFile (localFilename);
237     }
238 #else /* !HAVE_LIBWMF */
239     //doc = xmlParseFile (localFilename);
240 #endif /* !HAVE_LIBWMF */
242     //rdoc = sp_repr_do_read (doc, default_ns);
243     rdoc = sp_repr_do_read (doubleDoc, default_ns);
244     if (doc)
245         xmlFreeDoc (doc);
247     if ( localFilename != NULL )
248         g_free (localFilename);
250     if ( doubleDoc != NULL )
251     {
252         xmlFreeDoc( doubleDoc );
253     }
255     return rdoc;
258 /**
259  * Reads and parses XML from a buffer, returning it as an Document
260  */
261 Document *
262 sp_repr_read_mem (const gchar * buffer, gint length, const gchar *default_ns)
264     xmlDocPtr doc;
265     Document * rdoc;
267     xmlSubstituteEntitiesDefault(1);
269     g_return_val_if_fail (buffer != NULL, NULL);
271     doc = xmlParseMemory ((gchar *) buffer, length);
273     rdoc = sp_repr_do_read (doc, default_ns);
274     if (doc)
275         xmlFreeDoc (doc);
276     return rdoc;
279 namespace Inkscape {
281 struct compare_quark_ids {
282     bool operator()(Glib::QueryQuark const &a, Glib::QueryQuark const &b) const {
283         return a.id() < b.id();
284     }
285 };
289 namespace {
291 typedef std::map<Glib::QueryQuark, Glib::QueryQuark, Inkscape::compare_quark_ids> PrefixMap;
293 Glib::QueryQuark qname_prefix(Glib::QueryQuark qname) {
294     static PrefixMap prefix_map;
295     PrefixMap::iterator iter = prefix_map.find(qname);
296     if ( iter != prefix_map.end() ) {
297         return (*iter).second;
298     } else {
299         gchar const *name_string=g_quark_to_string(qname);
300         gchar const *prefix_end=strchr(name_string, ':');
301         if (prefix_end) {
302             Glib::Quark prefix=Glib::ustring(name_string, prefix_end);
303             prefix_map.insert(PrefixMap::value_type(qname, prefix));
304             return prefix;
305         } else {
306             return GQuark(0);
307         }
308     }
313 namespace {
315 void promote_to_svg_namespace(Node *repr) {
316     if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) {
317         GQuark code = repr->code();
318         if (!qname_prefix(code).id()) {
319             gchar *svg_name = g_strconcat("svg:", g_quark_to_string(code), NULL);
320             repr->setCodeUnsafe(g_quark_from_string(svg_name));
321             g_free(svg_name);
322         }
323         for ( Node *child = sp_repr_children(repr) ; child ; child = sp_repr_next(child) ) {
324             promote_to_svg_namespace(child);
325         }
326     }
331 /**
332  * Reads in a XML file to create a Document
333  */
334 Document *
335 sp_repr_do_read (xmlDocPtr doc, const gchar *default_ns)
337     if (doc == NULL) return NULL;
338     xmlNodePtr node=xmlDocGetRootElement (doc);
339     if (node == NULL) return NULL;
341     GHashTable * prefix_map;
342     prefix_map = g_hash_table_new (g_str_hash, g_str_equal);
344     GSList *reprs=NULL;
345     Node *root=NULL;
347     for ( node = doc->children ; node != NULL ; node = node->next ) {
348         if (node->type == XML_ELEMENT_NODE) {
349             Node *repr=sp_repr_svg_read_node (node, default_ns, prefix_map);
350             reprs = g_slist_append(reprs, repr);
352             if (!root) {
353                 root = repr;
354             } else {
355                 root = NULL;
356                 break;
357             }
358         } else if ( node->type == XML_COMMENT_NODE ) {
359             Node *comment=sp_repr_svg_read_node(node, default_ns, prefix_map);
360             reprs = g_slist_append(reprs, comment);
361         }
362     }
364     Document *rdoc=NULL;
366     if (root != NULL) {
367         /* promote elements of SVG documents that don't use namespaces
368          * into the SVG namespace */
369         if ( default_ns && !strcmp(default_ns, SP_SVG_NS_URI)
370              && !strcmp(root->name(), "svg") )
371         {
372             promote_to_svg_namespace(root);
373         }
375         rdoc = sp_repr_document_new_list(reprs);
376     }
378     for ( GSList *iter = reprs ; iter ; iter = iter->next ) {
379         Node *repr=(Node *)iter->data;
380         Inkscape::GC::release(repr);
381     }
382     g_slist_free(reprs);
384     g_hash_table_destroy (prefix_map);
386     return rdoc;
389 gint
390 sp_repr_qualified_name (gchar *p, gint len, xmlNsPtr ns, const xmlChar *name, const gchar *default_ns, GHashTable *prefix_map)
392     const xmlChar *prefix;
393     if ( ns && ns->href ) {
394         prefix = (xmlChar*)sp_xml_ns_uri_prefix ((gchar*)ns->href, (char*)ns->prefix);
395         g_hash_table_insert (prefix_map, (gpointer)prefix, (gpointer)ns->href);
396     } else {
397         prefix = NULL;
398     }
400     if (prefix)
401         return g_snprintf (p, len, "%s:%s", (gchar*)prefix, name);
402     else
403         return g_snprintf (p, len, "%s", name);
406 static Node *
407 sp_repr_svg_read_node (xmlNodePtr node, const gchar *default_ns, GHashTable *prefix_map)
409     Node *repr, *crepr;
410     xmlAttrPtr prop;
411     xmlNodePtr child;
412     gchar c[256];
414     if (node->type == XML_TEXT_NODE || node->type == XML_CDATA_SECTION_NODE) {
416         if (node->content == NULL || *(node->content) == '\0')
417             return NULL; // empty text node
419         bool preserve = (xmlNodeGetSpacePreserve (node) == 1);
421         xmlChar *p;
422         for (p = node->content; *p && g_ascii_isspace (*p) && !preserve; p++)
423             ; // skip all whitespace
425         if (!(*p)) { // this is an all-whitespace node, and preserve == default
426             return NULL; // we do not preserve all-whitespace nodes unless we are asked to
427         }
429         Node *rdoc = sp_repr_new_text((const gchar *)node->content);
430         return rdoc;
431     }
433     if (node->type == XML_COMMENT_NODE)
434         return sp_repr_new_comment((const gchar *)node->content);
436     if (node->type == XML_ENTITY_DECL) return NULL;
438     sp_repr_qualified_name (c, 256, node->ns, node->name, default_ns, prefix_map);
439     repr = sp_repr_new (c);
440     /* TODO remember node->ns->prefix if node->ns != NULL */
442     for (prop = node->properties; prop != NULL; prop = prop->next) {
443         if (prop->children) {
444             sp_repr_qualified_name (c, 256, prop->ns, prop->name, default_ns, prefix_map);
445             repr->setAttribute(c, (gchar*)prop->children->content);
446             /* TODO remember prop->ns->prefix if prop->ns != NULL */
447         }
448     }
450     if (node->content)
451         repr->setContent((gchar*)node->content);
453     child = node->xmlChildrenNode;
454     for (child = node->xmlChildrenNode; child != NULL; child = child->next) {
455         crepr = sp_repr_svg_read_node (child, default_ns, prefix_map);
456         if (crepr) {
457             repr->appendChild(crepr);
458             Inkscape::GC::release(crepr);
459         }
460     }
462     return repr;
465 void
466 sp_repr_save_stream (Document *doc, FILE *fp, gchar const *default_ns, bool compress)
468     Node *repr;
469     const gchar *str;
471     Inkscape::URI dummy("x");
472     Inkscape::IO::UriOutputStream bout(fp, dummy);
473     Inkscape::IO::GzipOutputStream *gout = compress ? new Inkscape::IO::GzipOutputStream(bout) : NULL;
474     Inkscape::IO::OutputStreamWriter *out  = compress ? new Inkscape::IO::OutputStreamWriter( *gout ) : new Inkscape::IO::OutputStreamWriter( bout );
476     /* fixme: do this The Right Way */
478     out->writeString( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" );
480     str = ((Node *)doc)->attribute("doctype");
481     if (str) {
482         out->writeString( str );
483     }
485     repr = sp_repr_document_first_child(doc);
486     for ( repr = sp_repr_document_first_child(doc) ;
487           repr ; repr = sp_repr_next(repr) )
488     {
489         if ( repr->type() == Inkscape::XML::ELEMENT_NODE ) {
490             sp_repr_write_stream_root_element(repr, *out, TRUE, default_ns);
491         } else if ( repr->type() == Inkscape::XML::COMMENT_NODE ) {
492             sp_repr_write_stream(repr, *out, 0, TRUE, GQuark(0));
493             out->writeChar( '\n' );
494         } else {
495             sp_repr_write_stream(repr, *out, 0, TRUE, GQuark(0));
496         }
497     }
498     if ( out ) {
499         delete out;
500         out = NULL;
501     }
502     if ( gout ) {
503         delete gout;
504         gout = NULL;
505     }
508 /* Returns TRUE if file successfully saved; FALSE if not
509  */
510 gboolean
511 sp_repr_save_file (Document *doc, const gchar *filename,
512                    gchar const *default_ns)
514     if (filename == NULL) {
515         return FALSE;
516     }
517     bool compress = false;
518     {
519         if (strlen (filename) > 5) {
520             gchar tmp[] = {0,0,0,0,0,0};
521             strncpy( tmp, filename + strlen (filename) - 5, 6 );
522             tmp[5] = 0;
523             if ( strcasecmp(".svgz", tmp ) == 0 )
524             {
525                 //g_message("TIME TO COMPRESS THE OUTPUT FOR SVGZ");
526                 compress = true;
527             }
528         }
529     }
531     Inkscape::IO::dump_fopen_call( filename, "B" );
532     FILE *file = Inkscape::IO::fopen_utf8name(filename, "w");
533     if (file == NULL) {
534         return FALSE;
535     }
537     sp_repr_save_stream (doc, file, default_ns, compress);
539     if (fclose (file) != 0) {
540         return FALSE;
541     }
543     return TRUE;
546 void
547 sp_repr_print (Node * repr)
549     Inkscape::IO::StdOutputStream bout;
550     Inkscape::IO::OutputStreamWriter out(bout);
552     sp_repr_write_stream (repr, out, 0, TRUE, GQuark(0));
554     return;
557 /* (No doubt this function already exists elsewhere.) */
558 static void
559 repr_quote_write (Writer &out, const gchar * val)
561     if (!val) return;
563     for (; *val != '\0'; val++) {
564         switch (*val) {
565         case '"': out.writeString( "&quot;" ); break;
566         case '&': out.writeString( "&amp;" ); break;
567         case '<': out.writeString( "&lt;" ); break;
568         case '>': out.writeString( "&gt;" ); break;
569         default: out.writeChar( *val ); break;
570         }
571     }
574 namespace {
576 typedef std::map<Glib::QueryQuark, gchar const *, Inkscape::compare_quark_ids> LocalNameMap;
577 typedef std::map<Glib::QueryQuark, Inkscape::Util::SharedCStringPtr, Inkscape::compare_quark_ids> NSMap;
579 gchar const *qname_local_name(Glib::QueryQuark qname) {
580     static LocalNameMap local_name_map;
581     LocalNameMap::iterator iter = local_name_map.find(qname);
582     if ( iter != local_name_map.end() ) {
583         return (*iter).second;
584     } else {
585         gchar const *name_string=g_quark_to_string(qname);
586         gchar const *prefix_end=strchr(name_string, ':');
587         if (prefix_end) {
588             return prefix_end + 1;
589         } else {
590             return name_string;
591         }
592     }
595 void add_ns_map_entry(NSMap &ns_map, Glib::QueryQuark prefix) {
596     using Inkscape::Util::SharedCStringPtr;
598     static const Glib::QueryQuark xml_prefix("xml");
600     NSMap::iterator iter=ns_map.find(prefix);
601     if ( iter == ns_map.end() ) {
602         if (prefix.id()) {
603             gchar const *uri=sp_xml_ns_prefix_uri(g_quark_to_string(prefix));
604             if (uri) {
605                 ns_map.insert(NSMap::value_type(prefix, SharedCStringPtr::coerce(uri)));
606             } else if ( prefix != xml_prefix ) {
607                 g_warning("No namespace known for normalized prefix %s", g_quark_to_string(prefix));
608             }
609         } else {
610             ns_map.insert(NSMap::value_type(prefix, SharedCStringPtr()));
611         }
612     }
615 void populate_ns_map(NSMap &ns_map, Node &repr) {
616     if ( repr.type() == Inkscape::XML::ELEMENT_NODE ) {
617         add_ns_map_entry(ns_map, qname_prefix(repr.code()));
618         for ( List<AttributeRecord const> iter=repr.attributeList() ;
619               iter ; ++iter )
620         {
621             Glib::QueryQuark prefix=qname_prefix(iter->key);
622             if (prefix.id()) {
623                 add_ns_map_entry(ns_map, prefix);
624             }
625         }
626         for ( Node *child=sp_repr_children(&repr) ;
627               child ; child = sp_repr_next(child) )
628         {
629             populate_ns_map(ns_map, *child);
630         }
631     }
636 void
637 sp_repr_write_stream_root_element (Node *repr, Writer &out, gboolean add_whitespace, gchar const *default_ns)
639     using Inkscape::Util::SharedCStringPtr;
640     g_assert(repr != NULL);
641     Glib::QueryQuark xml_prefix=g_quark_from_static_string("xml");
643     NSMap ns_map;
644     populate_ns_map(ns_map, *repr);
646     Glib::QueryQuark elide_prefix=GQuark(0);
647     if ( default_ns && ns_map.find(GQuark(0)) == ns_map.end() ) {
648         elide_prefix = g_quark_from_string(sp_xml_ns_uri_prefix(default_ns, NULL));
649     }
651     List<AttributeRecord const> attributes=repr->attributeList();
652     for ( NSMap::iterator iter=ns_map.begin() ; iter != ns_map.end() ; ++iter ) 
653     {
654         Glib::QueryQuark prefix=(*iter).first;
655         SharedCStringPtr ns_uri=(*iter).second;
657         if (prefix.id()) {
658             if ( prefix != xml_prefix ) {
659                 if ( elide_prefix == prefix ) {
660                     attributes = cons(AttributeRecord(g_quark_from_static_string("xmlns"), ns_uri), attributes);
661                 }
663                 Glib::ustring attr_name="xmlns:";
664                 attr_name.append(g_quark_to_string(prefix));
665                 GQuark key = g_quark_from_string(attr_name.c_str());
666                 attributes = cons(AttributeRecord(key, ns_uri), attributes);
667             }
668         } else {
669             // if there are non-namespaced elements, we can't globally
670             // use a default namespace
671             elide_prefix = GQuark(0);
672         }
673     }
675     return sp_repr_write_stream_element(repr, out, 0, add_whitespace, elide_prefix, attributes);
678 void
679 sp_repr_write_stream (Node *repr, Writer &out, gint indent_level,
680                       gboolean add_whitespace, Glib::QueryQuark elide_prefix)
682     if (repr->type() == Inkscape::XML::TEXT_NODE) {
683         repr_quote_write (out, repr->content());
684     } else if (repr->type() == Inkscape::XML::COMMENT_NODE) {
685         out.printf( "<!--%s-->", repr->content() );
686     } else if (repr->type() == Inkscape::XML::ELEMENT_NODE) {
687         sp_repr_write_stream_element(repr, out, indent_level, add_whitespace, elide_prefix, repr->attributeList());
688     } else {
689         g_assert_not_reached();
690     }
693 void
694 sp_repr_write_stream_element (Node * repr, Writer & out, gint indent_level,
695                               gboolean add_whitespace,
696                               Glib::QueryQuark elide_prefix,
697                               List<AttributeRecord const> attributes)
699     Node *child;
700     gboolean loose;
701     gint i;
703     g_return_if_fail (repr != NULL);
705     if ( indent_level > 16 )
706         indent_level = 16;
708     if (add_whitespace) {
709         for ( i = 0 ; i < indent_level ; i++ ) {
710             out.writeString( "  " );
711         }
712     }
714     GQuark code = repr->code();
715     gchar const *element_name;
716     if ( elide_prefix == qname_prefix(code) ) {
717         element_name = qname_local_name(code);
718     } else {
719         element_name = g_quark_to_string(code);
720     }
721     out.printf( "<%s", element_name );
723     // if this is a <text> element, suppress formatting whitespace
724     // for its content and children:
725     gchar const *xml_space_attr = repr->attribute("xml:space");
726     if (xml_space_attr != NULL && !strcmp(xml_space_attr, "preserve")) {
727         add_whitespace = FALSE;
728     }
730     for ( List<AttributeRecord const> iter = attributes ;
731           iter ; ++iter )
732     {
733         out.writeString("\n");
734         for ( i = 0 ; i < indent_level + 1 ; i++ ) {
735             out.writeString("  ");
736         }
737         out.printf(" %s=\"", g_quark_to_string(iter->key));
738         repr_quote_write(out, iter->value);
739         out.writeChar('"');
740     }
742     loose = TRUE;
743     for (child = repr->firstChild() ; child != NULL; child = child->next()) {
744         if (child->type() == Inkscape::XML::TEXT_NODE) {
745             loose = FALSE;
746             break;
747         }
748     }
749     if (repr->firstChild()) {
750         out.writeString( ">" );
751         if (loose && add_whitespace) {
752             out.writeString( "\n" );
753         }
754         for (child = repr->firstChild(); child != NULL; child = child->next()) {
755             sp_repr_write_stream (child, out, (loose) ? (indent_level + 1) : 0, add_whitespace, elide_prefix);
756         }
758         if (loose && add_whitespace) {
759             for (i = 0; i < indent_level; i++) {
760                 out.writeString( "  " );
761             }
762         }
763         out.printf( "</%s>", element_name );
764     } else {
765         out.writeString( " />" );
766     }
768     // text elements cannot nest, so we can output newline
769     // after closing text
771     if (add_whitespace || !strcmp (repr->name(), "svg:text")) {
772         out.writeString( "\n" );
773     }
777 /*
778   Local Variables:
779   mode:c++
780   c-file-style:"stroustrup"
781   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
782   indent-tabs-mode:nil
783   fill-column:99
784   End:
785 */
786 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:encoding=utf-8:textwidth=99 :