blob: 02d70cbd130cbf6387a03be33c54f6dc08e41c6e [file] [log] [blame]
/**************************************************************
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*************************************************************/
// MARKER(update_precomp.py): autogen include statement, do not remove
#include "precompiled_sc.hxx"
#include <algorithm>
#include <stdio.h>
#include "xestream.hxx"
#include "xlstyle.hxx"
#include "xestyle.hxx"
#include "xestring.hxx"
using namespace ::oox;
using ::rtl::OString;
using ::rtl::OUString;
// ============================================================================
namespace {
// compare vectors
/** Compares two vectors.
@return A negative value, if rLeft<rRight; or a positive value, if rLeft>rRight;
or 0, if rLeft==rRight. */
template< typename Type >
int lclCompareVectors( const ::std::vector< Type >& rLeft, const ::std::vector< Type >& rRight )
{
int nResult = 0;
// 1st: compare all elements of the vectors
typedef typename ::std::vector< Type >::const_iterator CIT;
CIT aEndL = rLeft.end(), aEndR = rRight.end();
for( CIT aItL = rLeft.begin(), aItR = rRight.begin(); !nResult && (aItL != aEndL) && (aItR != aEndR); ++aItL, ++aItR )
nResult = static_cast< int >( *aItL ) - static_cast< int >( *aItR );
// 2nd: no differences found so far -> compare the vector sizes. Shorter vector is less
if( !nResult )
nResult = static_cast< int >( rLeft.size() ) - static_cast< int >( rRight.size() );
return nResult;
}
// hashing helpers
/** Base class for value hashers.
@descr These function objects are used to hash any value to a sal_uInt32 value. */
template< typename Type >
struct XclHasher : public ::std::unary_function< Type, sal_uInt32 > {};
template< typename Type >
struct XclDirectHasher : public XclHasher< Type >
{
inline sal_uInt32 operator()( Type nVal ) const { return nVal; }
};
struct XclFormatRunHasher : public XclHasher< const XclFormatRun& >
{
inline sal_uInt32 operator()( const XclFormatRun& rRun ) const
{ return (rRun.mnChar << 8) ^ rRun.mnFontIdx; }
};
/** Calculates a hash value from a vector.
@descr Uses the passed hasher function object to calculate hash values from
all vector elements. */
template< typename Type, typename ValueHasher >
sal_uInt16 lclHashVector( const ::std::vector< Type >& rVec, const ValueHasher& rHasher )
{
sal_uInt32 nHash = rVec.size();
typedef typename ::std::vector< Type >::const_iterator CIT;
for( CIT aIt = rVec.begin(), aEnd = rVec.end(); aIt != aEnd; ++aIt )
(nHash *= 31) += rHasher( *aIt );
return static_cast< sal_uInt16 >( nHash ^ (nHash >> 16) );
}
/** Calculates a hash value from a vector. Uses XclDirectHasher to hash the vector elements. */
template< typename Type >
inline sal_uInt16 lclHashVector( const ::std::vector< Type >& rVec )
{
return lclHashVector( rVec, XclDirectHasher< Type >() );
}
} // namespace
// constructors ---------------------------------------------------------------
XclExpString::XclExpString( XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Init( 0, nFlags, nMaxLen, true );
}
XclExpString::XclExpString( const String& rString, XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Assign( rString, nFlags, nMaxLen );
}
XclExpString::XclExpString( const OUString& rString, XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Assign( rString, nFlags, nMaxLen );
}
//UNUSED2008-05 XclExpString::XclExpString(
//UNUSED2008-05 const String& rString, const XclFormatRunVec& rFormats,
//UNUSED2008-05 XclStrFlags nFlags, sal_uInt16 nMaxLen )
//UNUSED2008-05 {
//UNUSED2008-05 Assign( rString, rFormats, nFlags, nMaxLen );
//UNUSED2008-05 }
//UNUSED2008-05
//UNUSED2008-05 XclExpString::XclExpString(
//UNUSED2008-05 const OUString& rString, const XclFormatRunVec& rFormats,
//UNUSED2008-05 XclStrFlags nFlags, sal_uInt16 nMaxLen )
//UNUSED2008-05 {
//UNUSED2008-05 Assign( rString, rFormats, nFlags, nMaxLen );
//UNUSED2008-05 }
// assign ---------------------------------------------------------------------
void XclExpString::Assign( const String& rString, XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Build( rString.GetBuffer(), rString.Len(), nFlags, nMaxLen );
}
void XclExpString::Assign(
const String& rString, const XclFormatRunVec& rFormats,
XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Assign( rString, nFlags, nMaxLen );
SetFormats( rFormats );
}
void XclExpString::Assign( const OUString& rString, XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Build( rString.getStr(), rString.getLength(), nFlags, nMaxLen );
}
void XclExpString::Assign(
const OUString& rString, const XclFormatRunVec& rFormats,
XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Assign( rString, nFlags, nMaxLen );
SetFormats( rFormats );
}
void XclExpString::Assign( sal_Unicode cChar, XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Build( &cChar, 1, nFlags, nMaxLen );
}
void XclExpString::AssignByte(
const String& rString, rtl_TextEncoding eTextEnc, XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
ByteString aByteStr( rString, eTextEnc ); // length may differ from length of rString
Build( aByteStr.GetBuffer(), aByteStr.Len(), nFlags, nMaxLen );
}
//UNUSED2008-05 void XclExpString::AssignByte( sal_Unicode cChar, rtl_TextEncoding eTextEnc, XclStrFlags nFlags, sal_uInt16 nMaxLen )
//UNUSED2008-05 {
//UNUSED2008-05 if( !cChar )
//UNUSED2008-05 {
//UNUSED2008-05 sal_Char cByteChar = 0;
//UNUSED2008-05 Build( &cByteChar, 1, nFlags, nMaxLen );
//UNUSED2008-05 }
//UNUSED2008-05 else
//UNUSED2008-05 {
//UNUSED2008-05 ByteString aByteStr( &cChar, 1, eTextEnc ); // length may be >1
//UNUSED2008-05 Build( aByteStr.GetBuffer(), aByteStr.Len(), nFlags, nMaxLen );
//UNUSED2008-05 }
//UNUSED2008-05 }
// append ---------------------------------------------------------------------
void XclExpString::Append( const String& rString )
{
BuildAppend( rString.GetBuffer(), rString.Len() );
}
//UNUSED2008-05 void XclExpString::Append( const ::rtl::OUString& rString )
//UNUSED2008-05 {
//UNUSED2008-05 BuildAppend( rString.getStr(), rString.getLength() );
//UNUSED2008-05 }
//UNUSED2008-05
//UNUSED2008-05 void XclExpString::Append( sal_Unicode cChar )
//UNUSED2008-05 {
//UNUSED2008-05 BuildAppend( &cChar, 1 );
//UNUSED2008-05 }
void XclExpString::AppendByte( const String& rString, rtl_TextEncoding eTextEnc )
{
if( rString.Len() > 0 )
{
ByteString aByteStr( rString, eTextEnc ); // length may differ from length of rString
BuildAppend( aByteStr.GetBuffer(), aByteStr.Len() );
}
}
void XclExpString::AppendByte( sal_Unicode cChar, rtl_TextEncoding eTextEnc )
{
if( !cChar )
{
sal_Char cByteChar = 0;
BuildAppend( &cByteChar, 1 );
}
else
{
ByteString aByteStr( &cChar, 1, eTextEnc ); // length may be >1
BuildAppend( aByteStr.GetBuffer(), aByteStr.Len() );
}
}
// formatting runs ------------------------------------------------------------
void XclExpString::SetFormats( const XclFormatRunVec& rFormats )
{
maFormats = rFormats;
#ifdef DBG_UTIL
if( IsRich() )
{
XclFormatRunVec::const_iterator aCurr = maFormats.begin();
XclFormatRunVec::const_iterator aPrev = aCurr;
XclFormatRunVec::const_iterator aEnd = maFormats.end();
for( ++aCurr; aCurr != aEnd; ++aCurr, ++aPrev )
DBG_ASSERT( aPrev->mnChar < aCurr->mnChar, "XclExpString::SetFormats - invalid char order" );
DBG_ASSERT( aPrev->mnChar <= mnLen, "XclExpString::SetFormats - invalid char index" );
}
#endif
LimitFormatCount( mbIsBiff8 ? EXC_STR_MAXLEN : EXC_STR_MAXLEN_8BIT );
}
void XclExpString::AppendFormat( sal_uInt16 nChar, sal_uInt16 nFontIdx, bool bDropDuplicate )
{
DBG_ASSERT( maFormats.empty() || (maFormats.back().mnChar < nChar), "XclExpString::AppendFormat - invalid char index" );
size_t nMaxSize = static_cast< size_t >( mbIsBiff8 ? EXC_STR_MAXLEN : EXC_STR_MAXLEN_8BIT );
if( maFormats.empty() || ((maFormats.size() < nMaxSize) && (!bDropDuplicate || (maFormats.back().mnFontIdx != nFontIdx))) )
maFormats.push_back( XclFormatRun( nChar, nFontIdx ) );
}
void XclExpString::AppendTrailingFormat( sal_uInt16 nFontIdx )
{
AppendFormat( mnLen, nFontIdx, false );
}
void XclExpString::LimitFormatCount( sal_uInt16 nMaxCount )
{
if( maFormats.size() > nMaxCount )
maFormats.erase( maFormats.begin() + nMaxCount, maFormats.end() );
}
sal_uInt16 XclExpString::RemoveLeadingFont()
{
sal_uInt16 nFontIdx = EXC_FONT_NOTFOUND;
if( !maFormats.empty() && (maFormats.front().mnChar == 0) )
{
nFontIdx = maFormats.front().mnFontIdx;
maFormats.erase( maFormats.begin() );
}
return nFontIdx;
}
bool XclExpString::IsEqual( const XclExpString& rCmp ) const
{
return
(mnLen == rCmp.mnLen) &&
(mbIsBiff8 == rCmp.mbIsBiff8) &&
(mbIsUnicode == rCmp.mbIsUnicode) &&
(mbWrapped == rCmp.mbWrapped) &&
(
( mbIsBiff8 && (maUniBuffer == rCmp.maUniBuffer)) ||
(!mbIsBiff8 && (maCharBuffer == rCmp.maCharBuffer))
) &&
(maFormats == rCmp.maFormats);
}
bool XclExpString::IsLessThan( const XclExpString& rCmp ) const
{
int nResult = mbIsBiff8 ?
lclCompareVectors( maUniBuffer, rCmp.maUniBuffer ) :
lclCompareVectors( maCharBuffer, rCmp.maCharBuffer );
return (nResult != 0) ? (nResult < 0) : (maFormats < rCmp.maFormats);
}
// get data -------------------------------------------------------------------
sal_uInt16 XclExpString::GetFormatsCount() const
{
return static_cast< sal_uInt16 >( maFormats.size() );
}
sal_uInt8 XclExpString::GetFlagField() const
{
return (mbIsUnicode ? EXC_STRF_16BIT : 0) | (IsWriteFormats() ? EXC_STRF_RICH : 0);
}
sal_uInt16 XclExpString::GetHeaderSize() const
{
return
(mb8BitLen ? 1 : 2) + // length field
(IsWriteFlags() ? 1 : 0) + // flag field
(IsWriteFormats() ? 2 : 0); // richtext formattting count
}
sal_Size XclExpString::GetBufferSize() const
{
return mnLen * (mbIsUnicode ? 2 : 1);
}
sal_Size XclExpString::GetSize() const
{
return
GetHeaderSize() + // header
GetBufferSize() + // character buffer
(IsWriteFormats() ? (4 * GetFormatsCount()) : 0); // richtext formattting
}
sal_uInt16 XclExpString::GetChar( sal_uInt16 nCharIdx ) const
{
DBG_ASSERT( nCharIdx < Len(), "XclExpString::GetChar - invalid character index" );
return static_cast< sal_uInt16 >( mbIsBiff8 ? maUniBuffer[ nCharIdx ] : maCharBuffer[ nCharIdx ] );
}
sal_uInt16 XclExpString::GetHash() const
{
return
(mbIsBiff8 ? lclHashVector( maUniBuffer ) : lclHashVector( maCharBuffer )) ^
lclHashVector( maFormats, XclFormatRunHasher() );
}
// streaming ------------------------------------------------------------------
void XclExpString::WriteLenField( XclExpStream& rStrm ) const
{
if( mb8BitLen )
rStrm << static_cast< sal_uInt8 >( mnLen );
else
rStrm << mnLen;
}
void XclExpString::WriteFlagField( XclExpStream& rStrm ) const
{
if( mbIsBiff8 )
{
PrepareWrite( rStrm, 1 );
rStrm << GetFlagField();
rStrm.SetSliceSize( 0 );
}
}
void XclExpString::WriteHeader( XclExpStream& rStrm ) const
{
DBG_ASSERT( !mb8BitLen || (mnLen < 256), "XclExpString::WriteHeader - string too long" );
PrepareWrite( rStrm, GetHeaderSize() );
// length
WriteLenField( rStrm );
// flag field
if( IsWriteFlags() )
rStrm << GetFlagField();
// format run count
if( IsWriteFormats() )
rStrm << GetFormatsCount();
rStrm.SetSliceSize( 0 );
}
void XclExpString::WriteBuffer( XclExpStream& rStrm ) const
{
if( mbIsBiff8 )
rStrm.WriteUnicodeBuffer( maUniBuffer, GetFlagField() );
else
rStrm.WriteCharBuffer( maCharBuffer );
}
void XclExpString::WriteFormats( XclExpStream& rStrm, bool bWriteSize ) const
{
if( IsRich() )
{
XclFormatRunVec::const_iterator aIt = maFormats.begin(), aEnd = maFormats.end();
if( mbIsBiff8 )
{
if( bWriteSize )
rStrm << GetFormatsCount();
rStrm.SetSliceSize( 4 );
for( ; aIt != aEnd; ++aIt )
rStrm << aIt->mnChar << aIt->mnFontIdx;
}
else
{
if( bWriteSize )
rStrm << static_cast< sal_uInt8 >( GetFormatsCount() );
rStrm.SetSliceSize( 2 );
for( ; aIt != aEnd; ++aIt )
rStrm << static_cast< sal_uInt8 >( aIt->mnChar ) << static_cast< sal_uInt8 >( aIt->mnFontIdx );
}
rStrm.SetSliceSize( 0 );
}
}
void XclExpString::Write( XclExpStream& rStrm ) const
{
if (!mbSkipHeader)
WriteHeader( rStrm );
WriteBuffer( rStrm );
if( IsWriteFormats() ) // only in BIFF8 included in string
WriteFormats( rStrm );
}
void XclExpString::WriteHeaderToMem( sal_uInt8* pnMem ) const
{
DBG_ASSERT( pnMem, "XclExpString::WriteHeaderToMem - no memory to write to" );
DBG_ASSERT( !mb8BitLen || (mnLen < 256), "XclExpString::WriteHeaderToMem - string too long" );
DBG_ASSERT( !IsWriteFormats(), "XclExpString::WriteHeaderToMem - formatted strings not supported" );
// length
if( mb8BitLen )
{
*pnMem = static_cast< sal_uInt8 >( mnLen );
++pnMem;
}
else
{
ShortToSVBT16( mnLen, pnMem );
pnMem += 2;
}
// flag field
if( IsWriteFlags() )
*pnMem = GetFlagField();
}
void XclExpString::WriteBufferToMem( sal_uInt8* pnMem ) const
{
DBG_ASSERT( pnMem, "XclExpString::WriteBufferToMem - no memory to write to" );
if( !IsEmpty() )
{
if( mbIsBiff8 )
{
for( ScfUInt16Vec::const_iterator aIt = maUniBuffer.begin(), aEnd = maUniBuffer.end(); aIt != aEnd; ++aIt )
{
sal_uInt16 nChar = *aIt;
*pnMem = static_cast< sal_uInt8 >( nChar );
++pnMem;
if( mbIsUnicode )
{
*pnMem = static_cast< sal_uInt8 >( nChar >> 8 );
++pnMem;
}
}
}
else
memcpy( pnMem, &maCharBuffer[ 0 ], mnLen );
}
}
void XclExpString::WriteToMem( sal_uInt8* pnMem ) const
{
WriteHeaderToMem( pnMem );
WriteBufferToMem( pnMem + GetHeaderSize() );
}
static sal_uInt16 lcl_WriteRun( XclExpXmlStream& rStrm, const ScfUInt16Vec& rBuffer, sal_uInt16 nStart, sal_Int32 nLength, const XclExpFont* pFont )
{
if( nLength == 0 )
return nStart;
sax_fastparser::FSHelperPtr& rWorksheet = rStrm.GetCurrentStream();
rWorksheet->startElement( XML_r, FSEND );
if( pFont )
{
const XclFontData& rFontData = pFont->GetFontData();
rWorksheet->startElement( XML_rPr, FSEND );
rStrm.WriteFontData( rFontData, XML_rFont );
rWorksheet->endElement( XML_rPr );
}
rWorksheet->startElement( XML_t,
FSNS( XML_xml, XML_space ), "preserve",
FSEND );
rWorksheet->writeEscaped( XclXmlUtils::ToOUString( rBuffer, nStart, nLength ) );
rWorksheet->endElement( XML_t );
rWorksheet->endElement( XML_r );
return static_cast<sal_uInt16>(nStart + nLength);
}
void XclExpString::WriteXml( XclExpXmlStream& rStrm ) const
{
sax_fastparser::FSHelperPtr rWorksheet = rStrm.GetCurrentStream();
if( !IsWriteFormats() )
{
rWorksheet->startElement( XML_t, FSEND );
rWorksheet->writeEscaped( XclXmlUtils::ToOUString( *this ) );
rWorksheet->endElement( XML_t );
}
else
{
XclExpFontBuffer& rFonts = rStrm.GetRoot().GetFontBuffer();
XclFormatRunVec::const_iterator aIt = maFormats.begin(), aEnd = maFormats.end();
sal_uInt16 nStart = 0;
const XclExpFont* pFont = NULL;
for ( ; aIt != aEnd; ++aIt )
{
nStart = lcl_WriteRun( rStrm, GetUnicodeBuffer(),
nStart, aIt->mnChar-nStart, pFont );
pFont = rFonts.GetFont( aIt->mnFontIdx );
}
lcl_WriteRun( rStrm, GetUnicodeBuffer(),
nStart, GetUnicodeBuffer().size() - nStart, pFont );
}
}
// ----------------------------------------------------------------------------
bool XclExpString::IsWriteFlags() const
{
return mbIsBiff8 && (!IsEmpty() || !mbSmartFlags);
}
bool XclExpString::IsWriteFormats() const
{
return mbIsBiff8 && !mbSkipFormats && IsRich();
}
void XclExpString::SetStrLen( sal_Int32 nNewLen )
{
sal_uInt16 nAllowedLen = (mb8BitLen && (mnMaxLen > 255)) ? 255 : mnMaxLen;
mnLen = limit_cast< sal_uInt16 >( nNewLen, 0, nAllowedLen );
}
void XclExpString::CharsToBuffer( const sal_Unicode* pcSource, sal_Int32 nBegin, sal_Int32 nLen )
{
DBG_ASSERT( maUniBuffer.size() >= static_cast< size_t >( nBegin + nLen ),
"XclExpString::CharsToBuffer - char buffer invalid" );
ScfUInt16Vec::iterator aBeg = maUniBuffer.begin() + nBegin;
ScfUInt16Vec::iterator aEnd = aBeg + nLen;
const sal_Unicode* pcSrcChar = pcSource;
for( ScfUInt16Vec::iterator aIt = aBeg; aIt != aEnd; ++aIt, ++pcSrcChar )
{
*aIt = static_cast< sal_uInt16 >( *pcSrcChar );
if( *aIt & 0xFF00 )
mbIsUnicode = true;
}
if( !mbWrapped )
mbWrapped = ::std::find( aBeg, aEnd, EXC_LF ) != aEnd;
}
void XclExpString::CharsToBuffer( const sal_Char* pcSource, sal_Int32 nBegin, sal_Int32 nLen )
{
DBG_ASSERT( maCharBuffer.size() >= static_cast< size_t >( nBegin + nLen ),
"XclExpString::CharsToBuffer - char buffer invalid" );
ScfUInt8Vec::iterator aBeg = maCharBuffer.begin() + nBegin;
ScfUInt8Vec::iterator aEnd = aBeg + nLen;
const sal_Char* pcSrcChar = pcSource;
for( ScfUInt8Vec::iterator aIt = aBeg; aIt != aEnd; ++aIt, ++pcSrcChar )
*aIt = static_cast< sal_uInt8 >( *pcSrcChar );
mbIsUnicode = false;
if( !mbWrapped )
mbWrapped = ::std::find( aBeg, aEnd, EXC_LF_C ) != aEnd;
}
void XclExpString::Init( sal_Int32 nCurrLen, XclStrFlags nFlags, sal_uInt16 nMaxLen, bool bBiff8 )
{
mbIsBiff8 = bBiff8;
mbIsUnicode = bBiff8 && ::get_flag( nFlags, EXC_STR_FORCEUNICODE );
mb8BitLen = ::get_flag( nFlags, EXC_STR_8BITLENGTH );
mbSmartFlags = bBiff8 && ::get_flag( nFlags, EXC_STR_SMARTFLAGS );
mbSkipFormats = ::get_flag( nFlags, EXC_STR_SEPARATEFORMATS );
mbWrapped = false;
mbSkipHeader = ::get_flag( nFlags, EXC_STR_NOHEADER );
mnMaxLen = nMaxLen;
SetStrLen( nCurrLen );
maFormats.clear();
if( mbIsBiff8 )
{
maCharBuffer.clear();
maUniBuffer.resize( mnLen );
}
else
{
maUniBuffer.clear();
maCharBuffer.resize( mnLen );
}
}
void XclExpString::Build( const sal_Unicode* pcSource, sal_Int32 nCurrLen, XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Init( nCurrLen, nFlags, nMaxLen, true );
CharsToBuffer( pcSource, 0, mnLen );
}
void XclExpString::Build( const sal_Char* pcSource, sal_Int32 nCurrLen, XclStrFlags nFlags, sal_uInt16 nMaxLen )
{
Init( nCurrLen, nFlags, nMaxLen, false );
CharsToBuffer( pcSource, 0, mnLen );
}
void XclExpString::InitAppend( sal_Int32 nAddLen )
{
SetStrLen( static_cast< sal_Int32 >( mnLen ) + nAddLen );
if( mbIsBiff8 )
maUniBuffer.resize( mnLen );
else
maCharBuffer.resize( mnLen );
}
void XclExpString::BuildAppend( const sal_Unicode* pcSource, sal_Int32 nAddLen )
{
DBG_ASSERT( mbIsBiff8, "XclExpString::BuildAppend - must not be called at byte strings" );
if( mbIsBiff8 )
{
sal_uInt16 nOldLen = mnLen;
InitAppend( nAddLen );
CharsToBuffer( pcSource, nOldLen, mnLen - nOldLen );
}
}
void XclExpString::BuildAppend( const sal_Char* pcSource, sal_Int32 nAddLen )
{
DBG_ASSERT( !mbIsBiff8, "XclExpString::BuildAppend - must not be called at unicode strings" );
if( !mbIsBiff8 )
{
sal_uInt16 nOldLen = mnLen;
InitAppend( nAddLen );
CharsToBuffer( pcSource, nOldLen, mnLen - nOldLen );
}
}
void XclExpString::PrepareWrite( XclExpStream& rStrm, sal_uInt16 nBytes ) const
{
rStrm.SetSliceSize( nBytes + (mbIsUnicode ? 2 : 1) );
}
// ============================================================================