/* This file is part of the KDE project Copyright (C) 2004 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoXmlWriter.h" #include // kMin #include #include #include static const int s_indentBufferLength = 100; KoXmlWriter::KoXmlWriter( TQIODevice* dev, int indentLevel ) : m_dev( dev ), m_baseIndentLevel( indentLevel ) { init(); } void KoXmlWriter::init() { m_indentBuffer = new char[ s_indentBufferLength ]; memset( m_indentBuffer, ' ', s_indentBufferLength ); *m_indentBuffer = '\n'; // write newline before indentation, in one go m_escapeBuffer = new char[s_escapeBufferLen]; } KoXmlWriter::~KoXmlWriter() { delete[] m_indentBuffer; delete[] m_escapeBuffer; } void KoXmlWriter::startDocument( const char* rootElemName, const char* publicId, const char* systemId ) { Q_ASSERT( m_tags.isEmpty() ); writeCString( "\n" ); // There isn't much point in a doctype if there's no DTD to refer to // (I'm told that files that are validated by a RelaxNG schema cannot refer to the schema) if ( publicId ) { writeCString( "\n" ); } } void KoXmlWriter::endDocument() { // just to do exactly like TQDom does (newline at end of file). writeChar( '\n' ); Q_ASSERT( m_tags.isEmpty() ); } // returns the value of indentInside of the parent bool KoXmlWriter::prepareForChild() { if ( !m_tags.isEmpty() ) { Tag& parent = m_tags.top(); if ( !parent.hasChildren ) { closeStartElement( parent ); parent.hasChildren = true; parent.lastChildIsText = false; } if ( parent.indentInside ) { writeIndent(); } return parent.indentInside; } return true; } void KoXmlWriter::prepareForTextNode() { Tag& parent = m_tags.top(); if ( !parent.hasChildren ) { closeStartElement( parent ); parent.hasChildren = true; parent.lastChildIsText = true; } } void KoXmlWriter::startElement( const char* tagName, bool indentInside ) { Q_ASSERT( tagName != 0 ); // Tell parent that it has children bool parentIndent = prepareForChild(); m_tags.push( Tag( tagName, parentIndent && indentInside ) ); writeChar( '<' ); writeCString( tagName ); //kdDebug() << k_funcinfo << tagName << endl; } void KoXmlWriter::addCompleteElement( const char* cstr ) { prepareForChild(); writeCString( cstr ); } void KoXmlWriter::addCompleteElement( TQIODevice* indev ) { prepareForChild(); bool openOk = indev->open( IO_ReadOnly ); Q_ASSERT( openOk ); if ( !openOk ) return; static const int MAX_CHUNK_SIZE = 8*1024; // 8 KB TQByteArray buffer(MAX_CHUNK_SIZE); while ( !indev->atEnd() ) { TQ_LONG len = indev->readBlock( buffer.data(), buffer.size() ); if ( len <= 0 ) // e.g. on error break; m_dev->writeBlock( buffer.data(), len ); } } void KoXmlWriter::endElement() { if ( m_tags.isEmpty() ) kdWarning() << "Ouch, endElement() was called more times than startElement(). " "The generated XML will be invalid! " "Please report this bug (by saving the document to another format...)" << endl; Tag tag = m_tags.pop(); //kdDebug() << k_funcinfo << " tagName=" << tag.tagName << " hasChildren=" << tag.hasChildren << endl; if ( !tag.hasChildren ) { writeCString( "/>" ); } else { if ( tag.indentInside && !tag.lastChildIsText ) { writeIndent(); } writeCString( "' ); } } void KoXmlWriter::addTextNode( const char* cstr ) { prepareForTextNode(); char* escaped = escapeForXML( cstr, -1 ); writeCString( escaped ); if(escaped != m_escapeBuffer) delete[] escaped; } void KoXmlWriter::addProcessingInstruction( const char* cstr ) { prepareForTextNode(); writeCString( ""); } void KoXmlWriter::addAttribute( const char* attrName, const char* value ) { writeChar( ' ' ); writeCString( attrName ); writeCString("=\""); char* escaped = escapeForXML( value, -1 ); writeCString( escaped ); if(escaped != m_escapeBuffer) delete[] escaped; writeChar( '"' ); } void KoXmlWriter::addAttribute( const char* attrName, double value ) { TQCString str; str.setNum( value, 'g', DBL_DIG ); addAttribute( attrName, str.data() ); } void KoXmlWriter::addAttributePt( const char* attrName, double value ) { TQCString str; str.setNum( value, 'g', DBL_DIG ); str += "pt"; addAttribute( attrName, str.data() ); } void KoXmlWriter::writeIndent() { // +1 because of the leading '\n' m_dev->writeBlock( m_indentBuffer, kMin( indentLevel() + 1, s_indentBufferLength ) ); } void KoXmlWriter::writeString( const TQString& str ) { // cachegrind says .utf8() is where most of the time is spent TQCString cstr = str.utf8(); m_dev->writeBlock( cstr.data(), cstr.size() - 1 ); } // In case of a reallocation (ret value != m_buffer), the caller owns the return value, // it must delete it (with []) char* KoXmlWriter::escapeForXML( const char* source, int length = -1 ) const { // we're going to be pessimistic on char length; so lets make the outputLength less // the amount one char can take: 6 char* destBoundary = m_escapeBuffer + s_escapeBufferLen - 6; char* destination = m_escapeBuffer; char* output = m_escapeBuffer; const char* src = source; // src moves, source remains for ( ;; ) { if(destination >= destBoundary) { // When we come to realize that our escaped string is going to // be bigger than the escape buffer (this shouldn't happen very often...), // we drop the idea of using it, and we allocate a bigger buffer. // Note that this if() can only be hit once per call to the method. if ( length == -1 ) length = tqstrlen( source ); // expensive... uint newLength = length * 6 + 1; // worst case. 6 is due to " and ' char* buffer = new char[ newLength ]; destBoundary = buffer + newLength; uint amountOfCharsAlreadyCopied = destination - m_escapeBuffer; memcpy( buffer, m_escapeBuffer, amountOfCharsAlreadyCopied ); output = buffer; destination = buffer + amountOfCharsAlreadyCopied; } switch( *src ) { case 60: // < memcpy( destination, "<", 4 ); destination += 4; break; case 62: // > memcpy( destination, ">", 4 ); destination += 4; break; case 34: // " memcpy( destination, """, 6 ); destination += 6; break; #if 0 // needed? case 39: // ' memcpy( destination, "'", 6 ); destination += 6; break; #endif case 38: // & memcpy( destination, "&", 5 ); destination += 5; break; case 0: *destination = '\0'; return output; default: *destination++ = *src++; continue; } ++src; } // NOTREACHED (see case 0) return output; } void KoXmlWriter::addManifestEntry( const TQString& fullPath, const TQString& mediaType ) { startElement( "manifest:file-entry" ); addAttribute( "manifest:media-type", mediaType ); addAttribute( "manifest:full-path", fullPath ); endElement(); } void KoXmlWriter::addConfigItem( const TQString & configName, const TQString& value ) { startElement( "config:config-item" ); addAttribute( "config:name", configName ); addAttribute( "config:type", "string" ); addTextNode( value ); endElement(); } void KoXmlWriter::addConfigItem( const TQString & configName, bool value ) { startElement( "config:config-item" ); addAttribute( "config:name", configName ); addAttribute( "config:type", "boolean" ); addTextNode( value ? "true" : "false" ); endElement(); } void KoXmlWriter::addConfigItem( const TQString & configName, int value ) { startElement( "config:config-item" ); addAttribute( "config:name", configName ); addAttribute( "config:type", "int"); addTextNode(TQString::number( value ) ); endElement(); } void KoXmlWriter::addConfigItem( const TQString & configName, double value ) { startElement( "config:config-item" ); addAttribute( "config:name", configName ); addAttribute( "config:type", "double" ); addTextNode( TQString::number( value ) ); endElement(); } void KoXmlWriter::addConfigItem( const TQString & configName, long value ) { startElement( "config:config-item" ); addAttribute( "config:name", configName ); addAttribute( "config:type", "long" ); addTextNode( TQString::number( value ) ); endElement(); } void KoXmlWriter::addConfigItem( const TQString & configName, short value ) { startElement( "config:config-item" ); addAttribute( "config:name", configName ); addAttribute( "config:type", "short" ); addTextNode( TQString::number( value ) ); endElement(); } void KoXmlWriter::addTextSpan( const TQString& text ) { TQMap tabCache; addTextSpan( text, tabCache ); } void KoXmlWriter::addTextSpan( const TQString& text, const TQMap& tabCache ) { uint len = text.length(); int nrSpaces = 0; // number of consecutive spaces bool leadingSpace = false; TQString str; str.reserve( len ); // Accumulate chars either in str or in nrSpaces (for spaces). // Flush str when writing a subelement (for spaces or for another reason) // Flush nrSpaces when encountering two or more consecutive spaces for ( uint i = 0; i < len ; ++i ) { TQChar ch = text[i]; if ( ch != ' ' ) { if ( nrSpaces > 0 ) { // For the first space we use ' '. // "it is good practice to use (text:s) for the second and all following SPACE // characters in a sequence." (per the ODF spec) // however, per the HTML spec, "authors should not rely on user agents to render // white space immediately after a start tag or immediately before an end tag" // (and both we and OO.o ignore leading spaces in or elements...) if (!leadingSpace) { str += ' '; --nrSpaces; } if ( nrSpaces > 0 ) { // there are more spaces if ( !str.isEmpty() ) addTextNode( str ); str = TQString(); startElement( "text:s" ); if ( nrSpaces > 1 ) // it's 1 by default addAttribute( "text:c", nrSpaces ); endElement(); } } nrSpaces = 0; leadingSpace = false; } switch ( ch.tqunicode() ) { case '\t': if ( !str.isEmpty() ) addTextNode( str ); str = TQString(); startElement( "text:tab" ); if ( tabCache.contains( i ) ) addAttribute( "text:tab-ref", tabCache[i] + 1 ); endElement(); break; case '\n': if ( !str.isEmpty() ) addTextNode( str ); str = TQString(); startElement( "text:line-break" ); endElement(); break; case ' ': if ( i == 0 ) leadingSpace = true; ++nrSpaces; break; default: str += text[i]; break; } } // either we still have text in str or we have spaces in nrSpaces if ( !str.isEmpty() ) { addTextNode( str ); } if ( nrSpaces > 0 ) { // there are more spaces startElement( "text:s" ); if ( nrSpaces > 1 ) // it's 1 by default addAttribute( "text:c", nrSpaces ); endElement(); } }