2005-10-11 09:37:18 +00:00
|
|
|
// Adapted from http://www.codeproject.com/cpp/xmlite.asp.
|
|
|
|
|
// On 2004-02-09 Cho, Kyung-Min gave us permission to use and modify this
|
|
|
|
|
// library.
|
|
|
|
|
//
|
|
|
|
|
// XmlFile : XML Lite Parser Library
|
|
|
|
|
// by Cho, Kyung Min: bro@shinbiro.com 2002-10-30
|
|
|
|
|
|
2004-02-09 08:10:01 +00:00
|
|
|
#include "global.h"
|
|
|
|
|
#include "XmlFile.h"
|
2004-02-10 09:42:01 +00:00
|
|
|
#include "RageFile.h"
|
2004-04-02 05:06:32 +00:00
|
|
|
#include "RageLog.h"
|
2004-07-18 21:55:49 +00:00
|
|
|
#include "RageUtil.h"
|
|
|
|
|
#include "DateTime.h"
|
2005-01-07 09:09:23 +00:00
|
|
|
#include "Foreach.h"
|
2005-06-16 03:08:45 +00:00
|
|
|
#include "RageFileDriverMemory.h"
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2005-09-03 08:28:43 +00:00
|
|
|
static inline long XStr2Int( const char* str, long default_value = 0 )
|
|
|
|
|
{
|
|
|
|
|
return str ? atol(str) : default_value;
|
|
|
|
|
}
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2004-08-18 22:09:23 +00:00
|
|
|
static const char chXMLTagOpen = '<';
|
2005-02-18 13:05:17 +00:00
|
|
|
static const char chXMLTagClose = '>';
|
2005-02-18 14:03:50 +00:00
|
|
|
static const char chXMLQuestion = '?'; // used in checking for meta tags: "<?TAG ... ?/>"
|
2005-02-18 13:05:17 +00:00
|
|
|
static const char chXMLTagPre = '/';
|
2005-02-18 14:03:50 +00:00
|
|
|
static const char chXMLExclamation = '!';
|
|
|
|
|
static const char chXMLDash = '-';
|
2004-02-09 08:10:01 +00:00
|
|
|
|
|
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
static map<RString,RString> g_mapEntitiesToChars;
|
|
|
|
|
static map<char,RString> g_mapCharsToEntities;
|
2005-09-04 23:05:58 +00:00
|
|
|
|
|
|
|
|
static void InitEntities()
|
|
|
|
|
{
|
|
|
|
|
if( !g_mapEntitiesToChars.empty() )
|
|
|
|
|
return;
|
|
|
|
|
|
2005-10-27 16:57:18 +00:00
|
|
|
static struct Entity
|
2005-09-04 23:05:58 +00:00
|
|
|
{
|
|
|
|
|
char c;
|
|
|
|
|
const char *pEntity;
|
|
|
|
|
}
|
2005-10-27 16:57:18 +00:00
|
|
|
const EntityTable[] =
|
2005-09-04 23:05:58 +00:00
|
|
|
{
|
|
|
|
|
{ '&', "amp", },
|
|
|
|
|
{ '\"', "quot", },
|
|
|
|
|
{ '\'', "apos", },
|
|
|
|
|
{ '<', "lt", },
|
|
|
|
|
{ '>', "gt", }
|
2004-02-09 08:10:01 +00:00
|
|
|
};
|
|
|
|
|
|
2006-09-13 03:11:38 +00:00
|
|
|
for( unsigned i = 0; i < ARRAYLEN(EntityTable); ++i )
|
2005-09-04 23:05:58 +00:00
|
|
|
{
|
|
|
|
|
const Entity &ent = EntityTable[i];
|
2005-12-27 23:43:49 +00:00
|
|
|
g_mapEntitiesToChars[ent.pEntity] = RString(1, ent.c);
|
2005-09-04 23:05:58 +00:00
|
|
|
g_mapCharsToEntities[ent.c] = ent.pEntity;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2005-10-27 16:57:18 +00:00
|
|
|
static struct RunInitEntities
|
2005-09-04 23:05:58 +00:00
|
|
|
{
|
|
|
|
|
RunInitEntities() { InitEntities(); }
|
2005-10-27 16:57:18 +00:00
|
|
|
} g_RunInitEntities;
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2004-05-23 02:28:36 +00:00
|
|
|
// skip spaces
|
2005-12-27 23:43:49 +00:00
|
|
|
static void tcsskip( const RString &s, unsigned &i )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-09-06 18:25:16 +00:00
|
|
|
i = s.find_first_not_of( " \t\r\n", i );
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
static bool XIsEmptyString( const RString &s )
|
2005-08-26 19:13:08 +00:00
|
|
|
{
|
|
|
|
|
return s.find_first_not_of( "\r\n\t " ) == s.npos;
|
|
|
|
|
}
|
|
|
|
|
|
2005-09-03 08:28:43 +00:00
|
|
|
// put string of (psz~end) on ps string
|
2005-12-27 23:43:49 +00:00
|
|
|
static void SetString( const RString &s, int iStart, int iEnd, RString* ps, bool trim = false )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
|
|
|
|
if( trim )
|
|
|
|
|
{
|
2006-06-13 04:36:56 +00:00
|
|
|
while( iStart < iEnd && s[iStart] > 0 && isspace(s[iStart]) )
|
2005-09-06 18:25:16 +00:00
|
|
|
iStart++;
|
2006-06-13 04:36:56 +00:00
|
|
|
while( iEnd-1 >= iStart && s[iEnd-1] > 0 && isspace(s[iEnd-1]) )
|
2005-09-06 18:25:16 +00:00
|
|
|
iEnd--;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
2005-09-03 08:28:43 +00:00
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
int len = iEnd - iStart;
|
2005-09-03 08:28:43 +00:00
|
|
|
if( len <= 0 )
|
|
|
|
|
return;
|
|
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
ps->assign( s, iStart, len );
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
2005-10-11 09:35:53 +00:00
|
|
|
XNode::XNode( const XNode &cpy ):
|
|
|
|
|
m_sName( cpy.m_sName ),
|
2005-10-11 10:34:27 +00:00
|
|
|
m_sValue( cpy.m_sValue ),
|
|
|
|
|
m_attrs( cpy.m_attrs )
|
2005-10-11 09:35:53 +00:00
|
|
|
{
|
|
|
|
|
FOREACH_CONST_Child( &cpy, c )
|
|
|
|
|
this->AppendChild( new XNode(*c) );
|
|
|
|
|
}
|
|
|
|
|
|
2004-04-18 10:41:54 +00:00
|
|
|
XNode::~XNode()
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-01-07 14:28:00 +00:00
|
|
|
Clear();
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
2005-01-07 14:28:00 +00:00
|
|
|
void XNode::Clear()
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-01-07 09:09:23 +00:00
|
|
|
FOREACH_Child( this, p )
|
|
|
|
|
SAFE_DELETE( p );
|
2005-01-07 14:28:00 +00:00
|
|
|
m_childs.clear();
|
|
|
|
|
m_attrs.clear();
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// attr1="value1" attr2='value2' attr3=value3 />
|
|
|
|
|
// ^- return pointer
|
|
|
|
|
// Desc : loading attribute plain xml text
|
|
|
|
|
// Param : pszAttrs - xml of attributes
|
|
|
|
|
// pi = parser information
|
2005-09-06 18:25:16 +00:00
|
|
|
// Return : advanced string pointer. (error return npos)
|
2005-12-27 23:43:49 +00:00
|
|
|
unsigned XNode::LoadAttributes( const RString &xml, PARSEINFO *pi, unsigned iOffset )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-09-06 18:25:16 +00:00
|
|
|
while( iOffset < xml.size() )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-09-06 18:25:16 +00:00
|
|
|
tcsskip( xml, iOffset );
|
|
|
|
|
if( iOffset >= xml.size() )
|
2004-02-09 19:33:56 +00:00
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
// close tag
|
2005-09-06 18:25:16 +00:00
|
|
|
if( iOffset < xml.size() &&
|
|
|
|
|
(xml[iOffset] == chXMLTagClose || xml[iOffset] == chXMLTagPre || xml[iOffset] == chXMLQuestion || xml[iOffset] == chXMLDash) )
|
|
|
|
|
return iOffset; // well-formed tag
|
2004-02-09 19:33:56 +00:00
|
|
|
|
|
|
|
|
// XML Attr Name
|
2005-09-06 18:25:16 +00:00
|
|
|
unsigned iEnd = xml.find_first_of( " =", iOffset );
|
|
|
|
|
if( iEnd == xml.npos )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2004-02-09 19:33:56 +00:00
|
|
|
// error
|
2006-10-01 12:39:50 +00:00
|
|
|
if( pi->error_string.empty() )
|
2005-09-06 00:58:17 +00:00
|
|
|
pi->error_string = ssprintf( "<%s> attribute has error ", m_sName.c_str() );
|
2005-09-07 02:05:19 +00:00
|
|
|
return string::npos;
|
2004-02-09 19:33:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// XML Attr Name
|
2005-12-27 23:43:49 +00:00
|
|
|
RString sName;
|
2005-10-11 10:24:07 +00:00
|
|
|
SetString( xml, iOffset, iEnd, &sName );
|
2004-02-09 19:33:56 +00:00
|
|
|
|
|
|
|
|
// add new attribute
|
2005-10-11 10:24:07 +00:00
|
|
|
DEBUG_ASSERT( sName.size() );
|
2005-12-27 23:43:49 +00:00
|
|
|
pair<XAttrs::iterator,bool> it = m_attrs.insert( make_pair(sName, RString()) );
|
|
|
|
|
RString &sValue = it.first->second;
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset = iEnd;
|
2004-02-09 19:33:56 +00:00
|
|
|
|
|
|
|
|
// XML Attr Value
|
2005-09-06 18:25:16 +00:00
|
|
|
tcsskip( xml, iOffset );
|
2004-02-09 19:33:56 +00:00
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
if( iOffset < xml.size() && xml[iOffset] == '=' )
|
2004-02-09 19:33:56 +00:00
|
|
|
{
|
2005-09-06 18:25:16 +00:00
|
|
|
++iOffset;
|
|
|
|
|
|
|
|
|
|
tcsskip( xml, iOffset );
|
|
|
|
|
if( iOffset >= xml.size() )
|
2004-02-09 19:33:56 +00:00
|
|
|
continue;
|
2005-09-06 18:25:16 +00:00
|
|
|
|
2004-02-09 19:33:56 +00:00
|
|
|
// if " or '
|
|
|
|
|
// or none quote
|
2005-09-06 18:25:16 +00:00
|
|
|
char quote = xml[iOffset];
|
2004-02-09 19:33:56 +00:00
|
|
|
if( quote == '"' || quote == '\'' )
|
2005-01-07 14:28:00 +00:00
|
|
|
{
|
2005-09-06 18:25:16 +00:00
|
|
|
++iOffset;
|
|
|
|
|
iEnd = xml.find( quote, iOffset );
|
2005-01-07 14:28:00 +00:00
|
|
|
}
|
2004-02-09 19:33:56 +00:00
|
|
|
else
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2004-02-09 19:33:56 +00:00
|
|
|
//attr= value>
|
|
|
|
|
// none quote mode
|
2005-09-06 18:25:16 +00:00
|
|
|
iEnd = xml.find_first_of( " >", iOffset );
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
2005-10-15 06:12:58 +00:00
|
|
|
if( iEnd == xml.npos )
|
|
|
|
|
{
|
|
|
|
|
// error
|
2006-10-01 12:39:50 +00:00
|
|
|
if( pi->error_string.empty() )
|
2005-10-15 06:12:58 +00:00
|
|
|
pi->error_string = ssprintf( "<%s> attribute text: couldn't find matching quote", sName.c_str() );
|
|
|
|
|
return string::npos;
|
|
|
|
|
}
|
|
|
|
|
|
2006-10-01 12:29:14 +00:00
|
|
|
SetString( xml, iOffset, iEnd, &sValue, true );
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset = iEnd;
|
2004-02-09 19:33:56 +00:00
|
|
|
// ATTRVALUE
|
2006-10-01 12:29:14 +00:00
|
|
|
ReplaceEntityText( sValue, g_mapEntitiesToChars );
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2004-02-09 19:33:56 +00:00
|
|
|
if( quote == '"' || quote == '\'' )
|
2005-09-06 18:25:16 +00:00
|
|
|
++iOffset;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2005-02-18 14:03:50 +00:00
|
|
|
// not well-formed tag
|
2005-09-06 18:25:16 +00:00
|
|
|
return string::npos;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// <TAG attr1="value1" attr2='value2' attr3=value3 >
|
|
|
|
|
// </TAG>
|
|
|
|
|
// or
|
|
|
|
|
// <TAG />
|
|
|
|
|
// ^- return pointer
|
|
|
|
|
// Desc : load xml plain text
|
|
|
|
|
// Param : pszXml - plain xml text
|
|
|
|
|
// pi = parser information
|
2005-09-06 18:25:16 +00:00
|
|
|
// Return : advanced string pointer (error return npos)
|
2005-12-27 23:43:49 +00:00
|
|
|
unsigned XNode::Load( const RString &xml, PARSEINFO *pi, unsigned iOffset )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-01-07 14:28:00 +00:00
|
|
|
Clear();
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2005-02-25 06:13:21 +00:00
|
|
|
// <
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset = xml.find( chXMLTagOpen, iOffset );
|
2005-09-07 02:05:19 +00:00
|
|
|
if( iOffset == string::npos )
|
|
|
|
|
return string::npos;
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2005-02-25 06:13:21 +00:00
|
|
|
// </
|
2005-09-06 18:25:16 +00:00
|
|
|
if( xml[iOffset+1] == chXMLTagPre )
|
|
|
|
|
return iOffset;
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2005-02-25 06:13:21 +00:00
|
|
|
/* <!-- */
|
2005-09-06 18:25:16 +00:00
|
|
|
if( !xml.compare(iOffset+1, 3, "!--") )
|
2005-02-25 06:13:21 +00:00
|
|
|
{
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset += 4;
|
2005-02-25 06:13:21 +00:00
|
|
|
|
|
|
|
|
/* Find the close tag. */
|
2005-09-06 23:39:45 +00:00
|
|
|
unsigned iEnd = xml.find( "-->", iOffset );
|
2005-09-07 02:05:19 +00:00
|
|
|
if( iEnd == string::npos )
|
2005-02-25 06:13:21 +00:00
|
|
|
{
|
2006-10-01 12:39:50 +00:00
|
|
|
if( pi->error_string.empty() )
|
2005-02-25 06:13:21 +00:00
|
|
|
pi->error_string = "Unterminated comment";
|
|
|
|
|
|
2005-09-07 02:05:19 +00:00
|
|
|
return string::npos;
|
2005-02-25 06:13:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip -->.
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset = iEnd + 3;
|
2005-02-25 06:13:21 +00:00
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
return Load( xml, pi, iOffset );
|
2005-02-25 06:13:21 +00:00
|
|
|
}
|
|
|
|
|
|
2004-02-09 08:10:01 +00:00
|
|
|
// XML Node Tag Name Open
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset++;
|
|
|
|
|
unsigned iTagEnd = xml.find_first_of( " \t\r\n/>", iOffset );
|
|
|
|
|
SetString( xml, iOffset, iTagEnd, &m_sName );
|
|
|
|
|
iOffset = iTagEnd;
|
2005-02-18 14:03:50 +00:00
|
|
|
|
2004-02-09 08:10:01 +00:00
|
|
|
// Generate XML Attributte List
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset = LoadAttributes( xml, pi, iOffset );
|
2005-09-07 02:05:19 +00:00
|
|
|
if( iOffset == string::npos )
|
|
|
|
|
return string::npos;
|
2004-02-09 19:33:56 +00:00
|
|
|
|
2005-02-18 14:03:50 +00:00
|
|
|
// alone tag <TAG ... /> or <?TAG ... ?> or <!-- ... -->
|
|
|
|
|
// current pointer: ^ ^ ^
|
|
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
if( iOffset < xml.size() && (xml[iOffset] == chXMLTagPre || xml[iOffset] == chXMLQuestion || xml[iOffset] == chXMLDash) )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset++;
|
2005-02-18 14:03:50 +00:00
|
|
|
|
|
|
|
|
// skip over 2nd dash
|
2005-09-06 18:25:16 +00:00
|
|
|
if( iOffset < xml.size() && xml[iOffset] == chXMLDash )
|
|
|
|
|
iOffset++;
|
2005-02-18 14:03:50 +00:00
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
if( iOffset == xml.size() || xml[iOffset] != chXMLTagClose )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2004-02-09 19:33:56 +00:00
|
|
|
// error: <TAG ... / >
|
2006-10-01 12:39:50 +00:00
|
|
|
if( pi->error_string.empty() )
|
2005-09-03 08:28:43 +00:00
|
|
|
pi->error_string = "Element must be closed.";
|
|
|
|
|
|
|
|
|
|
// ill-formed tag
|
2005-09-07 02:05:19 +00:00
|
|
|
return string::npos;
|
2004-02-09 19:33:56 +00:00
|
|
|
}
|
2005-09-03 08:28:43 +00:00
|
|
|
|
|
|
|
|
// well-formed tag
|
2005-09-06 18:25:16 +00:00
|
|
|
++iOffset;
|
2005-09-03 08:28:43 +00:00
|
|
|
|
|
|
|
|
// UGLY: We want to ignore all XML meta tags. So, since the Node we
|
|
|
|
|
// just loaded is a meta tag, then Load ourself again using the rest
|
|
|
|
|
// of the file until we reach a non-meta tag.
|
|
|
|
|
if( !m_sName.empty() && (m_sName[0] == chXMLQuestion || m_sName[0] == chXMLExclamation) )
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset = Load( xml, pi, iOffset );
|
2005-09-03 08:28:43 +00:00
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
return iOffset;
|
2004-02-09 19:33:56 +00:00
|
|
|
}
|
2005-09-03 08:28:43 +00:00
|
|
|
|
2004-02-09 19:33:56 +00:00
|
|
|
// open/close tag <TAG ..> ... </TAG>
|
|
|
|
|
// ^- current pointer
|
2005-09-03 08:28:43 +00:00
|
|
|
if( XIsEmptyString( m_sValue ) )
|
|
|
|
|
{
|
|
|
|
|
// Text Value
|
2005-09-06 18:25:16 +00:00
|
|
|
++iOffset;
|
|
|
|
|
unsigned iEnd = xml.find( chXMLTagOpen, iOffset );
|
2005-09-07 02:05:19 +00:00
|
|
|
if( iEnd == string::npos )
|
2005-09-03 08:28:43 +00:00
|
|
|
{
|
2006-10-01 12:39:50 +00:00
|
|
|
if( pi->error_string.empty() )
|
2005-09-03 08:28:43 +00:00
|
|
|
pi->error_string = ssprintf( "%s must be closed with </%s>", m_sName.c_str(), m_sName.c_str() );
|
|
|
|
|
// error cos not exist CloseTag </TAG>
|
2005-09-07 02:05:19 +00:00
|
|
|
return string::npos;
|
2005-09-03 08:28:43 +00:00
|
|
|
}
|
|
|
|
|
|
2006-10-01 12:29:14 +00:00
|
|
|
SetString( xml, iOffset, iEnd, &m_sValue, true );
|
2005-09-03 08:28:43 +00:00
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset = iEnd;
|
2005-09-03 08:28:43 +00:00
|
|
|
// TEXTVALUE reference
|
2006-10-01 12:29:14 +00:00
|
|
|
ReplaceEntityText( m_sValue, g_mapEntitiesToChars );
|
2005-09-03 08:28:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// generate child nodes
|
2005-09-06 18:25:16 +00:00
|
|
|
while( iOffset < xml.size() )
|
2004-02-09 19:33:56 +00:00
|
|
|
{
|
2005-09-03 08:28:43 +00:00
|
|
|
XNode *node = new XNode;
|
|
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset = node->Load( xml, pi, iOffset );
|
2005-09-03 08:28:43 +00:00
|
|
|
if( !node->m_sName.empty() )
|
|
|
|
|
{
|
|
|
|
|
DEBUG_ASSERT( node->m_sName.size() );
|
|
|
|
|
m_childs.insert( make_pair(node->m_sName, node) );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
delete node;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// open/close tag <TAG ..> ... </TAG>
|
|
|
|
|
// ^- current pointer
|
|
|
|
|
// CloseTag case
|
2005-09-06 18:25:16 +00:00
|
|
|
if( iOffset+1 < xml.size() && xml[iOffset] == chXMLTagOpen && xml[iOffset+1] == chXMLTagPre )
|
2004-02-09 19:33:56 +00:00
|
|
|
{
|
2005-09-03 08:28:43 +00:00
|
|
|
// </Close>
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset += 2; // C
|
2005-09-03 08:28:43 +00:00
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
tcsskip( xml, iOffset );
|
|
|
|
|
if( iOffset >= xml.size() )
|
|
|
|
|
continue;
|
2005-09-03 08:28:43 +00:00
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
unsigned iEnd = xml.find_first_of( " >", iOffset );
|
2005-09-07 02:05:19 +00:00
|
|
|
if( iEnd == string::npos )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2006-10-01 12:39:50 +00:00
|
|
|
if( pi->error_string.empty() )
|
2005-09-03 08:28:43 +00:00
|
|
|
pi->error_string = ssprintf( "it must be closed with </%s>", m_sName.c_str() );
|
|
|
|
|
// error
|
2005-09-07 02:05:19 +00:00
|
|
|
return string::npos;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
2005-09-06 00:58:17 +00:00
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
RString closename;
|
2005-09-06 18:25:16 +00:00
|
|
|
SetString( xml, iOffset, iEnd, &closename );
|
|
|
|
|
iOffset = iEnd+1;
|
2005-09-03 08:28:43 +00:00
|
|
|
if( closename == this->m_sName )
|
2004-02-09 19:33:56 +00:00
|
|
|
{
|
2005-09-03 08:28:43 +00:00
|
|
|
// wel-formed open/close
|
|
|
|
|
// return '>' or ' ' after pointer
|
2005-09-06 18:25:16 +00:00
|
|
|
return iOffset;
|
2004-02-09 19:33:56 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2005-09-03 08:28:43 +00:00
|
|
|
// not welformed open/close
|
2006-10-01 12:39:50 +00:00
|
|
|
if( pi->error_string.empty() )
|
2005-09-03 08:28:43 +00:00
|
|
|
pi->error_string = ssprintf( "'<%s> ... </%s>' is not well-formed.", m_sName.c_str(), closename.c_str() );
|
2005-09-07 02:05:19 +00:00
|
|
|
return string::npos;
|
2005-09-03 08:28:43 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else // Alone child Tag Loaded
|
|
|
|
|
{
|
2005-09-06 18:25:16 +00:00
|
|
|
if( XIsEmptyString( m_sValue ) && iOffset < xml.size() && xml[iOffset] != chXMLTagOpen )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-09-03 08:28:43 +00:00
|
|
|
// Text Value
|
2005-09-06 18:25:16 +00:00
|
|
|
unsigned iEnd = xml.find( chXMLTagOpen, iOffset );
|
2005-09-07 02:05:19 +00:00
|
|
|
if( iEnd == string::npos )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-09-03 08:28:43 +00:00
|
|
|
// error cos not exist CloseTag </TAG>
|
2006-10-01 12:39:50 +00:00
|
|
|
if( pi->error_string.empty() )
|
2005-01-07 14:28:00 +00:00
|
|
|
pi->error_string = ssprintf( "it must be closed with </%s>", m_sName.c_str() );
|
2005-09-07 02:05:19 +00:00
|
|
|
return string::npos;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
2004-02-09 19:33:56 +00:00
|
|
|
|
2006-10-01 12:29:14 +00:00
|
|
|
SetString( xml, iOffset, iEnd, &m_sValue, true );
|
2005-09-03 08:28:43 +00:00
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
iOffset = iEnd;
|
2005-09-03 08:28:43 +00:00
|
|
|
//TEXTVALUE
|
2006-10-01 12:29:14 +00:00
|
|
|
ReplaceEntityText( m_sValue, g_mapEntitiesToChars );
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2005-09-06 18:25:16 +00:00
|
|
|
return iOffset;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Desc : convert plain xml text from parsed xml attirbute
|
|
|
|
|
// Return : converted plain string
|
2005-12-27 23:43:49 +00:00
|
|
|
bool XNode::GetAttrXML( RageFileBasic &f, DISP_OPT &opt, const RString &sName, const RString &sValue ) const
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-12-27 23:43:49 +00:00
|
|
|
RString s(sValue);
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.reference_value )
|
2005-09-04 23:05:58 +00:00
|
|
|
ReplaceEntityText( s, g_mapCharsToEntities );
|
2005-10-11 10:24:07 +00:00
|
|
|
return f.Write(sName + "='" + s + "' ") != -1;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Desc : convert plain xml text from parsed xml node
|
|
|
|
|
// Return : converted plain string
|
2005-11-22 21:14:48 +00:00
|
|
|
bool XNode::GetXML( RageFileBasic &f, DISP_OPT &opt ) const
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
|
|
|
|
// tab
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.newline )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-09-03 08:28:43 +00:00
|
|
|
if( f.Write("\r\n") == -1 )
|
|
|
|
|
return false;
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.write_tabs )
|
|
|
|
|
for( int i = 0 ; i < opt.tab_base ; i++)
|
2004-08-28 21:37:57 +00:00
|
|
|
if( f.Write("\t") == -1 )
|
|
|
|
|
return false;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// <TAG
|
2005-01-07 14:28:00 +00:00
|
|
|
if( f.Write("<" + m_sName) == -1 )
|
2004-06-06 20:47:48 +00:00
|
|
|
return false;
|
2004-02-09 08:10:01 +00:00
|
|
|
|
|
|
|
|
// <TAG Attr1="Val1"
|
2005-01-07 14:28:00 +00:00
|
|
|
if( !m_attrs.empty() )
|
2004-06-06 20:47:48 +00:00
|
|
|
if( f.Write(" ") == -1 )
|
|
|
|
|
return false;
|
2005-02-18 03:07:22 +00:00
|
|
|
FOREACH_CONST_Attr( this, p )
|
2005-10-18 07:00:33 +00:00
|
|
|
if( !GetAttrXML(f, opt, p->first, p->second) )
|
2004-06-06 20:47:48 +00:00
|
|
|
return false;
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2005-01-07 14:28:00 +00:00
|
|
|
if( m_childs.empty() && m_sValue.empty() )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
|
|
|
|
// <TAG Attr1="Val1"/> alone tag
|
2004-06-06 20:47:48 +00:00
|
|
|
if( f.Write("/>") == -1 )
|
|
|
|
|
return false;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// <TAG Attr1="Val1"> and get child
|
2004-06-06 20:47:48 +00:00
|
|
|
if( f.Write(">") == -1 )
|
|
|
|
|
return false;
|
|
|
|
|
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.newline && !m_childs.empty() )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-11-22 21:14:48 +00:00
|
|
|
opt.tab_base++;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
2005-02-18 03:07:22 +00:00
|
|
|
FOREACH_CONST_Child( this, p )
|
2005-01-07 09:09:23 +00:00
|
|
|
if( !p->GetXML( f, opt ) )
|
2004-06-06 20:47:48 +00:00
|
|
|
return false;
|
2004-02-09 08:10:01 +00:00
|
|
|
|
|
|
|
|
// Text Value
|
2005-09-04 23:05:58 +00:00
|
|
|
if( !m_sValue.empty() )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.newline && !m_childs.empty() )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.newline )
|
2004-06-06 20:47:48 +00:00
|
|
|
if( f.Write("\r\n") == -1 )
|
|
|
|
|
return false;
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.write_tabs )
|
|
|
|
|
for( int i = 0 ; i < opt.tab_base ; i++)
|
2004-08-28 21:37:57 +00:00
|
|
|
if( f.Write("\t") == -1 )
|
|
|
|
|
return false;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
2005-12-27 23:43:49 +00:00
|
|
|
RString s( m_sValue );
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.reference_value )
|
2005-09-04 23:05:58 +00:00
|
|
|
ReplaceEntityText( s, g_mapCharsToEntities );
|
|
|
|
|
if( f.Write(s) == -1 )
|
2004-06-06 20:47:48 +00:00
|
|
|
return false;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// </TAG> CloseTag
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.newline && !m_childs.empty() )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2004-06-06 20:47:48 +00:00
|
|
|
if( f.Write("\r\n") == -1 )
|
|
|
|
|
return false;
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.write_tabs )
|
|
|
|
|
for( int i = 0 ; i < opt.tab_base-1 ; i++)
|
2004-08-28 21:37:57 +00:00
|
|
|
if( f.Write("\t") == -1 )
|
|
|
|
|
return false;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
2005-01-07 14:28:00 +00:00
|
|
|
if( f.Write("</" + m_sName + ">") == -1 )
|
2004-06-06 20:47:48 +00:00
|
|
|
return false;
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2005-11-22 21:14:48 +00:00
|
|
|
if( opt.newline )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-01-07 14:28:00 +00:00
|
|
|
if( !m_childs.empty() )
|
2005-11-22 21:14:48 +00:00
|
|
|
opt.tab_base--;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
2004-06-06 20:47:48 +00:00
|
|
|
return true;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
2005-06-16 03:08:45 +00:00
|
|
|
// Desc : convert plain xml text from parsed xml node
|
|
|
|
|
// Return : converted plain string
|
2005-12-27 23:43:49 +00:00
|
|
|
RString XNode::GetXML() const
|
2005-06-16 03:08:45 +00:00
|
|
|
{
|
|
|
|
|
RageFileObjMem f;
|
2005-11-22 21:14:48 +00:00
|
|
|
DISP_OPT opt;
|
|
|
|
|
GetXML( f, opt );
|
2005-06-16 03:08:45 +00:00
|
|
|
return f.GetString();
|
|
|
|
|
}
|
|
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
void XNode::GetValue( RString &out ) const { out = m_sValue; }
|
2005-09-03 08:28:43 +00:00
|
|
|
void XNode::GetValue( int &out ) const { out = atoi(m_sValue); }
|
2006-06-12 06:42:25 +00:00
|
|
|
void XNode::GetValue( float &out ) const { out = StringToFloat(m_sValue); }
|
2005-09-03 08:28:43 +00:00
|
|
|
void XNode::GetValue( bool &out ) const { out = atoi(m_sValue) != 0; }
|
|
|
|
|
void XNode::GetValue( unsigned &out ) const { out = 0; sscanf(m_sValue,"%u",&out); }
|
|
|
|
|
void XNode::GetValue( DateTime &out ) const { out.FromString( m_sValue ); }
|
2004-07-18 21:55:49 +00:00
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
bool XNode::GetAttrValue( const RString &sName, RString &out ) const { const RString* pAttr=GetAttr(sName); if(pAttr==NULL) return false; out = *pAttr; return true; }
|
|
|
|
|
bool XNode::GetAttrValue( const RString &sName, int &out ) const { const RString* pAttr=GetAttr(sName); if(pAttr==NULL) return false; out = atoi(*pAttr); return true; }
|
2006-06-12 06:42:25 +00:00
|
|
|
bool XNode::GetAttrValue( const RString &sName, float &out ) const { const RString* pAttr=GetAttr(sName); if(pAttr==NULL) return false; out = StringToFloat(*pAttr); return true; }
|
2005-12-27 23:43:49 +00:00
|
|
|
bool XNode::GetAttrValue( const RString &sName, bool &out ) const { const RString* pAttr=GetAttr(sName); if(pAttr==NULL) return false; out = atoi(*pAttr) != 0; return true; }
|
|
|
|
|
bool XNode::GetAttrValue( const RString &sName, unsigned &out ) const { const RString* pAttr=GetAttr(sName); if(pAttr==NULL) return false; out = 0; sscanf(*pAttr,"%u",&out); return true; }
|
|
|
|
|
bool XNode::GetAttrValue( const RString &sName, DateTime &out ) const { const RString* pAttr=GetAttr(sName); if(pAttr==NULL) return false; out.FromString( *pAttr ); return true; }
|
2004-07-18 21:55:49 +00:00
|
|
|
|
2005-09-03 08:28:43 +00:00
|
|
|
void XNode::SetValue( int v ) { m_sValue = ssprintf("%d",v); }
|
|
|
|
|
void XNode::SetValue( float v ) { m_sValue = ssprintf("%f",v); }
|
|
|
|
|
void XNode::SetValue( bool v ) { m_sValue = ssprintf("%d",v); }
|
|
|
|
|
void XNode::SetValue( unsigned v ) { m_sValue = ssprintf("%u",v); }
|
|
|
|
|
void XNode::SetValue( const DateTime &v ) { m_sValue = v.GetString(); }
|
2004-07-17 21:49:32 +00:00
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
const RString *XNode::GetAttr( const RString &attrname ) const
|
2004-02-22 05:12:23 +00:00
|
|
|
{
|
2005-12-27 23:43:49 +00:00
|
|
|
map<RString, RString>::const_iterator it = m_attrs.find( attrname );
|
2005-01-07 14:28:00 +00:00
|
|
|
if( it != m_attrs.end() )
|
2005-10-11 10:24:07 +00:00
|
|
|
return &it->second;
|
2004-02-22 05:12:23 +00:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
RString *XNode::GetAttr( const RString &attrname )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-12-27 23:43:49 +00:00
|
|
|
map<RString, RString>::iterator it = m_attrs.find( attrname );
|
2005-01-07 14:28:00 +00:00
|
|
|
if( it != m_attrs.end() )
|
2005-10-11 10:24:07 +00:00
|
|
|
return &it->second;
|
2004-02-09 08:10:01 +00:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Desc : Find child with name and return child
|
|
|
|
|
// Return : NULL return if no child.
|
2005-12-27 23:43:49 +00:00
|
|
|
XNode *XNode::GetChild( const RString &sName )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-12-27 23:43:49 +00:00
|
|
|
multimap<RString, XNode*>::iterator it = m_childs.find( sName );
|
2005-01-07 14:28:00 +00:00
|
|
|
if( it != m_childs.end() )
|
|
|
|
|
{
|
2005-09-04 16:45:38 +00:00
|
|
|
DEBUG_ASSERT( sName == it->second->m_sName );
|
2005-01-07 09:09:23 +00:00
|
|
|
return it->second;
|
2005-01-07 14:28:00 +00:00
|
|
|
}
|
2004-02-09 08:10:01 +00:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
const XNode *XNode::GetChild( const RString &sName ) const
|
2004-02-19 03:19:41 +00:00
|
|
|
{
|
2005-12-27 23:43:49 +00:00
|
|
|
multimap<RString, XNode*>::const_iterator it = m_childs.find( sName );
|
2005-01-07 14:28:00 +00:00
|
|
|
if( it != m_childs.end() )
|
|
|
|
|
{
|
2005-09-04 16:45:38 +00:00
|
|
|
DEBUG_ASSERT( sName == it->second->m_sName );
|
2005-01-07 09:09:23 +00:00
|
|
|
return it->second;
|
2005-01-07 14:28:00 +00:00
|
|
|
}
|
2004-02-19 03:19:41 +00:00
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
XNode *XNode::AppendChild( const RString &sName, const RString &value ) { XNode *p = new XNode; p->m_sName = sName; p->m_sValue = value; return AppendChild( p ); }
|
|
|
|
|
XNode *XNode::AppendChild( const RString &sName, float value ) { XNode *p = new XNode; p->m_sName = sName; p->SetValue( value ); return AppendChild( p ); }
|
|
|
|
|
XNode *XNode::AppendChild( const RString &sName, int value ) { XNode *p = new XNode; p->m_sName = sName; p->SetValue( value ); return AppendChild( p ); }
|
|
|
|
|
XNode *XNode::AppendChild( const RString &sName, unsigned value ) { XNode *p = new XNode; p->m_sName = sName; p->SetValue( value ); return AppendChild( p ); }
|
|
|
|
|
XNode *XNode::AppendChild( const RString &sName, const DateTime &value ) { XNode *p = new XNode; p->m_sName = sName; p->SetValue( value ); return AppendChild( p ); }
|
2004-04-23 00:53:29 +00:00
|
|
|
|
2004-07-23 02:27:37 +00:00
|
|
|
XNode *XNode::AppendChild( XNode *node )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-01-07 14:28:00 +00:00
|
|
|
DEBUG_ASSERT( node->m_sName.size() );
|
2005-10-11 09:35:53 +00:00
|
|
|
|
|
|
|
|
/* Hinted insert: optimize for alphabetical inserts, for the copy ctor. */
|
2005-12-27 23:43:49 +00:00
|
|
|
m_childs.insert( m_childs.end(), pair<RString,XNode*>(node->m_sName,node) );
|
2004-02-09 08:10:01 +00:00
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
|
2005-07-15 22:58:11 +00:00
|
|
|
// detach node and delete object
|
2004-07-23 02:27:37 +00:00
|
|
|
bool XNode::RemoveChild( XNode *node )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-12-27 23:43:49 +00:00
|
|
|
FOREACHMM( RString, XNode*, m_childs, p )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-01-07 09:09:23 +00:00
|
|
|
if( p->second == node )
|
|
|
|
|
{
|
|
|
|
|
SAFE_DELETE( p->second );
|
2005-01-07 14:28:00 +00:00
|
|
|
m_childs.erase( p );
|
2005-01-07 09:09:23 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2005-10-11 10:24:07 +00:00
|
|
|
// detach attribute
|
2005-12-27 23:43:49 +00:00
|
|
|
bool XNode::RemoveAttr( const RString &sName )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-10-11 10:24:07 +00:00
|
|
|
return m_attrs.erase(sName) > 0;
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
void XNode::AppendAttr( const RString &sName, const RString &sValue )
|
2004-02-09 08:10:01 +00:00
|
|
|
{
|
2005-10-11 11:12:24 +00:00
|
|
|
pair<XAttrs::iterator,bool> ret = m_attrs.insert( make_pair(sName,sValue) );
|
|
|
|
|
if( !ret.second )
|
|
|
|
|
ret.first->second = sValue; // already existed
|
2004-02-09 08:10:01 +00:00
|
|
|
}
|
|
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
void XNode::AppendAttr( const RString &sName, float value ){ AppendAttr(sName,ssprintf("%f",value)); }
|
|
|
|
|
void XNode::AppendAttr( const RString &sName, int value ) { AppendAttr(sName,ssprintf("%d",value)); }
|
|
|
|
|
void XNode::AppendAttr( const RString &sName, unsigned value ) { AppendAttr(sName,ssprintf("%u",value)); }
|
2004-02-09 08:10:01 +00:00
|
|
|
|
2005-11-22 21:14:48 +00:00
|
|
|
bool XNode::SaveToFile( RageFileBasic &f, DISP_OPT &opt ) const
|
2004-02-10 09:42:01 +00:00
|
|
|
{
|
2004-07-11 01:58:55 +00:00
|
|
|
f.PutLine( "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>" );
|
2005-11-22 21:14:48 +00:00
|
|
|
if( !opt.stylesheet.empty() )
|
|
|
|
|
f.PutLine( "<?xml-stylesheet type=\"text/xsl\" href=\"" + opt.stylesheet + "\"?>" );
|
2004-06-06 20:47:48 +00:00
|
|
|
if( !this->GetXML(f, opt) )
|
|
|
|
|
return false;
|
|
|
|
|
if( f.Flush() == -1 )
|
|
|
|
|
return false;
|
2004-02-10 09:42:01 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
2004-07-18 21:55:49 +00:00
|
|
|
|
2005-12-27 23:43:49 +00:00
|
|
|
bool XNode::SaveToFile( const RString &sFile, DISP_OPT &opt ) const
|
2004-12-18 05:37:53 +00:00
|
|
|
{
|
|
|
|
|
RageFile f;
|
|
|
|
|
if( !f.Open(sFile, RageFile::WRITE) )
|
|
|
|
|
{
|
2006-02-15 00:39:02 +00:00
|
|
|
LOG->Warn( "Couldn't open %s for writing: %s", sFile.c_str(), f.GetError().c_str() );
|
2004-12-18 05:37:53 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return SaveToFile( f, opt );
|
|
|
|
|
}
|