/*
 *  XMLTV element parser
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License Version 2 as
 *  published by the Free Software Foundation. You find a copy of this
 *  license in the file COPYRIGHT in the root directory of this release.
 *
 *  THIS PROGRAM 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 General Public License for more details.
 *
 *
 *  Description:
 *
 *    This module processes XML tags and attributes and the respective
 *    data.  It's based on a scanner generated by _T("flex") which breaks
 *    down the input file into tokens and invokes callback functions
 *    in this module to process these tokens.
 *
 *    TODO:
 *    - re-sync after error when toplevel tags are found: channel, timeslot (programme)
 *    - search stack upwards when mis-matching closing tag is encountered
 *    - don't accept PCDATA or attributes in error state
 *    - don't accept tags or PCDATA after </tv>
 *    - check that attribute is set only once per tag (XML spec ch. 3.1)
 *    - complain about unexpected PCDATA in non-MIXED elements
 *    - strip whitespace from attributes which are not of type CDATA
 *    - transcode non-ASCII chars in tag and attribute names (not req. for XMLTV)
 *    - dynamically grow parser stack if necessary (not req. for w.-f. XMLTV)
 *
 *  Author: Tom Zoerner
 *
 *  $Id: xmltv_tags.cpp 4589 2009-05-08 13:34:28Z adcockj $
 */

#define DEBUG_SWITCH DEBUG_SWITCH_XMLTV
#define DPRINTF_OFF

#include "stdafx.h"
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

#include "xmltv_types.h"
#include "xmltv_debug.h"

#include "xml_cdata.h"
#include "xmltv_db.h"
#include "xmltv_tags.h"

#pragma warning(disable : 4390)

extern FILE *yyin;
extern int yyparse( void );
extern int yylex( void );


//#define ALL_CONTENT


// ----------------------------------------------------------------------------
// Definition of tags and their hierarchy
//
typedef enum
{
   // tags according DTD 0.5
   XMLTV5_TOP,
   XMLTV5_TV,
   XMLTV5_CHANNEL,
      XMLTV5_CHN_DISP_NAME,
#ifdef ALL_CONTENT
      XMLTV5_CHN_ICON,
      XMLTV5_CHN_URL,
#endif
   XMLTV5_PROG,
      XMLTV5_PI_TITLE,
      XMLTV5_PI_TITLE2,
      XMLTV5_PI_DESC,
#ifdef ALL_CONTENT
      XMLTV5_PI_CREDITS,
         XMLTV5_PI_CRED_DIR,
         XMLTV5_PI_CRED_ACT,
         XMLTV5_PI_CRED_WRI,
         XMLTV5_PI_CRED_ADAP,
         XMLTV5_PI_CRED_PROD,
         XMLTV5_PI_CRED_PRES,
         XMLTV5_PI_CRED_COMM,
         XMLTV5_PI_CRED_GUEST,
      XMLTV5_PI_DATE,
#endif
      XMLTV5_PI_CAT,
#ifdef ALL_CONTENT
      XMLTV5_PI_VIDEO,
         XMLTV5_PI_VIDEO_ASPECT,
      XMLTV5_PI_AUDIO,
         XMLTV5_PI_AUDIO_STEREO,
      XMLTV5_PI_SUBT,
      XMLTV5_PI_PRAT,
         XMLTV5_PI_PRAT_VAL,
      XMLTV5_PI_ERAT,
         XMLTV5_PI_ERAT_VAL,
#endif
   // tags according DTD 0.6
   XMLTV6_TOP,
   XMLTV6_TV,
#ifdef ALL_CONTENT
   XMLTV6_ABOUT,
      XMLTV6_SRC_INFO,
      XMLTV6_SRC_DATA,
      XMLTV6_GEN_INFO,
#endif
   XMLTV6_CHANNEL,
      XMLTV6_CHN_DISP_NAME,
   XMLTV6_TIMESLOT,
#ifdef ALL_CONTENT
      XMLTV6_TS_CODE_TIME,
      XMLTV6_TS_CODE_NUM,
      XMLTV6_TS_PROG_REF,
      XMLTV6_TS_PROG_UNKNOWN,
#endif
      XMLTV6_TS_PROG,
         XMLTV6_PI_TITLE,
         XMLTV6_PI_DESC,
            XMLTV6_PI_DESC_P,
#ifdef ALL_CONTENT
         XMLTV6_PI_CREDITS,
            XMLTV6_PI_CRED_DIR,
            XMLTV6_PI_CRED_ROLE,
               XMLTV6_PI_CRED_ROLE_ACTOR,
               XMLTV6_PI_CRED_ROLE_CHAR,
            XMLTV6_PI_CRED_WRITER,
            XMLTV6_PI_CRED_PROD,
            XMLTV6_PI_CRED_EXPR,
            XMLTV6_PI_CRED_PRES,
            XMLTV6_PI_CRED_COMM,
            XMLTV6_PI_CRED_GUEST,
            XMLTV6_PI_CRED_NARR,
            XMLTV6_PI_CRED_COMPANY,
            XMLTV6_PI_CRED_ADAP,
               XMLTV6_PI_CRED_ADAP_TYPE,
               XMLTV6_PI_CRED_ADAP_TITLE,
               XMLTV6_PI_CRED_ADAP_WRITER,
#endif
         XMLTV6_PI_CAT,
         XMLTV6_PI_EPISODE,
            XMLTV6_PI_EPISODE_EP,
               XMLTV6_PI_EPISODE_EP_TITLE,
#ifdef ALL_CONTENT
         XMLTV6_PI_VIDEO,
            XMLTV6_PI_VIDEO_ASPECT,
            XMLTV6_PI_VIDEO_QUAL,
         XMLTV6_PI_AUDIO,
            XMLTV6_PI_AUDIO_MONO,
            XMLTV6_PI_AUDIO_STEREO,
            XMLTV6_PI_AUDIO_SURR,
         XMLTV6_PI_SUBT,
            XMLTV6_PI_SUBT_TTX,
            XMLTV6_PI_SUBT_OSD,
         XMLTV6_PI_PRAT,
            XMLTV6_PI_PRAT_TEXT,
         XMLTV6_PI_ERAT,
   XMLTV6_LINK,
      XMLTV6_LINK_TEXT,
      XMLTV6_LINK_BLURB,
         XMLTV6_LINK_BLURB_P,
#endif
   // DTD auto-detection & catch-all for unrecognized tags
   XMLTV_DETECT,
      XMLTV_DETECT_TV,
      XMLTV_DETECT_PROG,
      XMLTV_DETECT_ABOUT,
      XMLTV_DETECT_TIMESLOT,
   XMLTV_SKIP,
   XMLTV_TAG_COUNT
} XMLTV_TAG;

// ----------------------------------------------------------------------------
// Tag hierarchy for XMLTV DTD 0.5
// - note: tags are sorted by probability to speed-up name comparison

static const XMLTV_TAG xmltv5_tags_top[] =
{
   XMLTV5_TV
};
static const XMLTV_TAG xmltv5_tags_tv[] =
{
   XMLTV5_PROG,
   XMLTV5_CHANNEL
};
static const XMLTV_TAG xmltv5_tags_channel[] =
{
   XMLTV5_CHN_DISP_NAME,
#ifdef ALL_CONTENT
   XMLTV5_CHN_ICON,
   XMLTV5_CHN_URL
#endif
};
static const XMLTV_TAG xmltv5_tags_programme[] =
{
   XMLTV5_PI_TITLE,
   XMLTV5_PI_DESC,
   XMLTV5_PI_CAT,
#ifdef ALL_CONTENT
   XMLTV5_PI_VIDEO,
   XMLTV5_PI_AUDIO,
   XMLTV5_PI_PRAT,
   XMLTV5_PI_ERAT,
   XMLTV5_PI_SUBT,
   XMLTV5_PI_DATE,
   XMLTV5_PI_CREDITS,
#endif
   XMLTV5_PI_TITLE2
};
#ifdef ALL_CONTENT
static const XMLTV_TAG xmltv5_tags_credits[] =
{
   XMLTV5_PI_CRED_DIR,
   XMLTV5_PI_CRED_ACT,
   XMLTV5_PI_CRED_WRI,
   XMLTV5_PI_CRED_ADAP,
   XMLTV5_PI_CRED_PROD,
   XMLTV5_PI_CRED_PRES,
   XMLTV5_PI_CRED_COMM,
   XMLTV5_PI_CRED_GUEST
};
static const XMLTV_TAG xmltv5_tags_video[] =
{
   XMLTV5_PI_VIDEO_ASPECT
};
static const XMLTV_TAG xmltv5_tags_audio[] =
{
   XMLTV5_PI_AUDIO_STEREO
};
static const XMLTV_TAG xmltv5_tags_prat[] =
{
   XMLTV5_PI_PRAT_VAL
};
static const XMLTV_TAG xmltv5_tags_erat[] =
{
   XMLTV5_PI_ERAT_VAL
};
#endif

// ----------------------------------------------------------------------------
// Tag hierarchy for XMLTV DTD 0.6

static const XMLTV_TAG xmltv6_tags_top[] =
{
   XMLTV6_TV
};
static const XMLTV_TAG xmltv6_tags_tv[] =
{
   XMLTV6_TIMESLOT,
   XMLTV6_CHANNEL,
#ifdef ALL_CONTENT
   XMLTV6_ABOUT
#endif
};
#ifdef ALL_CONTENT
static const XMLTV_TAG xmltv6_tags_about[] =
{
   XMLTV6_SRC_INFO,
   XMLTV6_SRC_DATA,
   XMLTV6_GEN_INFO
};
static const XMLTV_TAG xmltv6_tags_src_info[] =
{
   XMLTV6_LINK,
};
static const XMLTV_TAG xmltv6_tags_gen_info[] =
{
   XMLTV6_LINK,
};
#endif
static const XMLTV_TAG xmltv6_tags_channel[] =
{
   XMLTV6_CHN_DISP_NAME
};
static const XMLTV_TAG xmltv6_tags_timeslot[] =
{
   XMLTV6_TS_PROG,
#ifdef ALL_CONTENT
   XMLTV6_TS_PROG_REF,
   XMLTV6_TS_PROG_UNKNOWN,
   XMLTV6_TS_CODE_TIME,
   XMLTV6_TS_CODE_NUM
#endif
};
static const XMLTV_TAG xmltv6_tags_prog[] =
{
   XMLTV6_PI_TITLE,
   XMLTV6_PI_DESC,
   XMLTV6_PI_CAT,
#ifdef ALL_CONTENT
   XMLTV6_PI_VIDEO,
   XMLTV6_PI_AUDIO,
   XMLTV6_PI_PRAT,
   XMLTV6_PI_ERAT,
   XMLTV6_PI_SUBT,
#endif
   XMLTV6_PI_EPISODE,
#ifdef ALL_CONTENT
   XMLTV6_PI_CREDITS
#endif
};
static const XMLTV_TAG xmltv6_tags_episode[] =
{
   XMLTV6_PI_EPISODE_EP
};
static const XMLTV_TAG xmltv6_tags_episode_ep[] =
{
   XMLTV6_PI_EPISODE_EP_TITLE
};
static const XMLTV_TAG xmltv6_tags_desc[] =
{
   XMLTV6_PI_DESC_P
};
#ifdef ALL_CONTENT
static const XMLTV_TAG xmltv6_tags_credits[] =
{
   XMLTV6_PI_CRED_DIR,
   XMLTV6_PI_CRED_ROLE,
   XMLTV6_PI_CRED_WRITER,
   XMLTV6_PI_CRED_PROD,
   XMLTV6_PI_CRED_EXPR,
   XMLTV6_PI_CRED_PRES,
   XMLTV6_PI_CRED_COMM,
   XMLTV6_PI_CRED_GUEST,
   XMLTV6_PI_CRED_NARR,
   XMLTV6_PI_CRED_COMPANY,
   XMLTV6_PI_CRED_ADAP
};
static const XMLTV_TAG xmltv6_tags_credits_role[] =
{
   XMLTV6_PI_CRED_ROLE_ACTOR,
   XMLTV6_PI_CRED_ROLE_CHAR
};
static const XMLTV_TAG xmltv6_tags_credits_adapted_from[] =
{
   XMLTV6_PI_CRED_ADAP_TYPE,
   XMLTV6_PI_CRED_ADAP_TITLE,
   XMLTV6_PI_CRED_ADAP_WRITER
};
static const XMLTV_TAG xmltv6_tags_video[] =
{
   XMLTV6_PI_VIDEO_ASPECT,
   XMLTV6_PI_VIDEO_QUAL
};
static const XMLTV_TAG xmltv6_tags_audio[] =
{
   XMLTV6_PI_AUDIO_MONO,
   XMLTV6_PI_AUDIO_STEREO,
   XMLTV6_PI_AUDIO_SURR
};
static const XMLTV_TAG xmltv6_tags_subt[] =
{
   XMLTV6_PI_SUBT_TTX,
   XMLTV6_PI_SUBT_OSD
};
static const XMLTV_TAG xmltv6_tags_prat[] =
{
   XMLTV6_PI_PRAT_TEXT
};
static const XMLTV_TAG xmltv6_tags_link[] =
{
   XMLTV6_LINK_TEXT,
   XMLTV6_LINK_BLURB
};
static const XMLTV_TAG xmltv6_tags_link_blurb[] =
{
   XMLTV6_LINK_BLURB_P
};
#endif

// ----------------------------------------------------------------------------
// Tag hierarchy for DTD auto-detection

static const XMLTV_TAG xmltv_auto_detect[] =
{
   XMLTV_DETECT_TV
};
static const XMLTV_TAG xmltv_auto_detect_tv[] =
{
   XMLTV_DETECT_PROG,
   XMLTV_DETECT_ABOUT,
   XMLTV_DETECT_TIMESLOT
};

static void XmltvTags_SetLanguage( XML_STR_BUF * pBuf );
static void XmltvTags_SetVersion5_ByTag( void );
static void XmltvTags_SetVersion5_ByAttr( XML_STR_BUF * pBuf );
static void XmltvTags_SetVersion6_ByTag( void );
static void XmltvTags_SetVersion6_ByAttr( XML_STR_BUF * pBuf );

// ----------------------------------------------------------------------------
// Definition of DTD 0.5 tag attributes & their callback functions
//

typedef struct
{
   const TCHAR*  pName;
   void (* SetAttr) ( XML_STR_BUF * pBuf );
   Bool strip;
} XMLTV_ATTS;

static const XMLTV_ATTS xmltv5_attr_tv[] =
{
   { _T("source-info-name"), Xmltv_AboutSetSourceInfoName },
   { _T("source-info-url"), Xmltv_AboutSetSourceInfoUrl },
   { _T("source-data-url"), Xmltv_AboutSetSourceDataUrl },
   { _T("generator-info-name"), Xmltv_AboutSetGenInfoName },
   { _T("generator-info-url"), Xmltv_AboutSetGenInfoUrl },
   { _T("date"), NULL }
};
static const XMLTV_ATTS xmltv5_attr_channel[] =
{
   { _T("id"), Xmltv_ChannelSetId }
};
static const XMLTV_ATTS xmltv5_attr_disp_name[] =
{
   { _T("lang"), XmltvTags_SetLanguage }
};
static const XMLTV_ATTS xmltv5_attr_icon[] =
{
   { _T("src"), NULL },
   { _T("width"), NULL },
   { _T("height"), NULL }
};
static const XMLTV_ATTS xmltv5_attr_prog[] =
{
   { _T("start"), Xmltv_TsSetStartTime },
   { _T("stop"), Xmltv_TsSetStopTime },
   { _T("channel"), Xmltv_TsSetChannel },
   { _T("pdc-start"), Xmltv_TsCodeTimeSetPdc },
   { _T("vps-start"), Xmltv_TsCodeTimeSetVps },
   { _T("showview"), Xmltv_TsCodeTimeSetSV },
   { _T("videoplus"), Xmltv_TsCodeTimeSetVP },
   { _T("clumpidx"), NULL }
};
static const XMLTV_ATTS xmltv5_attr_pi_subt[] =
{
   { _T("type"), Xmltv_PiSubtitlesSetType }
};
static const XMLTV_ATTS xmltv5_attr_pi_prat[] =
{
   { _T("system"), Xmltv_PiRatingSetSystem }
};
static const XMLTV_ATTS xmltv5_attr_lang_only[] =
{
   { _T("lang"), XmltvTags_SetLanguage }
};

// ----------------------------------------------------------------------------
// Definition of DTD 0.6 tag attributes & their callback functions
//
static const XMLTV_ATTS xmltv6_attr_tv[] =
{
   { _T("from"), NULL },
   { _T("until"), NULL },
   { _T("xml:lang"), XmltvTags_SetLanguage }
};
static const XMLTV_ATTS xmltv6_attr_about[] =
{
   { _T("date"), NULL }
};
static const XMLTV_ATTS xmltv6_attr_src_data[] =
{
   { _T("href"), Xmltv_AboutSetSourceDataUrl }
};
static const XMLTV_ATTS xmltv6_attr_channel[] =
{
   { _T("id"), Xmltv_ChannelSetId }
};
static const XMLTV_ATTS xmltv6_attr_ts[] =
{
   { _T("channel"), Xmltv_TsSetChannel },
   { _T("start"), Xmltv_TsSetStartTime },
   { _T("stop"), Xmltv_TsSetStopTime },
   { _T("conditional"), NULL, TRUE },
   { _T("uncertain"), NULL, TRUE },
   { _T("liveness"), Xmltv_TsSetFeatLive, TRUE },
   { _T("encryption"), Xmltv_TsSetFeatCrypt }
};
static const XMLTV_ATTS xmltv6_attr_ts_code_time[] =
{
   { _T("system"), Xmltv_TsCodeTimeSetSystem },
   { _T("start"), Xmltv_TsCodeTimeSetStart },
   { _T("stop"), NULL }
};
static const XMLTV_ATTS xmltv6_attr_ts_code_num[] =
{
   { _T("system"), Xmltv_TsCodeTimeSetSystem },
   { _T("num"), Xmltv_TsCodeTimeSetStart }
};
static const XMLTV_ATTS xmltv6_attr_prog[] =
{
   { _T("newness"), NULL, TRUE },
   { _T("will-repeat"), NULL, TRUE },
   { _T("xml:lang"), XmltvTags_SetLanguage }
};
static const XMLTV_ATTS xmltv6_attr_title[] =
{
   { _T("original"), NULL, TRUE },
   { _T("xml:lang"), XmltvTags_SetLanguage }
};
static const XMLTV_ATTS xmltv6_attr_desc[] =
{
   { _T("type"), NULL, TRUE },
   { _T("xml:lang"), XmltvTags_SetLanguage }
};
static const XMLTV_ATTS xmltv6_attr_actor[] =
{
   { _T("guest"), NULL, TRUE }
};
static const XMLTV_ATTS xmltv6_attr_adaptor_title[] =
{
   { _T("original"), NULL, TRUE },
   { _T("xml:lang"), XmltvTags_SetLanguage }
};
static const XMLTV_ATTS xmltv6_attr_pi_cat[] =
{
   { _T("type"), Xmltv_PiCatSetType, TRUE },
   { _T("system"), Xmltv_PiCatSetSystem },
   { _T("code"), Xmltv_PiCatSetCode },
   { _T("xml:lang"), XmltvTags_SetLanguage }
};
static const XMLTV_ATTS xmltv6_attr_pi_aspect[] =
{
   { _T("x"), Xmltv_PiVideoAspectSetX },
   { _T("y"), Xmltv_PiVideoAspectSetY }
};
static const XMLTV_ATTS xmltv6_attr_pi_audio[] =
{
   { _T("channel"), NULL }
};
static const XMLTV_ATTS xmltv6_attr_pi_subt_ttx[] =
{
   { _T("page"), Xmltv_PiSubtitlesSetPage }
};
static const XMLTV_ATTS xmltv6_attr_pi_prat[] =
{
   { _T("system"), Xmltv_PiRatingSetSystem }
};
static const XMLTV_ATTS xmltv6_attr_pi_erat[] =
{
   { _T("stars"), Xmltv_PiStarRatingSetValue },
   { _T("out-of"), Xmltv_PiStarRatingSetMax }
};
static const XMLTV_ATTS xmltv6_attr_link[] =
{
   { _T("href"), Xmltv_LinkHrefSet }
};
static const XMLTV_ATTS xmltv6_attr_lang_only[] =
{
   { _T("xml:lang"), XmltvTags_SetLanguage }
};

// ----------------------------------------------------------------------------
// Definition of tag attributes for DTD version auto-detection

static const XMLTV_ATTS xmltv_auto_detect_attr_tv[] =
{
   { _T("source-info-name"), XmltvTags_SetVersion5_ByAttr },  // DTD 0.5
   { _T("source-info-url"), XmltvTags_SetVersion5_ByAttr },
   { _T("source-data-url"), XmltvTags_SetVersion5_ByAttr },
   { _T("generator-info-name"), XmltvTags_SetVersion5_ByAttr },
   { _T("generator-info-url"), XmltvTags_SetVersion5_ByAttr },
   { _T("date"), XmltvTags_SetVersion5_ByAttr },
   { _T("from"), XmltvTags_SetVersion6_ByAttr }, // DTD 0.6
   { _T("until"), XmltvTags_SetVersion6_ByAttr },
   { _T("xml:lang"), XmltvTags_SetVersion6_ByAttr }
};

// ----------------------------------------------------------------------------

typedef enum
{
   XML_NO_PCDATA = 0,
   XML_HAS_PCDATA,
   XML_DISCARD_PCDATA,
   XML_PCDATA_UNSTRIPPED
} XML_PCDATA_DEF;

typedef struct
{
   XMLTV_TAG            tagIdx;
   const TCHAR        * pTagName;
   const XMLTV_TAG    * pChilds;
   uint                 childCount;
   XML_PCDATA_DEF       hasPcData;
   struct {
      void (* TagOpen) ( void );
      void (* TagClose) ( void );
      void (* AddContent) ( XML_STR_BUF * pBuf );
      Bool (* PostAttr) ( void );
   } cb;
   struct {
      const XMLTV_ATTS * pTagAttr;
      uint              attrCount;
   } at;
} XML_TAGDEF;

#define XML_CHILDS(L)           (L), sizeof(L)/sizeof((L)[0])
#define XML_NO_CHILDS           NULL, 0
#define XMLTV_NO_OPEN_CLOSE_CB  { NULL, NULL, NULL, NULL }
#define XMLTV_ATTR(L)           { (L), sizeof(L)/sizeof((L)[0]) }
#define XMLTV_NO_ATTR           { NULL, 0 }

// ----------------------------------------------------------------------------

static const XML_TAGDEF xmltv_tag_def[] =
{
   // DTD 0.5
   { XMLTV5_TOP, _T("*root/dtd-0.5*"), XML_CHILDS(xmltv5_tags_top), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
   { XMLTV5_TV, _T("tv"), XML_CHILDS(xmltv5_tags_tv), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv5_attr_tv),
   },
   { XMLTV5_CHANNEL, _T("channel"), XML_CHILDS(xmltv5_tags_channel), XML_NO_PCDATA,
     { Xmltv_ChannelCreate, Xmltv_ChannelClose, NULL, NULL },
     XMLTV_ATTR(xmltv5_attr_channel),
   },
   { XMLTV5_CHN_DISP_NAME, _T("display-name"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_ChannelAddName, NULL },
     XMLTV_ATTR(xmltv5_attr_disp_name),
   },
#ifdef ALL_CONTENT
   { XMLTV5_CHN_ICON, _T("icon"), XML_NO_CHILDS, XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv5_attr_icon),
   },
   { XMLTV5_CHN_URL, _T("url"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_ChannelAddUrl, NULL },
     XMLTV_NO_ATTR,
   },
#endif
   { XMLTV5_PROG, _T("programme"), XML_CHILDS(xmltv5_tags_programme), XML_NO_PCDATA,
     { Xmltv_TsOpen, Xmltv_TsClose, NULL, NULL /*Xmltv_TsFilter*/ },
     XMLTV_ATTR(xmltv5_attr_prog),
   },
   { XMLTV5_PI_TITLE, _T("title"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiTitleAdd, NULL },
     XMLTV_ATTR(xmltv5_attr_lang_only),
   },
   { XMLTV5_PI_TITLE2, _T("sub-title"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiEpisodeTitleAdd, NULL },
     XMLTV_ATTR(xmltv5_attr_lang_only),
   },
   { XMLTV5_PI_DESC, _T("desc"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { Xmltv_PiDescOpen, Xmltv_PiDescClose, Xmltv_ParagraphAdd, NULL },
     XMLTV_ATTR(xmltv5_attr_lang_only),
   },
#ifdef ALL_CONTENT
   { XMLTV5_PI_CREDITS, _T("credits"), XML_CHILDS(xmltv5_tags_credits), XML_NO_PCDATA,
     { Xmltv_PiCreditsOpen, Xmltv_PiCreditsClose, NULL, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_CRED_DIR, _T("director"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddDirector, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_CRED_ACT, _T("actor"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddActor, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_CRED_WRI, _T("writer"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddWriter, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_CRED_ADAP, _T("adapter"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddAdapter, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_CRED_PROD, _T("producer"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddProducer, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_CRED_PRES, _T("presenter"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddPresenter, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_CRED_COMM, _T("commentator"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddCommentator, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_CRED_GUEST, _T("guest"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddGuest, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_DATE, _T("date"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiDateAdd, NULL },
     XMLTV_NO_ATTR,
   },
#endif
   { XMLTV5_PI_CAT, _T("category"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { Xmltv_PiCatOpen, Xmltv_PiCatClose, Xmltv_PiCatAddText, NULL },
     XMLTV_ATTR(xmltv5_attr_lang_only),
   },
#ifdef ALL_CONTENT
   { XMLTV5_PI_VIDEO, _T("video"), XML_CHILDS(xmltv5_tags_video), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_VIDEO_ASPECT, _T("aspect"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { Xmltv_PiVideoAspectOpen, Xmltv_PiVideoAspectClose, Xmltv_PiVideoAspectAddXY, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_AUDIO, _T("audio"), XML_CHILDS(xmltv5_tags_audio), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_AUDIO_STEREO, _T("stereo"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiAudioStereoAdd, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_SUBT, _T("subtitles"), XML_NO_CHILDS, XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv5_attr_pi_subt),
   },
   { XMLTV5_PI_PRAT, _T("rating"), XML_CHILDS(xmltv5_tags_prat), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv5_attr_pi_prat),
   },
   { XMLTV5_PI_PRAT_VAL, _T("value"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiRatingAddText, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_ERAT, _T("star-rating"), XML_CHILDS(xmltv5_tags_erat), XML_NO_PCDATA,
     { Xmltv_PiStarRatingOpen, Xmltv_PiStarRatingClose, NULL, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV5_PI_ERAT_VAL, _T("value"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiStarRatingAddText, NULL },
     XMLTV_NO_ATTR,
   },
#endif

   // DTD 0.6
   { XMLTV6_TOP, _T("*root/dtd-0.6*"), XML_CHILDS(xmltv6_tags_top), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
   { XMLTV6_TV, _T("tv"), XML_CHILDS(xmltv6_tags_tv), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv6_attr_tv),
   },
#ifdef ALL_CONTENT
   { XMLTV6_ABOUT, _T("about"), XML_CHILDS(xmltv6_tags_about), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv6_attr_about),
   },
   { XMLTV6_SRC_INFO, _T("source-info"), XML_CHILDS(xmltv6_tags_src_info), XML_NO_PCDATA,
     { Xmltv_SourceInfoOpen, Xmltv_SourceInfoClose, NULL, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_SRC_DATA, _T("source-data"), XML_NO_CHILDS, XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv6_attr_src_data),
   },
   { XMLTV6_GEN_INFO, _T("generator-info"), XML_CHILDS(xmltv6_tags_gen_info), XML_NO_PCDATA,
     { Xmltv_GenInfoOpen, Xmltv_GenInfoClose, NULL, NULL },
     XMLTV_NO_ATTR,
   },
#endif
   { XMLTV6_CHANNEL, _T("channel"), XML_CHILDS(xmltv6_tags_channel), XML_NO_PCDATA,
     { Xmltv_ChannelCreate, Xmltv_ChannelClose, NULL, NULL },
     XMLTV_ATTR(xmltv6_attr_channel),
   },
   { XMLTV6_CHN_DISP_NAME, _T("display-name"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_ChannelAddName, NULL },
     XMLTV_ATTR(xmltv6_attr_lang_only),
   },
   { XMLTV6_TIMESLOT, _T("timeslot"), XML_CHILDS(xmltv6_tags_timeslot), XML_NO_PCDATA,
     { Xmltv_TsOpen, Xmltv_TsClose, NULL, NULL /*Xmltv_TsFilter*/ },
     XMLTV_ATTR(xmltv6_attr_ts),
   },
#ifdef ALL_CONTENT
   { XMLTV6_TS_CODE_TIME, _T("code-time"), XML_NO_CHILDS, XML_NO_PCDATA,
     { Xmltv_TsCodeTimeOpen, Xmltv_TsCodeTimeClose, NULL, NULL },
     XMLTV_ATTR(xmltv6_attr_ts_code_time),
   },
   { XMLTV6_TS_CODE_NUM, _T("code-num"), XML_NO_CHILDS, XML_NO_PCDATA,
     { Xmltv_TsCodeTimeOpen, Xmltv_TsCodeTimeClose, NULL, NULL },
     XMLTV_ATTR(xmltv6_attr_ts_code_num),
   },
   { XMLTV6_TS_PROG_REF, _T("programme-ref"), XML_NO_CHILDS, XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
   { XMLTV6_TS_PROG_UNKNOWN, _T("unknown"), XML_NO_CHILDS, XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
#endif
   { XMLTV6_TS_PROG, _T("programme"), XML_CHILDS(xmltv6_tags_prog), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv6_attr_prog),
   },
   { XMLTV6_PI_TITLE, _T("title"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiTitleAdd, NULL },
     XMLTV_ATTR(xmltv6_attr_title),
   },
   { XMLTV6_PI_DESC, _T("desc"), XML_CHILDS(xmltv6_tags_desc), XML_NO_PCDATA,
     { Xmltv_PiDescOpen, Xmltv_PiDescClose, NULL, NULL },
     XMLTV_ATTR(xmltv6_attr_desc),
   },
   { XMLTV6_PI_DESC_P, _T("p"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { Xmltv_ParagraphCreate, Xmltv_ParagraphClose, Xmltv_ParagraphAdd, NULL },
     XMLTV_NO_ATTR,
   },
#ifdef ALL_CONTENT
   { XMLTV6_PI_CREDITS, _T("credits"), XML_CHILDS(xmltv6_tags_credits), XML_NO_PCDATA,
     { Xmltv_PiCreditsOpen, Xmltv_PiCreditsClose, NULL, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_DIR, _T("director"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddDirector, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_ROLE, _T("role"), XML_CHILDS(xmltv6_tags_credits_role), XML_DISCARD_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB, /* TODO: open/close: combine actor&role */
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_ROLE_ACTOR, _T("actor"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddActor, NULL },
     XMLTV_ATTR(xmltv6_attr_actor),
   },
   { XMLTV6_PI_CRED_ROLE_CHAR, _T("character"), XML_NO_CHILDS, XML_DISCARD_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB, /* TODO */
     XMLTV_ATTR(xmltv6_attr_lang_only),
   },
   { XMLTV6_PI_CRED_WRITER, _T("writer"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddWriter, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_PROD, _T("producer"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddProducer, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_EXPR, _T("executive-producer"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddExecProducer, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_PRES, _T("presenter"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddPresenter, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_COMM, _T("commentator"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddCommentator, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_GUEST, _T("guest"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddGuest, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_NARR, _T("narrator"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddNarrator, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_COMPANY, _T("company"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiCreditsAddCompany, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_ADAP, _T("adapted-from"), XML_CHILDS(xmltv6_tags_credits_adapted_from), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB /*TODO*/,
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_CRED_ADAP_TYPE, _T("type"), XML_NO_CHILDS, XML_DISCARD_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB /*TODO*/,
     XMLTV_ATTR(xmltv6_attr_lang_only),
   },
   { XMLTV6_PI_CRED_ADAP_TITLE, _T("title"), XML_NO_CHILDS, XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB /*TODO*/,
     XMLTV_ATTR(xmltv6_attr_adaptor_title),
   },
   { XMLTV6_PI_CRED_ADAP_WRITER, _T("writer"), XML_NO_CHILDS, XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB /*TODO*/,
     XMLTV_NO_ATTR,
   },
#endif
   { XMLTV6_PI_CAT, _T("category"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { Xmltv_PiCatOpen, Xmltv_PiCatClose, Xmltv_PiCatAddText, NULL },
     XMLTV_ATTR(xmltv6_attr_pi_cat),
   },
   { XMLTV6_PI_EPISODE, _T("episode-details"), XML_CHILDS(xmltv6_tags_episode), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_EPISODE_EP, _T("episode"), XML_CHILDS(xmltv6_tags_episode_ep), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_EPISODE_EP_TITLE, _T("title"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiEpisodeTitleAdd, NULL },
     XMLTV_NO_ATTR,
   },
#ifdef ALL_CONTENT
   { XMLTV6_PI_VIDEO, _T("video"), XML_CHILDS(xmltv6_tags_video), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_VIDEO_ASPECT, _T("aspect"), XML_NO_CHILDS, XML_NO_PCDATA,
     { Xmltv_PiVideoAspectOpen, Xmltv_PiVideoAspectClose, NULL, NULL },
     XMLTV_ATTR(xmltv6_attr_pi_aspect),
   },
   { XMLTV6_PI_VIDEO_QUAL, _T("quality"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiVideoQualityAdd, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_AUDIO, _T("audio"), XML_CHILDS(xmltv6_tags_audio), XML_NO_PCDATA,
     { Xmltv_PiAudioOpen, Xmltv_PiAudioClose, NULL, NULL },
     XMLTV_ATTR(xmltv6_attr_pi_audio),
   },
   { XMLTV6_PI_AUDIO_MONO, _T("mono"), XML_NO_CHILDS, XML_NO_PCDATA,
     { Xmltv_PiAudioMonoOpen, NULL, NULL, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_AUDIO_STEREO, _T("stereo"), XML_NO_CHILDS, XML_NO_PCDATA,
     { Xmltv_PiAudioStereoOpen, NULL, NULL, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_AUDIO_SURR, _T("surround"), XML_NO_CHILDS, XML_NO_PCDATA,
     { Xmltv_PiAudioSurrOpen, NULL, NULL, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_SUBT, _T("subtitles"), XML_CHILDS(xmltv6_tags_subt), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_SUBT_TTX, _T("teletext"), XML_NO_CHILDS, XML_NO_PCDATA,
     { Xmltv_PiSubtitlesTtx, NULL, NULL, NULL },
     XMLTV_ATTR(xmltv6_attr_pi_subt_ttx),
   },
   { XMLTV6_PI_SUBT_OSD, _T("superimposed"), XML_NO_CHILDS, XML_NO_PCDATA,
     { Xmltv_PiSubtitlesOsd, NULL, NULL, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_PRAT, _T("classification"), XML_CHILDS(xmltv6_tags_prat), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv6_attr_pi_prat),
   },
   { XMLTV6_PI_PRAT_TEXT, _T("text"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_PiRatingAddText, NULL },
     XMLTV_NO_ATTR,
   },
   { XMLTV6_PI_ERAT, _T("star-rating"), XML_NO_CHILDS, XML_NO_PCDATA,
     { Xmltv_PiStarRatingOpen, Xmltv_PiStarRatingClose, NULL, NULL },
     XMLTV_ATTR(xmltv6_attr_pi_erat),
   },
   { XMLTV6_LINK, _T("link"), XML_CHILDS(xmltv6_tags_link), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv6_attr_link),
   },
   { XMLTV6_LINK_TEXT, _T("text"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_LinkAddText, NULL },
     XMLTV_ATTR(xmltv6_attr_lang_only),
   },
   { XMLTV6_LINK_BLURB, _T("blurb"), XML_CHILDS(xmltv6_tags_link_blurb), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv6_attr_lang_only),
   },
   { XMLTV6_LINK_BLURB_P, _T("p"), XML_NO_CHILDS, XML_HAS_PCDATA,
     { NULL, NULL, Xmltv_LinkBlurbAddText, NULL },
     XMLTV_NO_ATTR,
   },
#endif

   // DTD auto-detection
   { XMLTV_DETECT, _T("*auto-detect*"), XML_CHILDS(xmltv_auto_detect), XML_DISCARD_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR
   },
   { XMLTV_DETECT_TV, _T("tv"), XML_CHILDS(xmltv_auto_detect_tv), XML_NO_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_ATTR(xmltv_auto_detect_attr_tv)
   },
   { XMLTV_DETECT_PROG, _T("programme"), XML_NO_CHILDS, XML_NO_PCDATA,
     { XmltvTags_SetVersion5_ByTag, NULL, NULL},
     XMLTV_NO_ATTR
   },
   { XMLTV_DETECT_ABOUT, _T("about"), XML_NO_CHILDS, XML_NO_PCDATA,
     { XmltvTags_SetVersion6_ByTag, NULL, NULL},
     XMLTV_NO_ATTR
   },
   { XMLTV_DETECT_TIMESLOT, _T("timeslot"), XML_NO_CHILDS, XML_NO_PCDATA,
     { XmltvTags_SetVersion6_ByTag, NULL, NULL},
     XMLTV_NO_ATTR
   },
   { XMLTV_SKIP, _T("*skip*"), XML_NO_CHILDS, XML_DISCARD_PCDATA,
     XMLTV_NO_OPEN_CLOSE_CB,
     XMLTV_NO_ATTR
   }
};

// ----------------------------------------------------------------------------
// Check consistency of the above table
//
static void XmltvScan_CheckTablesConsistency( void )
{
#ifdef DEBUG_SWITCH
   uint  tagIdx;
   uint  childIdx;
   Bool  refTable[XMLTV_TAG_COUNT];

   assert(sizeof(xmltv_tag_def) == sizeof(xmltv_tag_def[0]) * XMLTV_TAG_COUNT);
   memset(refTable, 0, sizeof(refTable));

   for (tagIdx = 0; tagIdx < XMLTV_TAG_COUNT; tagIdx++)
   {
      assert(xmltv_tag_def[tagIdx].tagIdx == tagIdx);
      assert(xmltv_tag_def[tagIdx].pTagName != NULL);

      // check child references
      assert((xmltv_tag_def[tagIdx].childCount == 0) || (xmltv_tag_def[tagIdx].pChilds != NULL));
      if ((xmltv_tag_def[tagIdx].childCount == 0) && (xmltv_tag_def[tagIdx].pChilds != NULL))
         debug2(_T("CheckTables: Warning state %d (tag '%s') with 0 childs but non-NULL child table"), tagIdx, xmltv_tag_def[tagIdx].pTagName);
      for (childIdx = 0; childIdx < xmltv_tag_def[tagIdx].childCount; childIdx++)
      {
         assert(xmltv_tag_def[tagIdx].pChilds[childIdx] < XMLTV_TAG_COUNT);
         refTable[ xmltv_tag_def[tagIdx].pChilds[childIdx] ] += 1;
      }
      if ( (xmltv_tag_def[tagIdx].hasPcData != XML_NO_PCDATA) &&
           (xmltv_tag_def[tagIdx].hasPcData != XML_DISCARD_PCDATA) &&
           (xmltv_tag_def[tagIdx].cb.AddContent == NULL) )
         debug2(_T("CheckTables: Warning state %d (tag '%s') has no callback for PCDATA"), tagIdx, xmltv_tag_def[tagIdx].pTagName);
      else if ( (xmltv_tag_def[tagIdx].hasPcData == XML_NO_PCDATA) &&
           (xmltv_tag_def[tagIdx].cb.AddContent != NULL) )
         debug2(_T("CheckTables: Warning state %d (tag '%s') PCDATA callback is disabled"), tagIdx, xmltv_tag_def[tagIdx].pTagName);

      // check attributes
      assert((xmltv_tag_def[tagIdx].at.attrCount == 0) || (xmltv_tag_def[tagIdx].at.pTagAttr != NULL));
      if ((xmltv_tag_def[tagIdx].at.attrCount == 0) && (xmltv_tag_def[tagIdx].at.pTagAttr != NULL))
         debug2(_T("CheckTables: Warning state %d (tag '%s') with 0 attributes but non-NULL attribute table"), tagIdx, xmltv_tag_def[tagIdx].pTagName);
   }
   for (tagIdx = 0; tagIdx < XMLTV_TAG_COUNT; tagIdx++)
   {
      if ( (refTable[tagIdx] == 0) &&
           (tagIdx != XMLTV5_TOP) && (tagIdx != XMLTV6_TOP) &&
           (tagIdx != XMLTV_DETECT) && (tagIdx != XMLTV_SKIP) )
         debug2(_T("CheckTables: Warning state %d (tag '%s') never referenced"), tagIdx, xmltv_tag_def[tagIdx].pTagName);
   }
#endif // DEBUG_SWITCH
}

// ----------------------------------------------------------------------------
// Internal parser stack
//
#define XML_STACK_MAX_DEPTH   20
#define XML_SKIP_ATTRIB       (~0)
#define XML_MAX_SYNTAX_ERR    10

typedef struct
{
   XMLTV_TAG            tagStack[XML_STACK_MAX_DEPTH];
   XML_LANG_CODE        lang[XML_STACK_MAX_DEPTH];
   uint                 stackIdx;
   uint                 syntaxError;
   Bool                 earlyStop;
   uint                 xmlAttrToken;
   XMLTV_DTD_VERSION    dtd;
   XML_ENCODING         encoding;
   XMLTV_DETECTION      detected;
} XML_PARSER_STATE;

static XML_PARSER_STATE xps;

// ----------------------------------------------------------------------------
// Parse language code
// - this is a callback function for the _T("lang") and _T("xml:lang") attributes
// - implemented here to support inheritage to child elements
// - expects language codes in RFC 1766 style; only the language itself is
//   evaluated, possible dialect or country tags are ignored
//
static void XmltvTags_SetLanguage( XML_STR_BUF * pBuf )
{
   TCHAR*  pStr;
   uint  idx;
   XML_LANG_CODE code;

   pStr = XML_STR_BUF_GET_STR(*pBuf);
   code = 0;

   for (idx = 0; idx < 4; idx++)
   {
      if ((*pStr >= 'a') && (*pStr <= 'z'))
         code = (code << 8) | (*pStr - 'a' + 'A');
      else if ((*pStr >= 'A') && (*pStr <= 'Z'))
         code = (code << 8) | *pStr;
      else
         break;

      pStr++;
   }

   xps.lang[xps.stackIdx] = code;
}

// ----------------------------------------------------------------------------
// Query the language of the current element's content
//
XML_LANG_CODE XmltvTags_GetLanguage( void )
{
   return xps.lang[xps.stackIdx];
}

// ----------------------------------------------------------------------------
// Push new tag on parser stack
//
void XmltvTags_Open( const TCHAR*  pTagName )
{
   const XMLTV_TAG  * pChild;
   XMLTV_TAG  state;
   uint  childIdx;
   Bool  found;

   if (xps.stackIdx + 1 < XML_STACK_MAX_DEPTH)
   {
      state = xps.tagStack[xps.stackIdx];
      found = FALSE;

      pChild = xmltv_tag_def[state].pChilds;
      for (childIdx = 0; childIdx < xmltv_tag_def[state].childCount; childIdx++)
      {
         if (_tcscmp(xmltv_tag_def[*pChild].pTagName, pTagName) == 0)
         {
            found = TRUE;
            break;
         }
         pChild++;
      }

      if (found)
      {
         dprintf2(_T("XmltvTag-Open: <%s> inside <%s>\n"), pTagName, xmltv_tag_def[state].pTagName);
         xps.stackIdx += 1;
         xps.tagStack[xps.stackIdx] = *pChild;
         xps.xmlAttrToken = XML_SKIP_ATTRIB;
         xps.syntaxError = 0;

         if (xps.dtd == XMLTV_DTD_6)
            xps.lang[xps.stackIdx] = xps.lang[xps.stackIdx - 1];
         else
            xps.lang[xps.stackIdx] = XML_LANG_UNKNOWN;

         state = *pChild;
         if (xmltv_tag_def[state].cb.TagOpen != NULL)
         {
            xmltv_tag_def[state].cb.TagOpen();
         }
      }
      else
      {
         dprintf2(_T("XmltvTag-Open: ignore <%s> inside <%s>\n"), pTagName, xmltv_tag_def[state].pTagName);
         if ((xps.stackIdx == 0) && (xps.syntaxError == 0))
         {
            Xmltv_SyntaxError(_T("XMLTV toplevel tag is not <tv> - probably not an XMLTV document"), pTagName);
            xps.detected |= XMLTV_DETECTED_NOT_TV;
         }
         xps.stackIdx += 1;
         xps.tagStack[xps.stackIdx] = XMLTV_SKIP;
      }
   }
   else
   {  // abort: stack overflow (the current tag is discarded, which will cause more errors)
      Xmltv_SyntaxError(_T("XMLTV parser fatal error: tags nested too deply"), pTagName);
   }
}

// ----------------------------------------------------------------------------
// Pop tag from parser stack
// - tag name can be NULL for empty tags (e.g. <stereo />)
// - returns FALSE if the document top-level tag is closed
//
Bool XmltvTags_Close( const TCHAR*  pTagName )
{
   XMLTV_TAG  state;

   if (xps.stackIdx > 0)
   {
      state = xps.tagStack[xps.stackIdx];

      if (state != XMLTV_SKIP)
      {
         if ( (pTagName == NULL) ||
              (_tcscmp(xmltv_tag_def[state].pTagName, pTagName) == 0) )
         {
            if (pTagName != NULL)
            {
               dprintf2(_T("XmltvTag-Close: </%s>, new stack depth %d\n"), pTagName, xps.stackIdx - 1);
               xps.syntaxError = 0;
            }

            if (xmltv_tag_def[state].cb.TagClose != NULL)
            {
               xmltv_tag_def[state].cb.TagClose();
            }

            xps.stackIdx -= 1;
            state = xps.tagStack[xps.stackIdx];
         }
         else
         {
            Xmltv_SyntaxError(_T("XMLTV parser error: mismatching closing tag"), pTagName);
         }
      }
      else
      {
         // FIXME should still compare tag names
         dprintf1(_T("XmltvTag-Close: popping SKIP, new stack depth %d\n"), xps.stackIdx - 1);
         xps.stackIdx -= 1;
      }
   }
   else
   {
      Xmltv_SyntaxError(_T("XMLTV parser error: unexpected closing tag"), pTagName);
   }

   return (xps.stackIdx > 0);
}

// ----------------------------------------------------------------------------
// Processe PCDATA inbetween open and close tag
//
void XmltvTags_Data( XML_STR_BUF * pBuf )
{
   XMLTV_TAG  state;

   dprintf1(_T("XmltvTag-Data: '%s'\n"), XML_STR_BUF_GET_STR(*pBuf));

   state = xps.tagStack[xps.stackIdx];

   if ( (xmltv_tag_def[state].hasPcData != XML_NO_PCDATA) &&
        (xmltv_tag_def[state].hasPcData != XML_DISCARD_PCDATA) )
   {
      if (xmltv_tag_def[state].hasPcData != XML_PCDATA_UNSTRIPPED)
      {
         XmlCdata_TrimWhitespace(pBuf);
      }

      XML_STR_BUF_SET_LANG(*pBuf, xps.lang[xps.stackIdx]);

      if (xmltv_tag_def[state].cb.AddContent != NULL)
      {
         xmltv_tag_def[state].cb.AddContent(pBuf);
      }
   }
}

// ----------------------------------------------------------------------------
// Notify that opening tag is closed, i.e. all attributes processed
// - when the callback returns FALSE, it's content and child elements are skipped;
//   this is an optimization for skipping tags which fall outside certain criteria
// - note this function is currently not called for empty tags, i.e. when the
//   has no childs nor content
//
Bool XmltvTags_AttribsComplete( void )
{
   XMLTV_TAG  state;
   Bool result = TRUE;

   state = xps.tagStack[xps.stackIdx];

   if (xmltv_tag_def[state].cb.PostAttr != NULL)
   {
      if ( xmltv_tag_def[state].cb.PostAttr() == FALSE )
      {
         // change into the _T("skip") state, as if the current tag was unknown
         xps.tagStack[xps.stackIdx] = XMLTV_SKIP;
         result = FALSE;
      }
   }
   return result;
}

// ----------------------------------------------------------------------------
// Identify a tag's attribute by its name
//
void XmltvTags_AttribIdentify( const TCHAR*  pName )
{
   const XMLTV_ATTS * pAttrib;
   XMLTV_TAG  state;
   uint  attrIdx;
   Bool  found;

   state = xps.tagStack[xps.stackIdx];
   pAttrib = xmltv_tag_def[state].at.pTagAttr;
   found = FALSE;

   if (pAttrib != NULL)
   {
      for (attrIdx = 0; attrIdx < xmltv_tag_def[state].at.attrCount; attrIdx++)
      {
         if (_tcscmp(pAttrib->pName, pName) == 0)
         {
            dprintf3(_T("XmltvTag-Attrib: '%s' (%d) for '%s'\n"), pName, attrIdx, xmltv_tag_def[state].pTagName);
            found = TRUE;
            break;
         }
         pAttrib++;
      }

      if (found == FALSE)
      {
         dprintf2(_T("XmltvTag-AttribIdentify: ignore '%s' inside '%s'\n"), pName, xmltv_tag_def[state].pTagName);
         attrIdx = XML_SKIP_ATTRIB;
      }
   }
   else
      attrIdx = XML_SKIP_ATTRIB;

   xps.xmlAttrToken = attrIdx;
}

// ----------------------------------------------------------------------------
// Process an attribute's data
// - data is already normalized by scanner according to XML 1.0 ch. 3.3.3
//   i.e. entity references are resolved and newline chars replaced by blank
// - TODO: for non-CDATA remove leading space and reduce multiple whitespace into one
//
void XmltvTags_AttribData( XML_STR_BUF * pBuf )
{
   const XMLTV_ATTS * pAttrib;
   XMLTV_TAG  state;

   state = xps.tagStack[xps.stackIdx];
   if (xps.xmlAttrToken < xmltv_tag_def[state].at.attrCount)
   {
      pAttrib = xmltv_tag_def[state].at.pTagAttr + xps.xmlAttrToken;

      dprintf2(_T("XmltvTag-AttribData: assign value '%s' to attrib '%s'\n"), XML_STR_BUF_GET_STR(*pBuf), pAttrib->pName);

      XML_STR_BUF_SET_LANG(*pBuf, XML_LANG_UNKNOWN);

      if (pAttrib->SetAttr != NULL)
      {
         pAttrib->SetAttr(pBuf);
      }

      // invalidate attribute: to be used only once
      xps.xmlAttrToken = XML_SKIP_ATTRIB;
   }
   else
      dprintf1(_T("XmltvTag-AttribData: discard attrib value '%s'\n"), XML_STR_BUF_GET_STR(*pBuf));
}

// ----------------------------------------------------------------------------
// Scanner callback when an unsupported encoding is encountered
//
void XmltvTags_ScanUnsupEncoding( const TCHAR*  pName )
{
   Xmltv_SyntaxError(_T("Unsupported encoding"), pName);
   xps.detected |= XMLTV_DETECTED_UNSUP_ENC;
}

// ----------------------------------------------------------------------------
// Process XML header <?xml version=_T("...") encoding=_T("...")?>
//
void XmltvTags_Encoding( const TCHAR*  pName )
{
   Bool result;

   if ( (strncasecmp(pName, _T("iso8859"), 7) == 0) ||
        (strncasecmp(pName, _T("iso-8859"), 8) == 0) ||
        (strncasecmp(pName, _T("iso_8859"), 8) == 0) ||
        (strncasecmp(pName, _T("8859"), 4) == 0) ||
        (strncasecmp(pName, _T("ascii"), 5) == 0) ||
        (strncasecmp(pName, _T("us-ascii"), 8) == 0) )
   {
      xps.encoding = XML_ENC_ISO8859;
   }
   else if ( (strcasecmp(pName, _T("utf8")) == 0) ||
             (strcasecmp(pName, _T("utf-8")) == 0) )
   {
      xps.encoding = XML_ENC_UTF8;
   }
   else if ( (strcasecmp(pName, _T("utf16")) == 0) ||
             (strcasecmp(pName, _T("utf-16")) == 0) ||
             (strcasecmp(pName, _T("utf16be")) == 0) ||
             (strcasecmp(pName, _T("utf-16be")) == 0) )
   {
      xps.encoding = XML_ENC_UTF16BE;
   }
   else if ( (strcasecmp(pName, _T("utf16le")) == 0) ||
             (strcasecmp(pName, _T("utf-16le")) == 0) )
   {
      xps.encoding = XML_ENC_UTF16LE;
   }

   xps.detected |= XMLTV_DETECTED_XML;

   if (xps.encoding != XML_ENC_UNKNOWN)
   {
      result = XmlScan_SetEncoding(xps.encoding);
      if (result == FALSE)
      {
         Xmltv_SyntaxError(_T("Encoding in <?xml?> mismatches auto-detected encoding"), pName);
         xps.detected |= XMLTV_DETECTED_UNSUP_XMLENC;
      }
   }
   else
   {
      Xmltv_SyntaxError(_T("Unsupported encoding in <?xml?>"), pName);
      xps.detected |= XMLTV_DETECTED_UNSUP_XMLENC;
      result = FALSE;
   }
}

void XmltvTags_XmlVersion( const TCHAR*  pVersion )
{
   if (_tcscmp(pVersion, _T("1.0")) != 0)
   {
      Xmltv_SyntaxError(_T("Incompatible XML version"), pVersion);
   }
   else
      xps.detected |= XMLTV_DETECTED_XML;
}

// ----------------------------------------------------------------------------
// Process <!DOCTYPE...> declaration
//
void XmltvTags_DocType( const TCHAR*  pName )
{
   if (_tcscmp(pName, _T("tv")) != 0)
   {
      Xmltv_SyntaxError(_T("XML DOCTYPE name mismatch (probably not an XMLTV document)"), pName);
      xps.detected |= XMLTV_DETECTED_NOT_TV;
   }
   xps.detected |= XMLTV_DETECTED_DOCTYPE;
}

// ----------------------------------------------------------------------------
// Prolog callbacks - only used during verification (see xml_verify.c)
//
void XmltvTags_PiTarget( const TCHAR*  pName )
{
}

void XmltvTags_PiContent( const TCHAR*  pValue )
{
}

void XmltvTags_DocIntDtdClose( void )
{
}

void XmltvTags_Notation( int stepIdx, const TCHAR*  pValue )
{
}

void XmltvTags_CheckName( const TCHAR*  pStr )
{
}

void XmltvTags_CheckCharset( const TCHAR*  pStr )
{
}

void XmltvTags_CheckNmtoken( const TCHAR*  pStr )
{
}

void XmltvTags_CheckSystemLiteral( const TCHAR*  pStr )
{
}

// ----------------------------------------------------------------------------
// Return a human-redable error message
// - the _T("detection") code is a bit-field which is returned by the parser after
//   reading (or attempting to read) the first tags of a document during XMLTV
//   DTD version auto-detection
//
const TCHAR*  XmltvTags_TranslateErrorCode( XMLTV_DETECTION detection )
{
   const TCHAR*  pMsg;

   if ((detection == 0) || (detection == XMLTV_DETECTED_SYNTAX))
   {
      pMsg = _T("does not appear to be an XML document (no markup found)");
   }
   else if (detection & XMLTV_DETECTED_UNSUP_ENC)
   {
      pMsg = _T("cannot be read because the character encoding is not supported");
   }
   else if (detection & XMLTV_DETECTED_UNSUP_XMLENC)
   {
      pMsg = _T("is an XML document with an unsupported character encoding");
   }
   else if (detection & XMLTV_DETECTED_NOT_TV)
   {
      pMsg = _T("is an XML document, but appearently not XMLTV (doctype mismatch)");
   }
   else if (detection & XMLTV_DETECTED_SYNTAX)
   {
      pMsg = _T("is not an XMLTV document or not valid (parse errors)");
   }
   else
   {
      pMsg = _T("is not an XMLTV document or incomplete (no content found)");
   }
   return pMsg;
}

// ----------------------------------------------------------------------------
// Notification about syntax error
//
void Xmltv_SyntaxError( const TCHAR*  pMsg, const TCHAR*  pStr )
{
   debug2(_T("Xmltv-SyntaxError: %s: '%.200s'"), pMsg, pStr);
   xps.detected |= XMLTV_DETECTED_SYNTAX;
   xps.syntaxError += 1;

   if (xps.syntaxError > XML_MAX_SYNTAX_ERR)
   {
      debug0(_T("Xmltv-SyntaxError: too many errors - aborting"));
      xps.earlyStop = TRUE;
      XmlScan_Stop();
   }
}

// ----------------------------------------------------------------------------
// Function called by scanner upon I/O or malloc failures
// - ideally this function should raise an exception and not return, else
//   the scanner might run into a NULL pointer dereference
//
void Xmltv_ScanFatalError( const TCHAR*  pMsg )
{
   debug1(_T("Xmltv-ScanFatalError: %s"), pMsg);
}

// ----------------------------------------------------------------------------
// Callback functions for DTD version auto-detection mode
// - in this mode the XML file is parsed (and all data discarded) until a tag
//   or attribute is found which only exists in one of the DTD versions
// - main difficutly is to detect the version also for near-empty files, e.g.
//   a file without programme data (or even with 0 channels, which is legal
//   for DTD 0.5)
//
static void XmltvTags_SetVersion5_ByTag( void )
{
   dprintf0(_T("XmltvTags-SetVersion5: DTD version 5 auto-detected\n"));
   xps.dtd = XMLTV_DTD_5;
   xps.earlyStop = TRUE;

   // stop the scanner, i.e. skip the rest of the input file
   XmlScan_Stop();
}

static void XmltvTags_SetVersion5_ByAttr( XML_STR_BUF * pBuf )
{
   XmltvTags_SetVersion5_ByTag();
}

static void XmltvTags_SetVersion6_ByTag( void )
{
   dprintf0(_T("XmltvTags-SetVersion6: DTD version 6 auto-detected\n"));
   xps.dtd = XMLTV_DTD_6;
   xps.earlyStop = TRUE;
   XmlScan_Stop();
}

static void XmltvTags_SetVersion6_ByAttr( XML_STR_BUF * pBuf )
{
   XmltvTags_SetVersion6_ByTag();
}

// ----------------------------------------------------------------------------
// Function called by database front-end to discard the rest of input
// - for example, used in _T("preview") mode when only source info and channel table
//   are read
//
void XmlTags_ScanStop( void )
{
   xps.earlyStop = TRUE;
   XmlScan_Stop();
}

// ----------------------------------------------------------------------------
// Query version after a run in DTD auto-detection mode
// - optionally, an additional _T("detection code") is returned: in case the XMLTV
//   version auto-detection fails this can be used to check if the file looks
//   like XML at all to produce an appropriate error message
//
XMLTV_DTD_VERSION XmltvTags_QueryVersion( XMLTV_DETECTION * pXmlDetected )
{
   if (pXmlDetected != NULL)
   {
      *pXmlDetected = xps.detected;
   }
   return xps.dtd;
}

// ----------------------------------------------------------------------------
// Initialize and start the parser and scanner
//
void XmltvTags_StartScan( FILE * fp, XMLTV_DTD_VERSION dtdVersion )
{
   dprintf1(_T("XmltvTags-StartScan: DVD version %d\n"), dtdVersion);

   XmltvScan_CheckTablesConsistency();

   XmlScan_Init();

   // push initial state on the element stack
   if (dtdVersion == XMLTV_DTD_5)
      xps.tagStack[0] = XMLTV5_TOP;
   else if (dtdVersion == XMLTV_DTD_6)
      xps.tagStack[0] = XMLTV6_TOP;
   else
      xps.tagStack[0] = XMLTV_DETECT;
   xps.stackIdx = 0;
   xps.dtd = dtdVersion;
   xps.detected = 0;
   xps.encoding = XML_ENC_UNKNOWN;

   yyin = fp;
   //yylex();  // called by yacc

   yyparse();

   // check if all tags were closed, i.e. if the stack is empty
   if ((xps.stackIdx > 0) && (xps.earlyStop == FALSE))
   {
      XMLTV_TAG  state;
      state = xps.tagStack[xps.stackIdx];
      debug1(_T("XmltvTag-StartScan: at end-of-file: tag not closed: '%s'"), xmltv_tag_def[state].pTagName);
      xps.syntaxError = TRUE;
   }
   XmlScan_Destroy();
}

