//****************************************************************************
// Filename: XmlElements.h
// Copyright 1999 Daniel X. Pape. All rights reserved.
//
// Description: A set of classes for reading and parsing simple XML files.
//
//****************************************************************************
// Revision History:
// Thursday, July 08, 1999 - Original. Heavily based on "A Simple XML
//   Parser" by Sebastien Andrivet. See Documentation.
//****************************************************************************

#include "XmlParser.h"

using std::string;
using std::auto_ptr;


// Macro to get the number of elements in a array
#ifndef elemof
#define elemof(array) (sizeof(array) / sizeof((array)[0]))
#endif

// Our namespace
namespace SimpleXMLParser
{

// ***************************************************************************
// Function: Constructor
//     Desc: 
// ***************************************************************************
XmlParser::XmlParser()
    : source_(0), sourceCurrent_(0), sourceEnd_(0), 
    line_(1), column_(1), xmlVersion_("1.0")
{
}

// ***************************************************************************
// Function: SyntaxError
//     Desc: Syntax error (throw exception)
// ***************************************************************************
void XmlParser::SyntaxError()
{
    throw XmlException(line_, column_);
}

// ***************************************************************************
// Function: NextChar
//     Desc: Next char (next position)
// ***************************************************************************
char XmlParser::NextChar()
{
    if(sourceCurrent_ >= sourceEnd_) return(0);
    char c = *sourceCurrent_++;

    // Skip \r if any
    if(c == '\r')
    {
        if(sourceCurrent_ >= sourceEnd_) return(0);
        c = *sourceCurrent_++;
    }

    if(c == '\n')
        ++line_, column_ = 1;
    else
        ++column_;

    return(c);
}

// ***************************************************************************
// Function: PreviousChar
//     Desc: Previous position
// ***************************************************************************
void XmlParser::PreviousChar()
{
    if(sourceCurrent_ - 1 < source_)
        sourceCurrent_ = source_;
    else
        sourceCurrent_ -= 1;
}

// ***************************************************************************
// Function: ParseString
//     Desc: 
// ***************************************************************************
bool XmlParser::ParseString(const char* str)
{
    Bookmark bookmark(*this);

    // not end of string ?
    while(*str != 0)
    {
        // Next char
        char c = NextChar();
        // Same ?
        if(c != *str)
        {
            // Not the same so revert back to
            // the previous position
            bookmark.Restore();
            return(false);
        }

        // Next char of the string
        ++str;
    }

    return(true);
}

// ***************************************************************************
// Function: ParseStringNoCase
//     Desc: Read the given string (not case sensitive)
// ***************************************************************************
bool XmlParser::ParseStringNoCase(const char* str)
{
    Bookmark bookmark(*this);

    // not end of string ?
    while(*str != 0)
    {
        // Next char
        char c = NextChar();
        // Same (not case sensitive) ?
        if(LowCase(c) != LowCase(*str))
        {
            // Not the same so revert back to
            // the previous position
            bookmark.Restore();
            return(false);
        }

        // Next char of the string
        ++str;
    }

    return(true);
}

// ***************************************************************************
// Function: ParseNumber
//     Desc: Read a (decimal) number
// ***************************************************************************
bool XmlParser::ParseNumber(int& n)
{
    char c = NextChar();

    if(!IsDigit(c))
        return(false); // not a number
    
    n = 0;
    while(IsDigit(c))
    {
        // Compute new number
        n = n * 10 + c - '0';
        // Next char
        c = NextChar();
    }

    // The current char if not part of the number
    PreviousChar();

    return(true);
}

// ***************************************************************************
// Function: ParseHexNumber
//     Desc: Read an hexadecimal number
// ***************************************************************************
bool XmlParser::ParseHexNumber(int& n)
{
    char c = NextChar();

    if(!IsHexDigit(c))
        return(false);

    n = 0;
    // Read all digits possible
    while(IsHexDigit(c))
    {
        // Compute new number
        n = n * 16 + HexDigitValue(c);
        // Next char
        c = NextChar();
    }

    // The current char if not part of the number
    PreviousChar();

    return(true);
}

// ***************************************************************************
// Function: ParseChar
//     Desc: 
// ***************************************************************************
bool XmlParser::ParseChar(char c)
{
    if(NextChar() != c)
    {
        PreviousChar();
        return(false);
    }
    return(true);
}

// ***************************************************************************
// Function: ParseSpaces
//     Desc: Read One or more spaces
//           S ::= (#x20 | #x9 | #xD | #xA)+
// ***************************************************************************
bool XmlParser::ParseSpaces()
{
    char c = NextChar();
    if(!IsSpace(c))
    {
        PreviousChar();
        return(false);
    }

    do c = NextChar(); 
    while(IsSpace(c));
    
    PreviousChar();
    
    return(true);
}

// ***************************************************************************
// Function: ParseDeclBegining
//     Desc: Parse a declaration (like: version = )
//           S <szString> Eq
// ***************************************************************************
bool XmlParser::ParseDeclBegining(const char * szString)
{
    // Parse: S
    char c = NextChar();
    if(!IsSpace(c))
    {
        PreviousChar();
        return(false);
    }
    ParseSpaces();

    // Parse: <szString>
    if(!ParseString(szString))
        return(false);

    // Parse: Eq
    if(!ParseEq())
        SyntaxError();

    return(true);
}

// ***************************************************************************
// Function: ParseXMLDecl
//     Desc: Parse XML declaration
//           XMLDecl ::= '<?xml' VersionInfo EncodingDecl? S? '?>'
//           Marker: '<?xml'
// ***************************************************************************
bool XmlParser::ParseXMLDecl()
{
    // Parse: '<?xml'
    if(!ParseStringNoCase("<?xml"))
        return(false);

    // Parse: VersionInfo EncodingDecl? SDDecl? S? '?>'
    if(!ParseVersionInfo(xmlVersion_))
        SyntaxError();

    // Parse EncodingDecl (optional)
    ParseEncodingDecl();
    // Parse S (spaces) (optional)
    ParseSpaces();

    // Parse end of declaration '?>'
    if(!ParseString("?>"))
        SyntaxError();

    return(true);
}

// ***************************************************************************
// Function: ParseEq
//     Desc: Parse equal sign
//           Eq ::= S? '=' S?
//           Marker: '='
// ***************************************************************************
bool XmlParser::ParseEq()
{
    // Record the current position
    Bookmark bookmark(*this);

    // Parse spaces (optional)
    ParseSpaces();
    // Is an equal sign ?
    if(!ParseChar('='))
    {
        // No, so revert to the previous position
        bookmark.Restore();
        return(false);
    }
    // Skip spaces if any
    ParseSpaces();

    return(true);
}

// ***************************************************************************
// Function: ParseVersionInfo
//     Desc: Parse XML version
//           VersionInfo ::= S 'version' Eq (' VersionNum ' | " VersionNum ")
//           Marker: 'version'
// ***************************************************************************
bool XmlParser::ParseVersionInfo(string& version)
{
    // Parse: S 'version' Eq
    if(!ParseDeclBegining("version"))
        return(false);

    // Parse: (' VersionNum ' | " VersionNum ")
    char c = NextChar();
    if(c != '\'' && c != '\"')
        SyntaxError();

    // Parse version number and check the delimiter
    if(!ParseVersionNum(version) || NextChar() != c)
        SyntaxError();

    return(true);
}

// ***************************************************************************
// Function: ParseVersionNum
//     Desc: Parse XML version number
//           VersionNum ::= ([a-zA-Z0-9_.:] | '-')+
// ***************************************************************************
bool XmlParser::ParseVersionNum(string& version)
{
    // Record the current position
    Bookmark bookmark(*this);

    char c = NextChar();
    // Is an allowed character ?
    if(!IsAlphaDigitEx(c))
        return(false);

    c = NextChar();
    // Get as more char as possible
    while(IsAlphaDigitEx(c))
        c = NextChar();

    // Current character is not part of the version num.
    PreviousChar();

    // Get the version number
    bookmark.GetSubString(version);
    return(true);
}

// ***************************************************************************
// Function: ParseEncodingDecl
//     Desc: Parse XML encoding declaration
//           EncodingDecl ::=  S 'encoding' Eq 
//                             ('"' EncName '"' |  "'" EncName "'")
//           Marker: 'encoding'
// ***************************************************************************
bool XmlParser::ParseEncodingDecl()
{
    // Parse: S 'encoding' Eq
    if(!ParseDeclBegining("encoding"))
        return(false);

    // Parse: ('"' EncName '"' |  "'" EncName "'")
    char c = NextChar();
    if(c != '\'' && c != '\"')
        SyntaxError();

    // Parse encoding name and check delimiter
    // Bug#0002: Was ParseEncName() instead of !ParseEncName()
    if(!ParseEncName() || NextChar() != c)
        SyntaxError();

    return(true);
}

// ***************************************************************************
// Function: ParseEncName
//     Desc: Parse encoding name
//           EncName ::=  [A-Za-z] ([A-Za-z0-9._] | '-')*
// ***************************************************************************
bool XmlParser::ParseEncName()
{
    char c = NextChar();
    // Is an allowed character ?
    if(!IsAlpha(c))
        return(false);

    c = NextChar();
    // Get as more char as possible
    while(IsAlphaDigitEx(c))
        c = NextChar();

    // Current character is not part of the version num.
    PreviousChar();

    // In this version, the encoding is not used
    return(true);
}

// ***************************************************************************
// Function: ParseMiscs
//     Desc: Parse Comments, spaces, etc.
//           Misc*
//           Misc ::= Comment | S
// ***************************************************************************
void XmlParser::ParseMiscs()
{
    for(;;)
    {
        // Parse spaces if any
        ParseSpaces();

        // Parse comment if any
        auto_ptr<ElementComment> pElem(ParseComment());
        if(0 == pElem.get())
            break;
    }
}

// ***************************************************************************
// Function: ParseComment
//     Desc: Parse comment and construct an element
//           Comment ::= '<!--' ((Char - '-') | ('-' (Char - '-')))* '-->'
// ***************************************************************************
ElementComment* XmlParser::ParseComment()
{
    // Check the start of the comment
    if(!ParseString("<!--"))
        return(0);

    // Record the current position to extract later the
    // content of the comment
    Bookmark bookmark(*this);
    for(;;)
    {
        // Look for the end of the comment
        if(ParseString("--"))
        {
            // Really the end ?
            if(!ParseChar('>'))
                SyntaxError();
            break;
        }

        // End of document ?
        if(NextChar() == 0)
            SyntaxError();
    }

    // Extract the content of the comment
    string strComment;
    bookmark.GetSubString(strComment, 3);

    // Construct an element
    return(new ElementComment(strComment));
}

// ***************************************************************************
// Function: ParseName
//     Desc: Read a name (letters, digits and special chars)
//           Name ::=  (Letter | '_' | ':') (NameChar)* 
//           NameChar ::=  Letter | Digit | '.' | '-' | 
//                         '_' | ':' | CombiningChar | Extender 
// ***************************************************************************
bool XmlParser::ParseName(string& name)
{
    // Record the current position to extract later the name
    Bookmark bookmark(*this);

    char c = NextChar();
    // Is allowed ?
    if(!IsAlpha(c) && c != '_' && c != ':')
    {
        PreviousChar();
        return(false);
    }

    // Get as more (allowed) char as possible
    for(;;)
    {
        c = NextChar();
        // Is allowed ?
        if(!IsAlphaDigitEx(c))
            break;
    }

    // Current character is not part of the version num.
    PreviousChar();

    // Extract the name
    bookmark.GetSubString(name);
    return(true);
}

// ***************************************************************************
// Function: ParseReference
//     Desc: Parse reference
//           Reference ::=  EntityRef | CharRef 
//           EntityRef ::=  '&' Name ';' 
//           CharRef ::=  '&#' [0-9]+ ';' | '&#x' [0-9a-fA-F]+ ';' 
//           Marker: '&'
// ***************************************************************************
bool XmlParser::ParseReference(char& cRef)
{
    // Begin like a reference ?
    if(!ParseChar('&'))
        return(false);

    char c = NextChar();
    // EntityRef ? (not a CharRef)
    if(c != '#')
    {
        // It is an EntityRef
        PreviousChar();
        // Get the name of the reference and check the end (';')
        string strReferenceName;
        if(!ParseName(strReferenceName) || !ParseChar(';'))
            SyntaxError();
        // Look for the reference
        if(!MapReferenceName(strReferenceName, cRef))
            SyntaxError();

        return(true);
    }

    // It is a CharRef
    c = NextChar();
    // Compute the value (character code)
    int n = 0;
    // Hexadecimal ?
    if(c == 'x')
    {
        // Get the value
        if(!ParseHexNumber(n))
            SyntaxError();
    }
    else
    {
        PreviousChar();
        // Get the value
        if(!ParseNumber(n))
            SyntaxError();
    }

    // Check the end of the reference
    if(!ParseChar(';'))
        SyntaxError();

    // Return the character with the computed code
    cRef = static_cast<char>(n);
    return(true);
}

// ***************************************************************************
// Function: ParseAttValue
//     Desc: Parse attribute value
//           '"' ([^<&"] | Reference)* '"'  | "'" ([^<&'] | Reference)* "'" 
//           Marker: '"' | "'"
// ***************************************************************************
bool XmlParser::ParseAttValue(string& value)
{
    // Get the value delimiter (quote or apostrophe)
    char cDelim = NextChar();
    if(cDelim != '\'' && cDelim != '\"')
    {
        PreviousChar();
        return(false);
    }

    // Record the current position to extract later the value
    Bookmark bookmark(*this);

    char c = NextChar();
    // Search the end of the value
    while(c != cDelim)
    {
        switch(c)
        {
        case 0: // end of document
            SyntaxError();

        case '<': // Tag
            SyntaxError();

        case '&': // Reference
            {
                PreviousChar();
                // Put what we already have in the value
                string before;
                bookmark.GetSubString(before);
                value += before;

                char c;
                // Get the reference
                if(!ParseReference(c))
                    SyntaxError();

                // Put the char in the value
                value += c;
                // Record the new position (after the reference)
                bookmark.Reset();
            }
                //break;
                // fall-though intentional -- DXP
        default:
            // Next character
            c = NextChar();
            break;
        }
    }

    // Put the remaining of the value
    string remaining;
    bookmark.GetSubString(remaining, 1);
    value += remaining;

    return(true);
}

// ***************************************************************************
// Function: ParseAttribute
//     Desc: Parse Attribute
//           Attribute ::=  Name Eq AttValue
//           Marker: Name
// ***************************************************************************
bool XmlParser::ParseAttribute(ElementTag* elem)
{
    // Get attribute name
    string name;
    if(!ParseName(name))
        return(false);

    // Get attribute value after the equal sign
    string value;
    if(!ParseEq() || !ParseAttValue(value))
        SyntaxError();

    // Construct an Attribute object and add it to the element
    elem->AddAttribute(name, value);

    return(true);
}

// ***************************************************************************
// Function: ParseCDATA
//     Desc: Parse CDATA
//           CDSect ::=  '<![CDATA[' CData ']]>' 
//           CData ::=  (Char* - (Char* ']]>' Char*))  
//           Marker: '<![CDATA[' 
// ***************************************************************************
bool XmlParser::ParseCDATA(Element& elem)
{
    // Parse: <![CDATA[
    if(!ParseString("<![CDATA["))
        return(false);

    Bookmark bookmark(*this);
    // Parse: CData
    for(;;)
    {
        // End of CDATA ?
        if(ParseString("]]>"))
            break;

        // Is character allowed ?
        if(!IsXmlChar(NextChar()))
            SyntaxError();
    }

    // Get CDATA content and add it as-is to the value
    string strCDATA;
    bookmark.GetSubString(strCDATA, 3);
    elem.AddValue(strCDATA);

    return(true);
}

// ***************************************************************************
// Function: ParseTagBegining
//     Desc: Parse start tag and construct an element
//           '<' Name (S Attribute)* S?
//           Marker: '<'
// ***************************************************************************
ElementTag* XmlParser::ParseTagBegining()
{
    // Parse: '<'
    if(!ParseChar('<'))
        return(0);

    // Get the name of the tag
    string strName;
    if(!ParseName(strName))
        SyntaxError();

    // Construct an element object
    auto_ptr<ElementTag> pElem(new ElementTag(strName));

    // Parse: (S Attribute)* S?
    while(ParseSpaces() && ParseAttribute(pElem.get()))
        ;

    // return the element to the caller
    return(pElem.release());
}

// ***************************************************************************
// Function: ParseETag
//     Desc: Parse end tag
//           ETag ::=  '</' Name S? '>'
//           Marker: '</'
// ***************************************************************************
bool XmlParser::ParseETag(Element& elem)
{
    // Is an End tag ?
    if(!ParseString("</"))
        return(false);

    // Get the tag name
    string endTagName;
    if(!ParseName(endTagName))
        SyntaxError();

    // Start and end tag names must match
    if(endTagName != elem.GetName())
        SyntaxError();

    // Skip spaces
    ParseSpaces();
    // End of the tag
    if(!ParseChar('>'))
        SyntaxError();

    return(true);
}

// ***************************************************************************
// Function: ParseMarkup
//     Desc: Parse markups like comments, CDATA, elements, etc.
//           Comment   begins with '<!--'
//           CDSect    begins with '<![CDATA['
//           element   begins with '<'
// ***************************************************************************
bool XmlParser::ParseMarkup(Element& elem)
{
    // Try to read a comment
    auto_ptr<ElementComment> comment(ParseComment());
    if(0 != comment.get())
    {
        // Add it to the element
        if(!elem.AddChild(comment.release()))
            SyntaxError();
        return(true);
    }

    // Try to read a CDATA
    if(ParseCDATA(elem))
        return(true);

    // Try to read an element
    auto_ptr<ElementTag> tag(ParseElement());
    if(0 != tag.get())
    {
        if(!elem.AddChild(tag.release()))
            SyntaxError();
        return(true);
    }

    return(false);
}

// ***************************************************************************
// Function: ParseContentETag
//     Desc: Parse element content and end tag
//           content  ::=  (element | CharData | Reference | CDSect | Comment)*
//           CharData ::=  [^<&]* - ([^<&]* ']]>' [^<&]*) 
//           Reference begins with '&'
// ***************************************************************************
void XmlParser::ParseContentETag(ElementTag& elem)
{
    // Record the current position to extract later the content
    Bookmark bookmark(*this);

    char c = NextChar();
    for(;;)
    {
        // "]]>" not allowed in content
        if(ParseString("]]>"))
            SyntaxError();

        switch(c)
        {
        case 0: // End of document
            SyntaxError();
            break;

        case '&':
        case '<':
            {
                PreviousChar();

                // Put what we already have in the value
                string value;
                bookmark.GetSubString(value);
                elem.AddValue(value);

                // Tag or reference ?
                if(c == '&')
                {
                    // Get the reference
                    if(!ParseReference(c))
                        SyntaxError();

                    // Add it to the value
                    elem.AddValue(c);
                }
                else
                {
                    // Is an end tag ?
                    if(ParseETag(elem))
                        return;
                    else if(!ParseMarkup(elem))
                        SyntaxError();
                }

                bookmark.Reset();
            }
            break;

        default:
            break;
        }

        // Next char
        c = NextChar();
    }
}

// ***************************************************************************
// Function: ParseElement
//     Desc: Parse element and construct an object
//           element      ::= EmptyElemTag | STag content ETag
//           EmptyElemTag ::=  '<' Name (S Attribute)* S? '/>'
//           STag         ::=  '<' Name (S Attribute)* S? '>'  
//           Marker: '<'
// ***************************************************************************
ElementTag* XmlParser::ParseElement()
{
    // Begining of element (start tag)
    auto_ptr<ElementTag> tag(ParseTagBegining());
    if(tag.get() == 0)
        return(0);

    char c = NextChar();
    if(c == '/') // Empty tag ?
    {
        c = NextChar();
        if(c != '>')
            SyntaxError();

        return(tag.release());
    }

    // End of the tag
    if(c != '>')
        SyntaxError();

    // Parse the remaining of the element
    ParseContentETag(*tag);
    return(tag.release());
}

// ***************************************************************************
// Function: ParseDocument
//     Desc: Parse document and construct a (root) element
//           document ::= XMLDecl? Misc* element Misc*
// ***************************************************************************
Element* XmlParser::ParseDocument()
{
    // XML declaration (optional)
    ParseXMLDecl();
    ParseMiscs();

    // Get the root element
    auto_ptr<Element> elem(ParseElement());
    if(0 == elem.get())
        SyntaxError();

    ParseMiscs();

    return(elem.release());
}

// ***************************************************************************
// Function: Parse
//     Desc: parse document
// ***************************************************************************
Element& XmlParser::Parse(const char * szSource, int nSourceSize)
{
    source_ = szSource;
    sourceCurrent_ = source_;
    sourceEnd_ = source_ + nSourceSize;
    line_ = column_ = 1;

    // Get the root eleement
#ifdef _MSC_VER
    rootElem_ = std::auto_ptr<Element>(ParseDocument());
#else
    rootElem_.reset(ParseDocument());
#endif

    return(*rootElem_);
}

//////////////////////////////////////////////////////////////////////
// Entity references
//////////////////////////////////////////////////////////////////////

// Map a name to a character
struct MapReference
{
    const char * szName;
    char c;
};

// Predefined refererences
static const MapReference s_MapReference[] = 
{
    { "lt",   '<' },
    { "gt",   '>' },
    { "amp",  '&' },
//    { "apos", '\"' },
//    { "quot", '\'' } DXP
    { "apos", '\'' },
    { "quot", '\"' }
};

// ***************************************************************************
// Function: MapReferenceName
//     Desc: Find the reference strName and return its equivalent
// ***************************************************************************
bool XmlParser::MapReferenceName(const string& strName, char& c)
{
    for(unsigned int nIndex = 0; nIndex < elemof(s_MapReference); ++nIndex)
    {
        // Same name ?
        if(strName == s_MapReference[nIndex].szName)
        {
            // return the equivalent
            c = s_MapReference[nIndex].c;
            return(true);
        }
    }
    
    return(false);
}

}

