/*
 * Copyright (c) 2006 Mode2 inc.
 * Copyright (c) 2007 - 2008 BROADTECH INC.
 * Copyright (c) 2006 - 2008 Kazuaki ODA
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the company nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: ctr_parser.c 540 2008-08-30 18:44:20Z oda $
 */

#include "httpd.h"
#include "http_log.h"
#include "apr_lib.h"
#include "apr_strings.h"
#include "ctr_parser.h"
#include "ctr_util.h"

typedef enum _ctr_lex_mode ctr_lex_mode_t;
typedef struct _ctr_parse_info ctr_parse_info_t;
typedef enum _ctr_parse_stat ctr_parse_stat_t;
typedef struct _ctr_parse_elm ctr_parse_elm_t;
typedef struct _ctr_html_node ctr_html_node_t;
typedef enum _ctr_html_ttype ctr_html_ttype_t;
typedef struct _ctr_html_tag ctr_html_tag_t;
typedef struct _ctr_html_chars ctr_html_chars_t;
typedef struct _ctr_nodewalk_info ctr_nodewalk_info_t;
typedef struct _ctr_nodewalk_elm ctr_nodewalk_elm_t;
typedef struct _ctr_symmap ctr_symmap_t;

enum _ctr_lex_mode {
    LM_NORMAL, LM_IGNORETAG, LM_IGNOREALL
};

struct _ctr_parse_info {
    apr_array_header_t *elms;
    int ancnest;
    int objnest;
    int validobj;
    int frmnest;
    int validfrm;
    int warnout;
    int lineno;
    const request_rec *r;
};

enum _ctr_parse_stat {
    ES_NEUTRAL, ES_HTML, ES_HEAD, ES_TITLE, ES_STYLE, ES_BODY, ES_INLINEBODY,
    ES_SELECT, ES_OPTION, ES_TEXTAREA, ES_PLAINTEXT, ES_LIST, ES_LISTITEM,
    ES_DEFLIST, ES_DEFTERM, ES_DEFDESC, ES_RAW
};

struct _ctr_parse_elm {
    ctr_parse_stat_t stat;
    const char *tagname;
    ctr_html_tag_t *tag;
};

struct _ctr_html_node {
    enum {
        NT_ELEMENT, NT_CHARS
    } type;
    const ctr_html_tag_t *parent;
    const ctr_html_node_t *siblings;
};

enum _ctr_html_ttype {
    TT_NONE, TT_BR, TT_IMG, TT_INPUT, TT_A, TT_SPAN, TT_FONT, TT_BLINK,
    TT_OBJECT, TT_PARAM, TT_SELECT, TT_TEXTAREA, TT_PLAINTEXT, TT_C3IF,
    TT_C3UNLESS, TT_OPTION, TT_HR, TT_CENTER, TT_MARQUEE, TT_FORM, TT_DIV,
    TT_P, TT_PRE, TT_H1, TT_H2, TT_H3, TT_H4, TT_H5, TT_H6, TT_UL, TT_OL,
    TT_LI, TT_DL, TT_DT, TT_DD, TT_HTML, TT_HEAD, TT_META, TT_BASE, TT_TITLE,
    TT_STYLE, TT_BODY, TT_C3RAW
};

struct _ctr_html_tag {
    ctr_html_node_t node;
    const char *name;
    ctr_html_ttype_t ttype;
    const apr_table_t *attrs;
    int lineno;
    ctr_html_node_t *children;
    ctr_html_node_t *last_child;
};

struct _ctr_html_chars {
    ctr_html_node_t node;
    char *s;
};

struct _ctr_nodewalk_info {
    apr_array_header_t *elms;
    apr_hash_t *stylehash;
    CTR_CLASS_NAME(page) *page;
    CTR_CLASS_NAME(flash) *curfla;
    CTR_CLASS_NAME(form) *curfrm;
    CTR_CLASS_NAME(select) *cursel;
    CTR_CLASS_NAME(option) *curopt;
    CTR_CLASS_NAME(textarea) *curtxa;
    CTR_CLASS_NAME(rawtext) *currtxt;
    ctr_wcontainer_t *pparent;
    int needpbr;
    apr_uri_t base;
    int prenest;
    int warnout;
    int imgconv;
    int imgpercent;
    int imgcopy;
    int imgstore;
    int flashcopy;
    int flashstore;
    const request_rec *r;
};

struct _ctr_nodewalk_elm {
    ctr_html_ttype_t ttype;
    ctr_wcontainer_t *cont;
    ctr_fontsize_t fsize;
};

struct _ctr_symmap {
    const char *sym;
    int val;
};

static ctr_html_tag_t *ctr_html_rawparse(char *data, int flg,
                                         const request_rec *r);
static void ctr_html_start_found(ctr_parse_info_t *info, const char *name,
                                 const apr_table_t *attrs,
                                 ctr_lex_mode_t *mode,
                                 const char **modeendtag);
static void ctr_html_end_found(ctr_parse_info_t *info, const char *name);
static void ctr_html_chars_found(ctr_parse_info_t *info, char *chars);
static void ctr_html_cdata_found(ctr_parse_info_t *info, char *chars);
static ctr_html_tag_t *ctr_html_tag_new(ctr_html_tag_t *parent,
                                        const char *name,
                                        ctr_html_ttype_t ttype,
                                        const apr_table_t *attrs, int lineno,
                                        apr_pool_t *pool);
static void ctr_html_chars_new(ctr_html_tag_t *parent, char *s,
                               apr_pool_t *pool);
static ctr_parse_elm_t *ctr_parse_elm_push(ctr_parse_info_t *info,
                                           ctr_parse_stat_t stat,
                                           const char *tagname,
                                           ctr_html_tag_t *tag);
static int ctr_parse_elm_pop(ctr_parse_info_t *info, const char *name);
static void ctr_html_node_walk(ctr_nodewalk_info_t *info,
                               const ctr_html_tag_t *tag);
static void ctr_html_start_handler(ctr_nodewalk_info_t *info,
                                   const ctr_html_tag_t *tag);
static void ctr_html_end_handler(ctr_nodewalk_info_t *info,
                                 const ctr_html_tag_t *tag);
static void ctr_html_chars_handler(ctr_nodewalk_info_t *info,
                                   const ctr_html_chars_t *chars);
static void ctr_html_style_parse(ctr_nodewalk_info_t *info,
                                 const ctr_html_chars_t *chars);
static ctr_nodewalk_elm_t *ctr_nodewalk_elm_push(ctr_nodewalk_info_t *info,
                                                 ctr_html_ttype_t ttype,
                                                 ctr_wcontainer_t *cont,
                                                 ctr_fontsize_t fsize);
static void ctr_pbr_insert(ctr_nodewalk_info_t *info);
static apr_uri_t *ctr_uri_absolute_convert(const apr_uri_t *base,
                                           const char *add,
                                           const char *scheme,
                                           int port,
                                           const request_rec *r);
static CTR_CLASS_NAME(deco) *ctr_gen_deco_get(const apr_table_t *styles,
                                              const ctr_html_tag_t *tag,
                                              const ctr_nodewalk_elm_t *curelm,
                                              int warnout,
                                              const request_rec *r);
static apr_table_t *ctr_style_split(const char *style, apr_pool_t *pool);
static int ctr_style_txtcolor_get(const apr_table_t *styles,
                                  const ctr_html_tag_t *tag, int warnout,
                                  const request_rec *r);
static int ctr_style_txtcolor2_get(const apr_table_t *styles,
                                   const ctr_html_tag_t *tag, int warnout,
                                   const request_rec *r);
static int ctr_style_bgcolor_get(const apr_table_t *styles,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r);
static ctr_halign_t ctr_style_txtalign_get(const apr_table_t *styles,
                                           const ctr_html_tag_t *tag,
                                           int warnout, const request_rec *r);
static ctr_fontsize_t ctr_style_fontsize_get(const apr_table_t *styles,
                                             const ctr_html_tag_t *tag,
                                             ctr_fontsize_t base, int warnout,
                                             const request_rec *r);
static char *ctr_style_width_get(const apr_table_t *styles,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r);
static int ctr_style_height_get(const apr_table_t *styles,
                                const ctr_html_tag_t *tag, int warnout,
                                const request_rec *r);
static int ctr_style_blink_get(const apr_table_t *styles,
                               const ctr_html_tag_t *tag, int warnout,
                               const request_rec *r);
static int ctr_style_marquee_get(const apr_table_t *styles,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r);
static ctr_direct_t ctr_style_mqedirect_get(const apr_table_t *styles,
                                            const ctr_html_tag_t *tag,
                                            int warnout, const request_rec *r);
static ctr_behav_t ctr_style_mqebehav_get(const apr_table_t *styles,
                                          const ctr_html_tag_t *tag,
                                          int warnout, const request_rec *r);
static int ctr_style_mqeloop_get(const apr_table_t *styles,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r);
static ctr_edit_style_t ctr_style_inpfmt_get(const apr_table_t *styles,
                                             const ctr_html_tag_t *tag,
                                             int warnout,
                                             const request_rec *r);
static const char *ctr_attrget_raw(const apr_table_t *attrs,
                                   const char *attrname);
static char *ctr_attrget_sym(const apr_table_t *attrs, const char *attrname,
                             apr_pool_t *pool);
static apr_table_t *ctr_attrget_symstable(const apr_table_t *attrs,
                                          const char *attrname,
                                          apr_pool_t *pool);
static int ctr_attrget_pint(const apr_table_t *attrs, const char *attrname,
                            int dflt, const ctr_html_tag_t *tag, int warnout,
                            const request_rec *r);
static int ctr_attrget_pint2(const apr_table_t *attrs, const char *attrname,
                             int dflt, const ctr_html_tag_t *tag, int warnout,
                             const request_rec *r);
static int ctr_attrget_flg(const apr_table_t *attrs, const char *attrname);
static int ctr_attrget_color(const apr_table_t *attrs, const char *attrname,
                             const ctr_html_tag_t *tag, int warnout,
                             const request_rec *r);
static int ctr_attrget_symtoval(const apr_table_t *attrs, const char *attrname,
                                const ctr_symmap_t *map, int dflt,
                                const ctr_html_tag_t *tag, int warnout,
                                const request_rec *r);
static int ctr_attrget_symstoorval(const apr_table_t *attrs,
                                   const char *attrname,
                                   const ctr_symmap_t *map, int dflt,
                                   const ctr_html_tag_t *tag, int warnout,
                                   const request_rec *r);
static char *ctr_attrget_lensize(const apr_table_t *attrs,
                                 const char *attrname,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r);

static const ctr_symmap_t copy_map[] = {
    { "yes", TRUE }, { "no", FALSE }, { "on", TRUE }, { "off", FALSE },
    { NULL, 0 }
};
static const ctr_symmap_t store_map[] = {
    { "yes", TRUE }, { "no", FALSE }, { "on", TRUE }, { "off", FALSE },
    { NULL, 0 }
};
static const ctr_symmap_t copyright_map[] = {
    { "yes", FALSE }, { "no", TRUE }, { "on", FALSE }, { "off", TRUE },
    { NULL, 0 }
};
static const ctr_symmap_t halign_map[] = {
    { "left", AL_LEFT }, { "center", AL_CENTER }, { "right", AL_RIGHT },
    { NULL, 0 }
};
static const ctr_symmap_t mqebehav_map[] = {
    { "scroll", BH_SCROLL }, { "slide", BH_SLIDE },
    { "alternate", BH_ALTERNATE }, { NULL, 0 }
};
static const ctr_symmap_t istyle_map[] = {
    { "1", ST_FULLKANA }, { "2", ST_HALFKANA }, { "3", ST_HALFALPHA },
    { "4", ST_HALFNUM }, { NULL, 0 }
};

apr_status_t ctr_html_parse(CTR_CLASS_NAME(page) *page, char *data,
                            const ctr_parser_opt_t *opt, const request_rec *r)
{
    ctr_html_tag_t *root;
    ctr_nodewalk_info_t info;

    root = ctr_html_rawparse(data, opt->flg, r);

    info.elms = apr_array_make(r->pool, 1, sizeof(ctr_nodewalk_elm_t));
    info.stylehash = apr_hash_make(r->pool);
    info.page = page;
    info.curfla = NULL;
    info.curfrm = NULL;
    info.cursel = NULL;
    info.curopt = NULL;
    info.curtxa = NULL;
    info.currtxt = NULL;
    info.pparent = NULL;
    info.needpbr = FALSE;
    info.base = r->parsed_uri;
    if (info.base.path == NULL)
        info.base.path = "/";
    info.prenest = 0;
    info.warnout = opt->flg & PF_WARN_OUT;
    info.imgconv = opt->flg & PF_IMG_CONV;
    info.imgpercent = opt->img_percent;
    info.imgcopy = opt->flg & PF_IMG_COPY;
    info.imgstore = opt->flg & PF_IMG_STORE;
    info.flashcopy = opt->flg & PF_FLASH_COPY;
    info.flashstore = opt->flg & PF_FLASH_STORE;
    info.r = r;

    ctr_nodewalk_elm_push(&info, TT_NONE,
                          CTR_WCONTAINER_CAST(page), FS_MEDIUM);

    ctr_html_node_walk(&info, root);

    return APR_SUCCESS;
}

static ctr_html_tag_t *ctr_html_rawparse(char *data, int flg,
                                         const request_rec *r)
{
#define CTR_HTML_START_FOUND() do {                                     \
    namep = ctr_tolower_str(namep);                                     \
    if (mode == LM_NORMAL) {                                            \
        ctr_html_start_found(&info, namep, attrs, &mode, &modeendtag);  \
    }                                                                   \
} while (0)

#define CTR_HTML_END_FOUND() do {                                       \
    namep = ctr_tolower_str(namep);                                     \
    if (mode != LM_NORMAL) {                                            \
        if (strcmp(namep, modeendtag) == 0) {                           \
            if (mode == LM_IGNORETAG && buf != NULL) {                  \
                *gtp = '\0';                                            \
                ctr_html_chars_found(&info, buf);                       \
                buf = bufp = gtp = NULL;                                \
                buflen = 0;                                             \
            }                                                           \
            ctr_html_end_found(&info, namep);                           \
            mode = LM_NORMAL;                                           \
            modeendtag = NULL;                                          \
        }                                                               \
    } else                                                              \
        ctr_html_end_found(&info, namep);                               \
} while (0)

#define CTR_HTML_CHARS_FOUND() do {                                     \
    if (mode == LM_NORMAL)                                              \
        ctr_html_chars_found(&info, genp);                              \
} while (0)

#define CTR_TAGATTR_ADD() do {                                          \
    attrkey = ctr_tolower_str(genp);                                    \
    apr_table_setn(attrs, attrkey, "");                                 \
} while (0)

    ctr_parse_info_t info;
    ctr_html_tag_t *root;
    enum {
        PS_NEUTRAL, PS_PRE_TAG_NAME, PS_START_TAG_NAME, PS_PRE_ATTR_NAME,
        PS_ATTR_NAME, PS_PRE_ATTR_EQUAL, PS_ATTR_EQUAL, PS_ATTR_QUOT,
        PS_ATTR_VAL, PS_ATTR_VAL_QUOT, PS_PRE_END_TAG_NAME, PS_END_TAG_NAME,
        PS_TAG_IS_CLOSING
    } psstat = PS_NEUTRAL;
    char *p;
    char *genp = NULL;
    char *namep = NULL;
    apr_table_t *attrs = NULL;
    char *attrkey = NULL;
    char quotc = '\0';
    ctr_lex_mode_t mode = LM_NORMAL;
    const char *modeendtag = NULL;
    char *buf = NULL, *bufp = NULL, *gtp = NULL;
    size_t buflen = 0;

    /*
     * Initialize parsing information.
     */
    info.elms = apr_array_make(r->pool, 1, sizeof(ctr_parse_elm_t));
    info.ancnest = 0;
    info.objnest = 0;
    info.validobj = 0;
    info.frmnest = 0;
    info.validfrm = 0;
    info.warnout = flg & PF_WARN_OUT;
    info.lineno = 1;
    info.r = r;

    /*
     * Initialize root element.
     */
    root = apr_palloc(r->pool, sizeof(*root));
    root->node.type = NT_ELEMENT;
    root->node.parent = NULL;
    root->node.siblings = NULL;
    root->ttype = TT_NONE;
    root->attrs = NULL;
    root->children = NULL;
    root->last_child = NULL;
    ctr_parse_elm_push(&info, ES_NEUTRAL, "", root);

    for (p = data; *p; p++) {
        if (mode == LM_IGNORETAG) {
            /*
             * If now not in normal mode or not in ignoreall mode, remember
             * all characters in order to pass them ctr_html_chars_found()
             * when we will go out of the mode.
             */
            if (buf == NULL) {
                buf = apr_palloc(r->pool, 64);  /* XXX: 64 */
                buflen = 64;
                bufp = buf;
            }
            if (bufp - buf >= buflen - 2) {
                char *newbuf = apr_palloc(r->pool, buflen * 2);
                strcpy(newbuf, buf);
                bufp = newbuf + (bufp - buf);
                if (gtp != NULL)
                    gtp = newbuf + (gtp - buf);
                buf = newbuf;
                buflen *= 2;
            }
            *bufp = *p;
            *(bufp + 1) = '\0';
            bufp++;
        }

        if (*p == '<') {
            switch (psstat) {
            case PS_NEUTRAL:
                /*
                 * A text is terminated.  And a new tag is begining.
                 */
                *p = '\0';
                if (genp != NULL)
                    CTR_HTML_CHARS_FOUND();
                psstat = PS_PRE_TAG_NAME;
                if (mode == LM_IGNORETAG)
                    gtp = bufp - 1;
                break;
            case PS_PRE_TAG_NAME:
            case PS_ATTR_VAL_QUOT:
            case PS_TAG_IS_CLOSING:
                /* NOTHING TO BE DONE */
                break;
            case PS_ATTR_QUOT:
                /*
                 * "<" in a attribute value that is quoted is regarded as a
                 * normal character.
                 */
                genp = p;
                psstat = PS_ATTR_VAL_QUOT;
                break;
            case PS_PRE_END_TAG_NAME:
                /*
                 * Fall in here if we found "</<".  This is a strange case.
                 * Ignore the previous "</", and assume that a new tag is
                 * begining.
                 */
                psstat = PS_PRE_TAG_NAME;
                if (mode == LM_IGNORETAG)
                    gtp = bufp - 1;
                break;
            default:
                /*
                 * Fall in here if psstat is:
                 *   PS_START_TAG_NAME, PS_PRE_ATTR_NAME, PS_ATTR_NAME,
                 *   PS_PRE_ATTR_EQUAL, PS_ATTR_EQUAL, PS_ATTR_VAL,
                 *   PS_END_TAG_NAME
                 *
                 * Found "<" before a tag is closed.  We assume that a previous
                 * tag is closed, and a new tag is begining.
                 */
                *p = '\0';
                if (psstat == PS_ATTR_NAME)
                    CTR_TAGATTR_ADD();
                else if (psstat == PS_ATTR_VAL)
                    apr_table_setn(attrs, attrkey,
                                   ctr_html_unescape(genp, r->pool));
                if (psstat == PS_END_TAG_NAME)
                    CTR_HTML_END_FOUND();
                else
                    CTR_HTML_START_FOUND();
                psstat = PS_PRE_TAG_NAME;
                if (mode == LM_IGNORETAG)
                    gtp = bufp - 1;
            }
        } else if (*p == '>') {
            switch (psstat) {
            case PS_NEUTRAL:
                /*
                 * Found ">" in a normal text.  Probably it means "&gt;".
                 */
                if (genp == NULL)
                    genp = p;
                break;
            case PS_ATTR_QUOT:
                /*
                 * ">" in a attribute value that is quoted is regarded as a
                 * normal character.
                 */
                genp = p;
                psstat = PS_ATTR_VAL_QUOT;
                break;
            case PS_ATTR_VAL_QUOT:
                /* NOTHING TO BE DONE */
                break;
            case PS_PRE_TAG_NAME:
            case PS_PRE_END_TAG_NAME:
            case PS_TAG_IS_CLOSING:
                genp = NULL;
                psstat = PS_NEUTRAL;
                break;
            default:
                /*
                 * Fall in here if psstat is:
                 *   PS_START_TAG_NAME, PS_PRE_ATTR_NAME, PS_ATTR_NAME,
                 *   PS_PRE_ATTR_EQUAL, PS_ATTR_EQUAL, PS_ATTR_VAL,
                 *   PS_END_TAG_NAME
                 *
                 * A tag is closed.  If psstat is PS_ATTR_EQUAL, this is a bit
                 * strange closing, but we can assume the intention.  if psstat
                 * is others, this is a quite normal case.
                 */
                *p = '\0';
                if (psstat == PS_ATTR_NAME)
                    CTR_TAGATTR_ADD();
                else if (psstat == PS_ATTR_VAL)
                    apr_table_setn(attrs, attrkey,
                                   ctr_html_unescape(genp, r->pool));
                if (psstat == PS_END_TAG_NAME)
                    CTR_HTML_END_FOUND();
                else
                    CTR_HTML_START_FOUND();
                genp = NULL;
                psstat = PS_NEUTRAL;
            }
        } else if (*p == '/') {
            switch (psstat) {
            case PS_NEUTRAL:
                /*
                 * Found "/" in a normal text.  This is a normal character.
                 */
                if (genp == NULL)
                    genp = p;
                break;
            case PS_PRE_TAG_NAME:
                /*
                 * Found "</".  An end tag is begining.
                 */
                psstat = PS_PRE_END_TAG_NAME;
                break;
            case PS_START_TAG_NAME:
            case PS_PRE_ATTR_NAME:
            case PS_ATTR_NAME:
            case PS_PRE_ATTR_EQUAL:
                /*
                 * An empty tag is closing.  If psstat is PS_START_TAG_NAME or
                 * PS_ATTR_NAME, there is no spaces before "/".  Althouth it
                 * is desriable that spaces exist, we can easily assume the
                 * meaning in this case.
                 */
                *p = '\0';
                if (psstat == PS_ATTR_NAME)
                    CTR_TAGATTR_ADD();
                CTR_HTML_START_FOUND();
                CTR_HTML_END_FOUND();
                psstat = PS_TAG_IS_CLOSING;
                break;
            case PS_ATTR_EQUAL:
            case PS_ATTR_QUOT:
                /*
                 * There can be "/" in an attribute value.  ex. file path.
                 */
                genp = p;
                if (psstat == PS_ATTR_EQUAL)
                    psstat = PS_ATTR_VAL;
                else
                    psstat = PS_ATTR_VAL_QUOT;
                break;
            default:
                /*
                 * Fall in here if psstat is:
                 *   PS_ATTR_VAL, PS_ATTR_VAL_QUOT, PS_PRE_END_TAG_NAME
                 *   PS_END_TAG_NAME, PS_TAG_IS_CLOSING
                 */
                /* NOTHING TO BE DONE */
                break;
            }
        } else if (*p == '=') {
            switch (psstat) {
            case PS_NEUTRAL:
                /*
                 * Found "=" in a normal text.  This is a normal character.
                 */
                if (genp == NULL)
                    genp = p;
                break;
            case PS_ATTR_NAME:
            case PS_PRE_ATTR_EQUAL:
                /*
                 * An attribute name is terminated.
                 */
                *p = '\0';
                if (psstat == PS_ATTR_NAME)
                    CTR_TAGATTR_ADD();
                psstat = PS_ATTR_EQUAL;
                break;
            case PS_ATTR_EQUAL:
            case PS_ATTR_QUOT:
                /*
                 * An attribute value is begining.  There can be "=" in an
                 * attribute value, of course.
                 */
                genp = p;
                if (psstat == PS_ATTR_EQUAL)
                    psstat = PS_ATTR_VAL;
                else
                    psstat = PS_ATTR_VAL_QUOT;
                break;
            default:
                /*
                 * Fall in here if psstat is:
                 *   PS_PRE_TAG_NAME, PS_START_TAG_NAME, PS_PRE_ATTR_NAME
                 *   PS_ATTR_VAL, PS_ATTR_VAL_QUOT, PS_PRE_END_TAG_NAME
                 *   PS_END_TAG_NAME, TAG_IS_CLOSING
                 */
                /* NOTHING TO BE DONE */
                break;
            }
        } else if (*p == '"' || *p == '\'') {
            switch (psstat) {
            case PS_NEUTRAL:
                /*
                 * Found """ or "'" in a normal text.  In case of """,
                 * probably it means "&quot;".
                 */
                if (genp == NULL)
                    genp = p;
                break;
            case PS_ATTR_EQUAL:
                /*
                 * An attribute value that is quoted is begining.
                 */
                quotc = *p;
                psstat = PS_ATTR_QUOT;
                break;
            case PS_ATTR_QUOT:
                if (quotc == *p) {
                    /*
                     * An attribute value that is null string is terminated.
                     */
                    psstat = PS_PRE_ATTR_NAME;
                } else {
                    /*
                     * If the quoting character we found right now differs from
                     * the one found before, it is a part of attribute value.
                     */
                    genp = p;
                    psstat = PS_ATTR_VAL_QUOT;
                }
                break;
            case PS_ATTR_VAL_QUOT:
                /*
                 * If the quoting character we found right now differs from the
                 * one found before, it is a part of attribute value.
                 */
                if (quotc == *p) {
                    *p = '\0';
                    apr_table_setn(attrs, attrkey,
                                   ctr_html_unescape(genp, r->pool));
                    psstat = PS_PRE_ATTR_NAME;
                }
                break;
            default:
                /*
                 * Fall in here if psstat is:
                 *   PS_PRE_TAG_NAME, PS_START_TAG_NAME, PS_PRE_ATTR_NAME
                 *   PS_ATTR_NAME, PS_PRE_ATTR_EQUAL, PS_ATTR_VAL
                 *   PS_PRE_END_TAG_NAME, PS_END_TAG_NAME, PS_TAG_IS_CLOSING
                 */
                /* NOTHING TO BE DONE */
                break;
            }
        } else if (apr_isspace(*p)) {
            if (*p == '\r' && *(p + 1) != '\n')
                info.lineno++;
            else if (*p == '\n')
                info.lineno++;

            switch (psstat) {
            case PS_NEUTRAL:
                /*
                 * Found " " in a normal text.  This is a normal character.
                 */
                if (genp == NULL)
                    genp = p;
                break;
            case PS_START_TAG_NAME:
                /*
                 * A start tag name is terminated.
                 */
                *p = '\0';
                psstat = PS_PRE_ATTR_NAME;
                break;
            case PS_ATTR_NAME:
                /*
                 * An attribute name is terminated.
                 */
                *p = '\0';
                CTR_TAGATTR_ADD();
                psstat = PS_PRE_ATTR_EQUAL;
                break;
            case PS_ATTR_QUOT:
                /*
                 * An attribute value is begining.  This space is the first
                 * character of the value.
                 */
                genp = p;
                psstat = PS_ATTR_VAL_QUOT;
                break;
            case PS_ATTR_VAL:
                /*
                 * An attribute value is terminated.
                 */
                *p = '\0';
                apr_table_setn(attrs, attrkey,
                               ctr_html_unescape(genp, r->pool));
                psstat = PS_PRE_ATTR_NAME;
                break;
            case PS_END_TAG_NAME:
                /*
                 * An end tag name is terminated.
                 */
                *p = '\0';
                CTR_HTML_END_FOUND();
                psstat = PS_TAG_IS_CLOSING;
                break;
            default:
                /*
                 * Fall in here if psstat is:
                 *   PS_PRE_TAG_NAME, PS_PRE_ATTR_NAME, PS_PRE_ATTR_EQUAL
                 *   PS_ATTR_EQUAL, PS_ATTR_VAL_QUOT, PS_PRE_END_TAG_NAME
                 *   PS_TAG_IS_CLOSING
                 */
                /* NOTHING TO BE DONE */
                break;
            }
        } else {
            switch (psstat) {
            case PS_NEUTRAL:
                /*
                 * Found a character in a normal text.
                 */
                if (genp == NULL)
                    genp = p;
                break;
            case PS_PRE_TAG_NAME:
                /*
                 * A start tag name is begining.
                 */
                namep = p;
                attrs = apr_table_make(r->pool, 1);
                psstat = PS_START_TAG_NAME;
                break;
            case PS_START_TAG_NAME:
                if (*p == '[' && p - namep == 7 && mode == LM_NORMAL &&
                    strncmp(namep, "![CDATA", 7) == 0) {
                    /*
                     * A CDATA section is begining.
                     */
                    char *q;

                    if ((q = strstr(p + 1, "]]>")) != NULL) {
                        *q = '\0';
                        ctr_html_cdata_found(&info, p + 1);
                        p = q + 2;
                        genp = NULL;
                        psstat = PS_NEUTRAL;
                    } else {
                        /*
                         * Return here since the left characters are all in a
                         * CDATA section.
                         */
                        ctr_html_cdata_found(&info, p + 1);
                        return root;
                    }
                } else if (*p == '-' && p - namep == 2 && mode == LM_NORMAL &&
                           strncmp(namep, "!-", 2) == 0) {
                    /*
                     * A comment is begining.
                     */
                    char *q;

                    if ((q = strstr(p + 1, "-->")) != NULL) {
                        p = q + 2;
                        genp = NULL;
                        psstat = PS_NEUTRAL;
                    } else if ((q = strchr(p + 1, '>')) != NULL) {
                        p = q;
                        genp = NULL;
                        psstat = PS_NEUTRAL;
                    } else {
                        /*
                         * Return here since the left characters are all
                         * commented out.
                         */
                        return root;
                    }
                }
                break;
            case PS_PRE_ATTR_NAME:
            case PS_PRE_ATTR_EQUAL:
                /*
                 * An attribute name is begining.  The previous attribute has
                 * no value if psstat is PS_PRE_ATTR_EQUAL.
                 */
                genp = p;
                psstat = PS_ATTR_NAME;
                break;
            case PS_ATTR_EQUAL:
            case PS_ATTR_QUOT:
                /*
                 * An attribute value is begining.
                 */
                genp = p;
                if (psstat == PS_ATTR_EQUAL)
                    psstat = PS_ATTR_VAL;
                else
                    psstat = PS_ATTR_VAL_QUOT;
                break;
            case PS_PRE_END_TAG_NAME:
                /*
                 * An end tag name is begining.
                 */
                namep = p;
                psstat = PS_END_TAG_NAME;
                break;
            default:
                /*
                 * Fall in here if psstat is:
                 *   PS_ATTR_NAME, PS_ATTR_VAL, PS_ATTR_VAL_QUOT,
                 *   PS_END_TAG_NAME, PS_TAG_IS_CLOSING
                 */
                /* NOTHING TO BE DONE */
                break;
            }
        }
    }

    if (mode != LM_NORMAL) {
        if (mode == LM_IGNORETAG && buf != NULL)
            ctr_html_chars_found(&info, buf);
    } else {
        switch (psstat) {
        case PS_NEUTRAL:
            if (genp != NULL)
                CTR_HTML_CHARS_FOUND();
            break;
        case PS_END_TAG_NAME:
            CTR_HTML_END_FOUND();
            break;
        default:
            /* NOTHING TO BE DONE */
            break;
        }
    }

    return root;
}

static void ctr_html_start_found(ctr_parse_info_t *info, const char *name,
                                 const apr_table_t *attrs,
                                 ctr_lex_mode_t *mode,
                                 const char **modeendtag)
{
    const request_rec *r;
    apr_pool_t *pool;
    ctr_parse_elm_t *curelm;
    ctr_html_ttype_t ttype;

    r = info->r;
    pool = r->pool;
    curelm = (ctr_parse_elm_t *)CTR_ARRAY_LAST(info->elms);
    ttype = TT_NONE;
    *mode = LM_NORMAL;
    *modeendtag = NULL;

    if ((strcmp(name, "br") == 0 && (ttype = TT_BR)) ||
        (strcmp(name, "img") == 0 && (ttype = TT_IMG)) ||
        (strcmp(name, "input") == 0 && (ttype = TT_INPUT)) ||
        (strcmp(name, "a") == 0 && (ttype = TT_A)) ||
        (strcmp(name, "span") == 0 && (ttype = TT_SPAN)) ||
        (strcmp(name, "font") == 0 && (ttype = TT_FONT)) ||
        (strcmp(name, "blink") == 0 && (ttype = TT_BLINK)) ||
        (strcmp(name, "object") == 0 && (ttype = TT_OBJECT)) ||
        (strcmp(name, "param") == 0 && (ttype = TT_PARAM)) ||
        (strcmp(name, "select") == 0 && (ttype = TT_SELECT)) ||
        (strcmp(name, "textarea") == 0 && (ttype = TT_TEXTAREA)) ||
        (strcmp(name, "plaintext") == 0 && (ttype = TT_PLAINTEXT)) ||
        (strcmp(name, "c3:if") == 0 && (ttype = TT_C3IF)) ||
        (strcmp(name, "c3:unless") == 0 && (ttype = TT_C3UNLESS))) {
        /*
         * Found the following tags:
         * <br>, <img>, <input>, <a>, <span>, <font>, <blink>, <object>,
         * <select>, <textarea>, <plaintext>, <c3:if>, <c3:unless>
         *
         * These are inline elements.
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_NEUTRAL:
            newtag = ctr_html_tag_new(curelm->tag, "html", TT_HTML,
                                      apr_table_make(pool, 1), info->lineno,
                                      pool);
            curelm = ctr_parse_elm_push(info, ES_HTML, "html", newtag);
            /* FALL THROUGH */
        case ES_HTML:
            if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: found <%s> "
                              "before <body>", r->uri, info->lineno, name);
            }
            newtag = ctr_html_tag_new(curelm->tag, "body", TT_BODY,
                                      apr_table_make(pool, 1), info->lineno,
                                      pool);
            curelm = ctr_parse_elm_push(info, ES_BODY, "body", newtag);
            /* FALL THROUGH */
        case ES_BODY:
        case ES_INLINEBODY:
            if (ttype == TT_PARAM && info->validobj == 0) {
                /*
                 * Ignore <param> tags if they are not offspring of <object>
                 * tag.
                 */
                if (info->warnout) {
                    ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                                  "mod_cthree: %s line %d: found <%s> "
                                  "before valid <object>, ignore the tag",
                                  r->uri, info->lineno, name);
                }
            } else if ((ttype == TT_INPUT ||
                 ttype == TT_SELECT ||
                 ttype == TT_TEXTAREA) && info->validfrm == 0) {
                /*
                 * Ignore <input>, <select> or <textarea> tags if they are not
                 * offspring of <form> tag.  This is i-mode HTML behaviour.
                 */
                if (info->warnout) {
                    ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                                  "mod_cthree: %s line %d: found <%s> "
                                  "before valid <form>, ignore the tag",
                                  r->uri, info->lineno, name);
                }
                if (ttype != TT_INPUT) {
                    curelm = ctr_parse_elm_push(info, curelm->stat,
                                                name, curelm->tag);
                }
                if (ttype == TT_TEXTAREA) {
                    /*
                     * When <textarea> appears outside <form>, all offspring of
                     * that tag is ignored.
                     */
                    *mode = LM_IGNOREALL;
                    *modeendtag = name;
                }
            } else if (ttype == TT_BR || ttype == TT_IMG ||
                       ttype == TT_PARAM || ttype == TT_INPUT) {
                /*
                 * Empty tags.  Parsing status is not changed.
                 */
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
            } else if (ttype == TT_A) {
                info->ancnest++;

                if (info->ancnest == 1) {
                    newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                              info->lineno, pool);
                    curelm = ctr_parse_elm_push(info, ES_INLINEBODY,
                                                name, newtag);
                } else {
                    /*
                     * Ignore <a> tag as a descendant of <a> tag.
                     */
                    goto invalid_place;
                }
            } else if (ttype == TT_OBJECT) {
                const char *otype;

                info->objnest++;

                if (info->validobj > 0) {
                    /*
                     * Ignore <object> tag if it is a descendant of <object>
                     * tag.  This limitation is mod_cthree specific.
                     */
                    goto invalid_place;
                } else if ((otype = ctr_attrget_raw(attrs, "type")) == NULL ||
                           strcasecmp(otype,
                                      "application/x-shockwave-flash") != 0) {
                    /*
                     * Ignore <object> tag if it does not have a type
                     * parameter or the value of that is not
                     * "application/x-shockwave-flash".
                     */
                    if (info->warnout) {
                        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                                      "mod_cthree: %s line %d: no type or "
                                      "invalid type in <%s>, "
                                      "ignore the tag",
                                      r->uri, info->lineno, name);
                    }
                    curelm = ctr_parse_elm_push(info, curelm->stat,
                                                name, curelm->tag);
                } else {
                    info->validobj = info->objnest;
                    newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                              info->lineno, pool);
                    /*
                     * mod_cthree allows only inline elements to be offspring
                     * of <object> tag.
                     */
                    curelm = ctr_parse_elm_push(info, ES_INLINEBODY,
                                                name, newtag);
                }
            } else if (ttype == TT_SELECT) {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, ES_SELECT, name, newtag);
            } else if (ttype == TT_TEXTAREA) {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, ES_TEXTAREA, name, newtag);
                *mode = LM_IGNORETAG;
                *modeendtag = name;
            } else if (ttype == TT_PLAINTEXT) {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, ES_PLAINTEXT, name, newtag);
                *mode = LM_IGNORETAG;
                *modeendtag = "";
            } else {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, curelm->stat, name, newtag);
            }
            break;
        case ES_LIST:
        case ES_DEFLIST:
            if (ttype == TT_BR || ttype == TT_A) {
                /*
                 * Ignore <br> or <a> if they are children of <ul>, <ol> or
                 * <dl> tag.
                 */
                goto invalid_place;
            }
            /* FALL THROUGH */
        case ES_LISTITEM:
        case ES_DEFTERM:
        case ES_DEFDESC:
            if (ttype == TT_IMG || ttype == TT_INPUT || ttype == TT_OBJECT ||
                ttype == TT_PARAM || ttype == TT_SELECT ||
                ttype == TT_TEXTAREA) {
                /*
                 * Ignore <img>, <input>, <object>, <param>, <select> or
                 * <textarea> tags if they are offspring of <li> tag.  This
                 * limitation is mod_cthree specific.  We hope the limitation
                 * will be removed in the future...
                 */
                if (ttype == TT_TEXTAREA) {
                    /*
                     * When <textarea> appears inside <li>, all offspring of
                     * that tag is ignored.
                     */
                    *mode = LM_IGNOREALL;
                    *modeendtag = name;
                }
                goto invalid_place;
            } else if (ttype == TT_BR) {
                /*
                 * Empty tags  Parsing status is not changed.
                 */
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
            } else if (ttype == TT_A) {
                info->ancnest++;

                if (info->ancnest == 1) {
                    newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                              info->lineno, pool);
                    curelm = ctr_parse_elm_push(info, ES_INLINEBODY,
                                                name, newtag);
                } else {
                    /*
                     * Ignore <a> tag as a descendant of <a> tag.
                     */
                    goto invalid_place;
                }
            } else {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, curelm->stat, name, newtag);
            }
            break;
        default:
            goto invalid_place;
        }
    } else if (strcmp(name, "option") == 0 && (ttype = TT_OPTION)) {
        /*
         * Found <option> tag.
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_OPTION:
            apr_array_pop(info->elms);
            curelm = (ctr_parse_elm_t *)CTR_ARRAY_LAST(info->elms);
            /* FALL THROUGH */
        case ES_SELECT:
            newtag = ctr_html_tag_new(curelm->tag, name, TT_OPTION, attrs,
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_OPTION, name, newtag);
            break;
        default:
            goto invalid_place;
        }
    } else if ((strcmp(name, "hr") == 0 && (ttype = TT_HR)) ||
               (strcmp(name, "center") == 0 && (ttype = TT_CENTER)) ||
               (strcmp(name, "marquee") == 0 && (ttype = TT_MARQUEE)) ||
               (strcmp(name, "form") == 0 && (ttype = TT_FORM)) ||
               (strcmp(name, "div") == 0 && (ttype = TT_DIV)) ||
               (strcmp(name, "p") == 0 && (ttype = TT_P)) ||
               (strcmp(name, "pre") == 0 && (ttype = TT_PRE)) ||
               (strcmp(name, "h1") == 0 && (ttype = TT_H1)) ||
               (strcmp(name, "h2") == 0 && (ttype = TT_H2)) ||
               (strcmp(name, "h3") == 0 && (ttype = TT_H3)) ||
               (strcmp(name, "h4") == 0 && (ttype = TT_H4)) ||
               (strcmp(name, "h5") == 0 && (ttype = TT_H5)) ||
               (strcmp(name, "h6") == 0 && (ttype = TT_H6)) ||
               (strcmp(name, "ul") == 0 && (ttype = TT_UL)) ||
               (strcmp(name, "dir") == 0 && (ttype = TT_UL)) ||
               (strcmp(name, "menu") == 0 && (ttype = TT_UL)) ||
               (strcmp(name, "ol") == 0 && (ttype = TT_OL)) ||
               (strcmp(name, "dl") == 0 && (ttype = TT_DL))) {
        /*
         * Found the following tags:
         * <hr>, <center>, <marquee>, <form>, <div>, <p>, <pre>, <h1>, <h2>,
         * <h3>, <h4>, <h5>, <h6>, <ul>, <dir>, <menu>, <ol>, <dl>
         *
         * These are block elements.
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_NEUTRAL:
            newtag = ctr_html_tag_new(curelm->tag, "html", TT_HTML,
                                      apr_table_make(pool, 1), info->lineno,
                                      pool);
            curelm = ctr_parse_elm_push(info, ES_HTML, "html", newtag);
            /* FALL THROUGH */
        case ES_HTML:
            if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: found <%s> "
                              "before <body>", r->uri, info->lineno, name);
            }
            newtag = ctr_html_tag_new(curelm->tag, "body", TT_BODY,
                                      apr_table_make(pool, 1),
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_BODY, "body", newtag);
            /* FALL THROUGH */
        case ES_BODY:
            if (ttype == TT_HR) {
                /*
                 * Empty tags.  Parsing status is not changed.
                 */
                ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                 info->lineno,pool);
            } else if (ttype == TT_FORM) {
                const char *port;

                info->frmnest++;

                if (info->validfrm > 0) {
                    /*
                     * Ignore <form> tag if it is a descendant of <form> tag.
                     */
                    goto invalid_place;
                } else if (ctr_attrget_raw(attrs, "action") == NULL &&
                           ctr_attrget_raw(attrs, "scheme") == NULL &&
                           ((port = ctr_attrget_raw(attrs, "port")) == NULL ||
                            apr_atoi64(port) <= 0)) {
                    /*
                     * Ignore <form> if it does not have any of action, scheme,
                     * and port parameters.
                     */
                    if (info->warnout) {
                        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                                      "mod_cthree: %s line %d: no action, "
                                      "no scheme, and no port in <%s>, "
                                      "ignore the tag",
                                      r->uri, info->lineno, name);
                    }
                    curelm = ctr_parse_elm_push(info, curelm->stat,
                                                name, curelm->tag);
                } else {
                    info->validfrm = info->frmnest;
                    newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                              info->lineno, pool);
                    curelm = ctr_parse_elm_push(info, curelm->stat,
                                                name, newtag);
                }
            } else if (ttype == TT_H1 ||
                       ttype == TT_H2 ||
                       ttype == TT_H3 ||
                       ttype == TT_H4 ||
                       ttype == TT_H5 ||
                       ttype == TT_H6) {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, ES_INLINEBODY, name, newtag);
            } else if (ttype == TT_UL || ttype == TT_OL) {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, ES_LIST, name, newtag);
            } else if (ttype == TT_DL) {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, ES_DEFLIST, name, newtag);
            } else {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, curelm->stat, name, newtag);
            }
            break;
        case ES_LISTITEM:
            /*
             * We forbid <dl> inside <li> because of an au WAP 2.0 browsers'
             * bug.  They cannot display correctly in this case.
             */
            if (ttype == TT_UL || ttype == TT_OL) {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, ES_LIST, name, newtag);
            } else
                goto invalid_place;
            break;
        case ES_INLINEBODY:
        case ES_LIST:
        case ES_DEFLIST:
        case ES_DEFTERM:
        case ES_DEFDESC:
            /*
             * Ignore the tag.
             * We forbid <ul>, <ol> and <dl> inside <dl> because of an au WAP
             * 2.0 browsers' bug...
             */
            /* FALL THROUGH */
        default:
            goto invalid_place;
        }
    } else if (strcmp(name, "li") == 0 && (ttype = TT_LI)) {
        /*
         * Found <li> tag.
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_LISTITEM:
            ctr_parse_elm_pop(info, "li");
            curelm = (ctr_parse_elm_t *)CTR_ARRAY_LAST(info->elms);
            /* FALL THROUGH */
        case ES_LIST:
            newtag = ctr_html_tag_new(curelm->tag, name, TT_LI, attrs,
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_LISTITEM, name, newtag);
            break;
        default:
            goto invalid_place;
        }
    } else if ((strcmp(name, "dt") == 0 && (ttype = TT_DT)) ||
               (strcmp(name, "dd") == 0 && (ttype = TT_DD))) {
        /*
         * Found <dt> or <dd> tag.
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_DEFTERM:
        case ES_DEFDESC:
            if (curelm->stat == ES_DEFTERM)
                ctr_parse_elm_pop(info, "dt");
            else
                ctr_parse_elm_pop(info, "dd");
            curelm = (ctr_parse_elm_t *)CTR_ARRAY_LAST(info->elms);
            /* FALL THROUGH */
        case ES_DEFLIST:
            newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info,
                                        ttype == TT_DT ?
                                        ES_DEFTERM : ES_DEFDESC,
                                        name, newtag);
            break;
        default:
            goto invalid_place;
        }
    } else if (strcmp(name, "?xml") == 0) {
        /*
         * Found XML declaration.
         */
        switch (curelm->stat) {
        case ES_NEUTRAL:
            /*
             * Simply ignore it.
             */
            break;
        default:
            if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: found XML declaration "
                              "at invalid place, ignore it",
                              r->uri, info->lineno);
            }
        }
    } else if (strcmp(name, "!doctype") == 0) {
        /*
         * Found <!DOCTYPE>
         */
        switch (curelm->stat) {
        case ES_NEUTRAL:
            /*
             * Simply ignore it.
             */
            break;
        default:
            if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: found <!DOCTYPE> "
                              "at invalid place, ignore it",
                              r->uri, info->lineno);
            }
        }
    } else if (strcmp(name, "html") == 0 && (ttype = TT_HTML)) {
        /*
         * Found <html> tag.
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_NEUTRAL:
            newtag = ctr_html_tag_new(curelm->tag, name, TT_HTML, attrs,
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_HTML, name, newtag);
            break;
        default:
            goto invalid_place;
        }
    } else if (strcmp(name, "head") == 0 && (ttype = TT_HEAD)) {
        /*
         * Found <head> tag.
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_NEUTRAL:
            if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: found <%s> "
                              "before <html>", r->uri, info->lineno, name);
            }
            newtag = ctr_html_tag_new(curelm->tag, "html", TT_HTML,
                                      apr_table_make(pool, 1), info->lineno,
                                      pool);
            curelm = ctr_parse_elm_push(info, ES_HTML, "html", newtag);
            /* FALL THROUGH */
        case ES_HTML:
            newtag = ctr_html_tag_new(curelm->tag, name, TT_HEAD, attrs,
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_HEAD, name, newtag);
            break;
        default:
            goto invalid_place;
        }
    } else if ((strcmp(name, "meta") == 0 && (ttype = TT_META)) ||
               (strcmp(name, "base") == 0 && (ttype = TT_BASE)) ||
               (strcmp(name, "title") == 0 && (ttype = TT_TITLE)) ||
               (strcmp(name, "style") == 0 && (ttype = TT_STYLE))) {
        /*
         * Found the following tags:
         * <meta>, <base>, <title>
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_NEUTRAL:
            newtag = ctr_html_tag_new(curelm->tag, "html", TT_HTML,
                                      apr_table_make(pool, 1),
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_HTML, "html", newtag);
            /* FALL THROUGH */
        case ES_HTML:
            if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: found <%s> "
                              "before <head>", r->uri, info->lineno, name);
            }
            newtag = ctr_html_tag_new(curelm->tag, "head", TT_HEAD,
                                      apr_table_make(pool, 1),
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_HEAD, "head", newtag);
            /* FALL THROUGH */
        case ES_HEAD:
            if (ttype == TT_META || ttype == TT_BASE) {
                /*
                 * Empty tags.  Parsing status is not changed.
                 */
                ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                 info->lineno, pool);
            } else if (ttype == TT_TITLE) {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, ES_TITLE, name, newtag);
            } else if (ttype == TT_STYLE) {
                newtag = ctr_html_tag_new(curelm->tag, name, ttype, attrs,
                                          info->lineno, pool);
                curelm = ctr_parse_elm_push(info, ES_STYLE, name, newtag);
                *mode = LM_IGNORETAG;
                *modeendtag = name;
            }
            break;
        default:
            goto invalid_place;
        }
    } else if (strcmp(name, "body") == 0 && (ttype = TT_BODY)) {
        /*
         * Found <body> tag.
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_NEUTRAL:
            if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: found <%s> "
                              "before <html>", r->uri, info->lineno, name);
            }
            newtag = ctr_html_tag_new(curelm->tag, "html", TT_HTML,
                                      apr_table_make(pool, 1),
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_HTML, "html", newtag);
            /* FALL THROUGH */
        case ES_HTML:
            newtag = ctr_html_tag_new(curelm->tag, name, TT_BODY, attrs,
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_BODY, name, newtag);
            break;
        default:
            goto invalid_place;
        }
    } else if (strcmp(name, "c3:raw") == 0 && (ttype = TT_C3RAW)) {
        /*
         * Found <c3:raw> tag.
         */
        ctr_html_tag_t *newtag;

        switch (curelm->stat) {
        case ES_BODY:
        case ES_INLINEBODY:
        case ES_LIST:
        case ES_LISTITEM:
        case ES_DEFLIST:
        case ES_DEFTERM:
        case ES_DEFDESC:
            newtag = ctr_html_tag_new(curelm->tag, name, TT_C3RAW, attrs,
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_RAW, name, newtag);
            *mode = LM_IGNORETAG;
            *modeendtag = name;
            break;
        default:
            goto invalid_place;
        }
    } else {
        if (info->warnout) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                          "mod_cthree: %s line %d: found unknown tag <%s>, "
                          "ignore the tag", r->uri, info->lineno, name);
        }
    }

    return;

invalid_place:
    if (info->warnout) {
        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                      "mod_cthree: %s line %d: found <%s> at invalid place, "
                      "ignore the tag", r->uri, info->lineno, name);
    }

    if (ttype != TT_BR && ttype != TT_IMG && ttype != TT_INPUT &&
        ttype != TT_PARAM && ttype != TT_HR && ttype != TT_META &&
        ttype != TT_BASE)
        curelm = ctr_parse_elm_push(info, curelm->stat, name, curelm->tag);
}

static void ctr_html_end_found(ctr_parse_info_t *info, const char *name)
{
    const request_rec *r;

    r = info->r;

    if (strcmp(name, "br") == 0 ||
        strcmp(name, "img") == 0 ||
        strcmp(name, "input") == 0 ||
        strcmp(name, "param") == 0 ||
        strcmp(name, "hr") == 0 ||
        strcmp(name, "meta") == 0 ||
        strcmp(name, "base") == 0) {
        /*
         * These are empty tags, so just ignore closure tags.
         */
        return;
    } else if (strcmp(name, "!doctype") == 0)
        return;

    if (!ctr_parse_elm_pop(info, name)) {
        if (info->warnout) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                          "mod_cthree: %s line %d: found </%s> "
                          "at invalid place, ignore the tag",
                          r->uri, info->lineno, name);
        }
    }
}

static void ctr_html_chars_found(ctr_parse_info_t *info, char *chars)
{
    const request_rec *r;
    apr_pool_t *pool;
    ctr_parse_elm_t *curelm;
    ctr_html_tag_t *newtag;

    r = info->r;
    pool = r->pool;
    curelm = (ctr_parse_elm_t *)CTR_ARRAY_LAST(info->elms);

    switch (curelm->stat) {
    case ES_TITLE:
    case ES_STYLE:
    case ES_OPTION:
    case ES_TEXTAREA:
        chars = ctr_html_unescape(chars, pool);
        /* FALL THROUGH */
    case ES_PLAINTEXT:
    case ES_RAW:
        ctr_html_chars_new(curelm->tag, chars, pool);
        break;
    case ES_NEUTRAL:
    case ES_HTML:
    case ES_HEAD:
        if (ctr_isspace_str(chars)) {
            /*
             * If it only consists of white spaces, ignore it.
             */
            return;
        }

        if (curelm->stat == ES_NEUTRAL) {
            newtag = ctr_html_tag_new(curelm->tag, "html", TT_HTML,
                                      apr_table_make(pool, 1),
                                      info->lineno, pool);
            curelm = ctr_parse_elm_push(info, ES_HTML, "html", newtag);
        } else if (curelm->stat == ES_HEAD) {
            apr_array_pop(info->elms);
            curelm = (ctr_parse_elm_t *)CTR_ARRAY_LAST(info->elms);
        }

        newtag = ctr_html_tag_new(curelm->tag, "body", TT_BODY,
                                  apr_table_make(pool, 1), info->lineno, pool);
        curelm = ctr_parse_elm_push(info, ES_BODY, "body", newtag);
        if (info->warnout) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                          "mod_cthree: %s line %d: found text before <body>",
                          r->uri, info->lineno);
        }
        /* FALL THROUGH */
    case ES_BODY:
    case ES_INLINEBODY:
    case ES_LISTITEM:
    case ES_DEFTERM:
    case ES_DEFDESC:
        chars = ctr_html_text_unescape(chars, pool);
        ctr_html_chars_new(curelm->tag, chars, pool);
        break;
    default:
        if (!ctr_isspace_str(chars) && info->warnout) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                          "mod_cthree: %s line %d: found text "
                          "at invalid place, ignore it", r->uri, info->lineno);
        }
    }
}

static void ctr_html_cdata_found(ctr_parse_info_t *info, char *chars)
{
    const request_rec *r;
    apr_pool_t *pool;
    ctr_parse_elm_t *curelm;

    if (*chars == '\0')
        return;

    r = info->r;
    pool = r->pool;
    curelm = (ctr_parse_elm_t *)CTR_ARRAY_LAST(info->elms);

    switch (curelm->stat) {
    case ES_BODY:
    case ES_INLINEBODY:
    case ES_LISTITEM:
    case ES_DEFTERM:
    case ES_DEFDESC:
        ctr_html_chars_new(curelm->tag, chars, pool);
        break;
    default:
        if (info->warnout) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                          "mod_cthree: %s line %d: found cdata "
                          "at invalid place, ignore it", r->uri, info->lineno);
        }
    }
}

static ctr_html_tag_t *ctr_html_tag_new(ctr_html_tag_t *parent,
                                        const char *name,
                                        ctr_html_ttype_t ttype,
                                        const apr_table_t *attrs,
                                        int lineno,
                                        apr_pool_t *pool)
{
    ctr_html_tag_t *newtag;

    newtag = apr_palloc(pool, sizeof(*newtag));
    newtag->node.type = NT_ELEMENT;
    newtag->node.parent = parent;
    newtag->node.siblings = NULL;
    newtag->name = name;
    newtag->ttype = ttype;
    newtag->attrs = attrs;
    newtag->lineno = lineno;
    newtag->children = NULL;
    newtag->last_child = NULL;

    if (parent->last_child == NULL)
        parent->children = &newtag->node;
    else
        parent->last_child->siblings = &newtag->node;
    parent->last_child = &newtag->node;

    return newtag;
}

static void ctr_html_chars_new(ctr_html_tag_t *parent, char *s,
                               apr_pool_t *pool)
{
    ctr_html_chars_t *newchars;

    newchars = apr_palloc(pool, sizeof(*newchars));
    newchars->node.type = NT_CHARS;
    newchars->node.parent = parent;
    newchars->node.siblings = NULL;
    newchars->s = s;

    if (parent->last_child == NULL)
        parent->children = &newchars->node;
    else
        parent->last_child->siblings = &newchars->node;
    parent->last_child = &newchars->node;
}

static ctr_parse_elm_t *ctr_parse_elm_push(ctr_parse_info_t *info,
                                           ctr_parse_stat_t stat,
                                           const char *tagname,
                                           ctr_html_tag_t *tag)
{
    ctr_parse_elm_t *newelm;

    newelm = apr_array_push(info->elms);
    newelm->stat = stat;
    newelm->tagname = tagname;
    newelm->tag = tag;

    return newelm;
}

static int ctr_parse_elm_pop(ctr_parse_info_t *info, const char *name)
{
    ctr_parse_elm_t *elm;
    int max, i, j;

    elm = (ctr_parse_elm_t *)CTR_ARRAY_LAST(info->elms);
    max = info->elms->nelts;
    for (i = 0; i < max; i++) {
        if (strcmp(elm->tagname, name) == 0) {
            /*
             * Found the tag.
             */
            for (j = 0; j <= i; j++) {
                elm = apr_array_pop(info->elms);
                if (elm->tag->ttype == TT_A)
                    info->ancnest--;
                else if (elm->tag->ttype == TT_OBJECT) {
                    if (info->validobj == info->objnest)
                        info->validobj = 0;
                    info->objnest--;
                } else if (elm->tag->ttype == TT_FORM) {
                    if (info->validfrm == info->frmnest)
                        info->validfrm = 0;
                    info->frmnest--;
                }
            }

            return TRUE;
        }

        elm--;
    }

    return FALSE;
}

static void ctr_html_node_walk(ctr_nodewalk_info_t *info,
                               const ctr_html_tag_t *tag)
{
    const ctr_html_node_t *node;

    ctr_html_start_handler(info, tag);

    for (node = tag->children; node != NULL; node = node->siblings) {
        switch (node->type) {
        case NT_ELEMENT:
            ctr_html_node_walk(info, (ctr_html_tag_t *)node);
            break;
        case NT_CHARS:
            ctr_html_chars_handler(info, (ctr_html_chars_t *)node);
        }
    }

    ctr_html_end_handler(info, tag);
}

static void ctr_html_start_handler(ctr_nodewalk_info_t *info,
                                   const ctr_html_tag_t *tag)
{
#define CTR_ATTRGET_RAW(_attrname)                                      \
    (ctr_attrget_raw(tag->attrs, (_attrname)))
#define CTR_ATTRGET_SYM(_attrname)                                      \
    (ctr_attrget_sym(tag->attrs, (_attrname), pool))
#define CTR_ATTRGET_SYMSTABLE(_attrname)                                \
    (ctr_attrget_symstable(tag->attrs, (_attrname), pool))
#define CTR_ATTRGET_PINT(_attrname, _dflt)                              \
    (ctr_attrget_pint(tag->attrs, (_attrname), (_dflt), tag,            \
                      info->warnout, r))
#define CTR_ATTRGET_PINT2(_attrname, _dflt)                             \
    (ctr_attrget_pint2(tag->attrs, (_attrname), (_dflt), tag,           \
                       info->warnout, r))
#define CTR_ATTRGET_FLG(_attrname)                                      \
    (ctr_attrget_flg(tag->attrs, (_attrname)))
#define CTR_ATTRGET_COLOR(_attrname)                                    \
    (ctr_attrget_color(tag->attrs, (_attrname), tag, info->warnout, r))
#define CTR_ATTRGET_SYMTOVAL(_attrname, _symmap, _dflt)                 \
    (ctr_attrget_symtoval(tag->attrs, (_attrname), (_symmap),           \
                          (_dflt), tag, info->warnout, r))
#define CTR_ATTRGET_SYMSTOORVAL(_attrname, _symmap, _dflt)              \
    (ctr_attrget_symstoorval(tag->attrs, (_attrname), (_symmap),        \
                             (_dflt), tag, info->warnout, r))

    const request_rec *r;
    apr_pool_t *pool;
    ctr_nodewalk_elm_t *curelm;
    int hdgtype;

    r = info->r;
    pool = r->pool;
    curelm = (ctr_nodewalk_elm_t *)CTR_ARRAY_LAST(info->elms);

    if (tag->ttype == TT_BR) {
        /*
         * <br> tag.
         */
        CTR_CLASS_NAME(newline) *nl;

        nl = ctr_newline_new(curelm->cont);
        CTR_CHILD_INSERT_LAST(curelm->cont, nl);
    } else if (tag->ttype == TT_IMG) {
        /*
         * <img> tag.
         */
        static const ctr_symmap_t convmode_map[] = {
            { "none", CV_NONE }, { "normal", CV_NORMAL },
            { "wallpaper", CV_WALLPAPER }, { "kp", CV_KP }, { NULL, 0 }
        };
        const char *rawsrc;
        apr_uri_t *src;
        const char *alt;
        int store;
        CTR_CLASS_NAME(image) *img;

        ctr_pbr_insert(info);

        if ((rawsrc = CTR_ATTRGET_SYM("src")) == NULL || *rawsrc == '\0')
            src = NULL;
        else {
            if ((src = ctr_uri_absolute_convert(&info->base, rawsrc,
                                                CTR_ATTRGET_SYM("scheme"),
                                                CTR_ATTRGET_PINT("port", -1),
                                                r)) == NULL ||
                ctr_startwith(src->path, "mailto:") ||
                ctr_startwith(src->path, "tel:")) {
                if (info->warnout) {
                    ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                                  "mod_cthree: %s line %d: invalid src [%s] "
                                  "in <%s>, ignore it",
                                  r->uri, tag->lineno, rawsrc, tag->name);
                }
            }
        }

        alt = CTR_ATTRGET_RAW("alt");
        if (alt != NULL)
            alt = ctr_unite_spaces(apr_pstrdup(pool, alt));

        if ((store = CTR_ATTRGET_SYMTOVAL("store", store_map, -1)) == -1)
            store = CTR_ATTRGET_SYMTOVAL("copyright", copyright_map,
                                         info->imgstore ? TRUE : FALSE);

        img = ctr_image_new(curelm->cont, src, alt,
                            CTR_ATTRGET_SYMTOVAL("convmode", convmode_map,
                                                 info->imgconv ?
                                                 CV_NORMAL : CV_NONE),
                            CTR_ATTRGET_PINT2("percent", info->imgpercent),
                            CTR_ATTRGET_SYMTOVAL("copy", copy_map,
                                                 info->imgcopy ?
                                                 TRUE : FALSE),
                            store,
                            CTR_ATTRGET_PINT("mainframe", 1));;
        CTR_CHILD_INSERT_LAST(curelm->cont, img);
    } else if (tag->ttype == TT_INPUT) {
        /*
         * <input> tag.
         */
        const char *s;
        char *type;
        const char *name;
        const char *val;
        apr_int64_t size;
        apr_int64_t max;
        ctr_edit_style_t istyle;
        int chk;

        istyle = ST_NONE;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);
            istyle = ctr_style_inpfmt_get(styles, tag, info->warnout, r);
        }

        if ((type = CTR_ATTRGET_SYM("type")) != NULL)
            type = ctr_tolower_str(type);
        else {
            /*
             * If type is not specified, we assume that type is text.
             */
            type = apr_pstrdup(pool, "text");
        }
        name = CTR_ATTRGET_RAW("name");
        val = CTR_ATTRGET_RAW("value");
        size = CTR_ATTRGET_PINT("size", -1);
        max = CTR_ATTRGET_PINT("maxlength", -1);
        if (istyle == ST_NONE) {
            istyle = CTR_ATTRGET_SYMTOVAL("istyle", istyle_map, ST_NONE);
        }
        chk = CTR_ATTRGET_FLG("checked");

        if (istyle == ST_NONE)
            istyle = ST_FULLKANA;

        /*
         * Check the type value, and create an appropriate object.
         */
        if (strcmp(type, "text") == 0) {
            CTR_CLASS_NAME(editbox) *ebox;

            ctr_pbr_insert(info);
            ebox = ctr_editbox_new(curelm->cont, name, val, size, max, istyle);
            CTR_CHILD_INSERT_LAST(curelm->cont, ebox);
        } else if (strcmp(type, "password") == 0) {
            CTR_CLASS_NAME(passwd) *pwd;

            ctr_pbr_insert(info);
            pwd = ctr_passwd_new(curelm->cont, name, val, size, max);
            CTR_CHILD_INSERT_LAST(curelm->cont, pwd);
        } else if (strcmp(type, "radio") == 0) {
            ctr_radio_info_t *rinfo;
            int no;
            CTR_CLASS_NAME(radio) *rdo;

            ctr_pbr_insert(info);

            if (name != NULL) {
                apr_hash_t *rdohash;

                rdohash = info->curfrm->rdohash;
                rinfo = CTR_HASH_GET(rdohash, name);

                if (rinfo == NULL) {
                    /*
                     * Radio info has not been created yet.  So create here and
                     * set it into hash table.
                     */
                    rinfo = apr_palloc(pool, sizeof(*rinfo));
                    rinfo->siblings = NULL;
                    rinfo->cnt = 0;
                    no = rinfo->cnt++;
                    if (chk)
                        rinfo->chk = no;
                    else
                        rinfo->chk = -1;
                    CTR_HASH_SET(rdohash, name, rinfo);
                } else {
                    no = rinfo->cnt++;
                    if (chk && rinfo->chk == -1)
                        rinfo->chk = no;
                }
            } else {
                rinfo = NULL;
                no = -1;
            }

            rdo = ctr_radio_new(curelm->cont, name, val, chk, rinfo, no);
            CTR_CHILD_INSERT_LAST(curelm->cont, rdo);
        } else if (strcmp(type, "checkbox") == 0) {
            CTR_CLASS_NAME(chkbox) *cbox;

            ctr_pbr_insert(info);
            cbox = ctr_chkbox_new(curelm->cont, name, val, chk);
            CTR_CHILD_INSERT_LAST(curelm->cont, cbox);
        } else if (strcmp(type, "hidden") == 0) {
            CTR_CLASS_NAME(hidden) *hdn;

            hdn = ctr_hidden_new(curelm->cont, name, val);
            CTR_CHILD_INSERT_LAST(curelm->cont, hdn);
        } else if (strcmp(type, "submit") == 0) {
            CTR_CLASS_NAME(submit) *smt;

            ctr_pbr_insert(info);
            smt = ctr_submit_new(curelm->cont, name, val);
            CTR_CHILD_INSERT_LAST(curelm->cont, smt);
        } else if (strcmp(type, "reset") == 0) {
            CTR_CLASS_NAME(reset) *rst;

            ctr_pbr_insert(info);
            rst = ctr_reset_new(curelm->cont, name, val);
            CTR_CHILD_INSERT_LAST(curelm->cont, rst);
        } else if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: invalid type [%s] "
                              "in <%s>, ignore the tag",
                              r->uri, tag->lineno, type, name);
        }
    } else if (tag->ttype == TT_A) {
        /*
         * <a> tag.
         */
        const char *s;
        static const ctr_symmap_t akey_map[] = {
            { "0", AC_ZERO }, { "1", AC_ONE }, { "2", AC_TWO },
            { "3", AC_THREE }, { "4", AC_FOUR }, { "5", AC_FIVE },
            { "6", AC_SIX }, { "7", AC_SEVEN }, { "8", AC_EIGHT },
            { "9", AC_NINE }, { "*", AC_ASTA }, { "#", AC_SHARP }
        };
        apr_uri_t *href;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(anchor) *anc;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);
            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }
        }

        href = ctr_uri_absolute_convert(&info->base,
                                        CTR_ATTRGET_SYM("href"),
                                        CTR_ATTRGET_SYM("scheme"),
                                        CTR_ATTRGET_PINT("port", -1), r);

        anc = ctr_anchor_new(curelm->cont,
                             CTR_ATTRGET_RAW("name"),
                             href,
                             CTR_ATTRGET_SYMTOVAL("accesskey",
                                                  akey_map, AC_NONE),
                             CTR_ATTRGET_FLG("utn"));
        CTR_CHILD_INSERT_LAST(curelm->cont, anc);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(anc),
                                       curelm->fsize);
    } else if (tag->ttype == TT_SPAN) {
        /*
         * <span> tag.
         */
        const char *s;
        CTR_CLASS_NAME(deco) *dec;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);
            dec = ctr_gen_deco_get(styles, tag, curelm, info->warnout, r);
        }

        if (dec == NULL) {
            dec = ctr_deco_new(curelm->cont, -1, -1, -1, FS_NONE, FALSE);
            CTR_CHILD_INSERT_LAST(curelm->cont, dec);
        }

        curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                       CTR_WCONTAINER_CAST(dec),
                                       dec->fsize != FS_NONE ?
                                       dec->fsize : curelm->fsize);
    } else if (tag->ttype == TT_FONT) {
        /*
         * <font> tag.
         */
        const char *s;
        static const ctr_symmap_t fontsize_map[] = {
            { "1", FS_XSMALL }, { "2", FS_SMALL }, { "3", FS_MEDIUM },
            { "4", FS_LARGE }, { "5", FS_XLARGE }, { "6", FS_XXLARGE },
            { "7", FS_XXLARGE }, { "+1", FS_LARGE }, { "+2", FS_XLARGE },
            { "+3", FS_XXLARGE }, { "+4", FS_XXLARGE }, { "-1", FS_SMALL },
            { "-2", FS_XSMALL }, { NULL, 0 }
        };
        CTR_CLASS_NAME(deco) *dec;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);
            dec = ctr_gen_deco_get(styles, tag, curelm, info->warnout, r);
        }

        if (dec != NULL) {
            if (dec->txtcolor == -1)
                dec->txtcolor = CTR_ATTRGET_COLOR("color");
            if (dec->txtcolor2 == -1)
                dec->txtcolor2 = CTR_ATTRGET_COLOR("color2");
            if (dec->fsize == FS_NONE)
                dec->fsize = CTR_ATTRGET_SYMTOVAL("size",
                                                  fontsize_map, FS_NONE);
        } else {
            dec = ctr_deco_new(curelm->cont,
                               CTR_ATTRGET_COLOR("color"),
                               CTR_ATTRGET_COLOR("color2"),
                               -1,
                               CTR_ATTRGET_SYMTOVAL("size",
                                                    fontsize_map, FS_NONE),
                               FALSE);
            CTR_CHILD_INSERT_LAST(curelm->cont, dec);
        }

        curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                       CTR_WCONTAINER_CAST(dec),
                                       dec->fsize != FS_NONE ?
                                       dec->fsize : curelm->fsize);
    } else if (tag->ttype == TT_BLINK) {
        /*
         * <blink> tag.
         */
        const char *s;
        CTR_CLASS_NAME(deco) *dec;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);
            dec = ctr_gen_deco_get(styles, tag, curelm, info->warnout, r);
        }

        if (dec != NULL)
            dec->blink = TRUE;
        else {
            dec = ctr_deco_new(curelm->cont, -1, -1, -1, FS_NONE, TRUE);
            CTR_CHILD_INSERT_LAST(curelm->cont, dec);
        }

        curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                       CTR_WCONTAINER_CAST(dec),
                                       dec->fsize != FS_NONE ?
                                       dec->fsize : curelm->fsize);
    } else if (tag->ttype == TT_OBJECT) {
        /*
         * <object> tag.
         */
        apr_uri_t *data;
        char *width;
        char *height;
        int store;
        CTR_CLASS_NAME(flash) *fla;

        data = ctr_uri_absolute_convert(&info->base,
                                        CTR_ATTRGET_SYM("data"),
                                        CTR_ATTRGET_SYM("scheme"),
                                        CTR_ATTRGET_PINT("port", -1), r);
        width = ctr_attrget_lensize(tag->attrs, "width",
                                    tag, info->warnout, r);
        height = ctr_attrget_lensize(tag->attrs, "height",
                                    tag, info->warnout, r);

        if ((store = CTR_ATTRGET_SYMTOVAL("store", store_map, -1)) == -1)
            store = CTR_ATTRGET_SYMTOVAL("copyright", copyright_map,
                                         info->flashstore ? TRUE : FALSE);

        fla = ctr_flash_new(curelm->cont,
                            data, width, height, -1, FALSE, QL_HIGH,
                            CTR_ATTRGET_SYMTOVAL("copy", copy_map,
                                                 info->flashcopy ?
                                                 TRUE : FALSE),
                            store);
        CTR_CHILD_INSERT_LAST(curelm->cont, fla);
        info->curfla = fla;
        curelm = ctr_nodewalk_elm_push(info,
                                       tag->ttype,
                                       CTR_WCONTAINER_CAST(fla),
                                       curelm->fsize);
    } else if (tag->ttype == TT_PARAM) {
        /*
         * <param> tag.
         */
        char *name;

        name = CTR_ATTRGET_SYM("name");
        if (name != NULL) {
            name = ctr_tolower_str(name);
            if (strcmp(name, "bgcolor") == 0)
                info->curfla->bgcolor = CTR_ATTRGET_COLOR("value");
            else if (strcmp(name, "loop") == 0) {
                static const ctr_symmap_t loop_map[] = {
                    { "on", TRUE }, { "off", FALSE }, { NULL, 0 }
                };

                info->curfla->loop = CTR_ATTRGET_SYMTOVAL("value", loop_map,
                                                          FALSE);
            } else if (strcmp(name, "quality") == 0) {
                static const ctr_symmap_t quality_map[] = {
                    { "high", QL_HIGH }, { "medium", QL_MEDIUM },
                    { "low", QL_LOW }, { NULL, 0 }
                };

                info->curfla->quality = CTR_ATTRGET_SYMTOVAL("value",
                                                             quality_map,
                                                             QL_HIGH);
            } else if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: invalid name [%s] "
                              "in <param>, ignore the tag",
                              r->uri, tag->lineno, name);
            }
        }
    } else if (tag->ttype == TT_SELECT) {
        /*
         * <select> tag.
         */
        const char *name;
        const char *selected;
        CTR_CLASS_NAME(select) *sel;

        ctr_pbr_insert(info);

        name = CTR_ATTRGET_RAW("name");
        selected = CTR_ATTRGET_RAW("selected");

        sel = ctr_select_new(curelm->cont, name, selected);
        CTR_CHILD_INSERT_LAST(curelm->cont, sel);
        curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                       CTR_WCONTAINER_CAST(sel),
                                       curelm->fsize);
        info->cursel = sel;
    } else if (tag->ttype == TT_TEXTAREA) {
        /*
         * <textarea> tag.
         */
        const char *s;
        const char *name;
        ctr_edit_style_t istyle;
        CTR_CLASS_NAME(textarea) *txa;

        ctr_pbr_insert(info);

        istyle = ST_NONE;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);
            istyle = ctr_style_inpfmt_get(styles, tag, info->warnout, r);
        }

        name = CTR_ATTRGET_RAW("name");
        if (istyle == ST_NONE)
            istyle = CTR_ATTRGET_SYMTOVAL("istyle", istyle_map, ST_NONE);

        if (istyle == ST_NONE)
            istyle = ST_FULLKANA;

        txa = ctr_textarea_new(curelm->cont, name,
                               CTR_ATTRGET_PINT("rows", -1),
                               CTR_ATTRGET_PINT("cols", -1),
                               istyle, NULL);
        CTR_CHILD_INSERT_LAST(curelm->cont, txa);
        info->curtxa = txa;
    } else if (tag->ttype == TT_PLAINTEXT) {
        /*
         * <plaintext> tag.
         */
        CTR_CLASS_NAME(block) *blk;

        blk = ctr_block_new(curelm->cont, AL_NONE,
                            FALSE, DL_LEFT, BH_SCROLL, 0);
        CTR_CHILD_INSERT_LAST(curelm->cont, blk);
        curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                       CTR_WCONTAINER_CAST(blk),
                                       curelm->fsize);
        info->prenest++;
    } else if (tag->ttype == TT_C3IF || tag->ttype == TT_C3UNLESS) {
        /*
         * <c3:if> or <c3:unless> tag.
         */
        static const ctr_symmap_t lang_map[] = {
            { "ihtml", LG_IHTML }, { "ixhtml", LG_IXHTML },
            { "auxhtml", LG_AUXHTML }, { "shtml", LG_SHTML },
            { "vhtml", LG_SHTML }, { "sxhtml", LG_SXHTML },
            { "vxhtml", LG_SXHTML }, { "hdml", LG_HDML },
            { "whtml", LG_WHTML }, { "full", LG_FULL },
            { NULL, 0 }
        };
        static const ctr_symmap_t carrier_map[] = {
            { "docomo", CA_DOCOMO }, { "kddi", CA_KDDI },
            { "vodafone", CA_SOFTBANK }, { "softbank", CA_SOFTBANK },
            { "willcom", CA_WILLCOM }, { "unknown", CA_UNKNOWN }, { NULL, 0 }
        };
        static const ctr_symmap_t flag_map[] = {
            { "true", 1 }, { "false", 0 }, { NULL, 0 }
        };
        int isxml;
        int isxhtml;
        CTR_CLASS_NAME(c3if) *c3if;

        isxml = CTR_ATTRGET_SYMTOVAL("isxml", flag_map, -1);
        isxhtml = CTR_ATTRGET_SYMTOVAL("isxhtml", flag_map, -1);
        if (isxml != -1 && isxhtml != -1) {
            if (info->warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: both isxml and isxhtml "
                              "is set in <%s>, ignore them",
                              r->uri, tag->lineno, tag->name);
            }
            isxml = -1;
        } else if (isxhtml != -1)
            isxml = isxhtml;

        c3if = ctr_c3if_new(curelm->cont,
                            CTR_ATTRGET_SYMSTOORVAL("lang", lang_map, LG_ALL),
                            CTR_ATTRGET_SYMSTOORVAL("carrier", carrier_map,
                                                    CA_ALL),
                            CTR_ATTRGET_SYMSTABLE("name"),
                            CTR_ATTRGET_SYMSTABLE("group"),
                            CTR_ATTRGET_SYMTOVAL("isfoma", flag_map, -1),
                            CTR_ATTRGET_SYMTOVAL("istuka", flag_map, -1),
                            isxml,
                            tag->ttype == TT_C3UNLESS);
        CTR_CHILD_INSERT_LAST(curelm->cont, c3if);
        curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                       CTR_WCONTAINER_CAST(c3if),
                                       curelm->fsize);
    } else if (tag->ttype == TT_OPTION) {
        /*
         * <option> tag.
         */
        const char *val;
        int selected;
        CTR_CLASS_NAME(option) *opt;

        val = CTR_ATTRGET_RAW("value");
        if (val != NULL && info->cursel != NULL &&
            info->cursel->selected != NULL &&
            strcmp(val, info->cursel->selected) == 0)
            selected = TRUE;
        else
            selected = CTR_ATTRGET_FLG("selected");

        opt = ctr_option_new(curelm->cont, val, selected, "");
        CTR_CHILD_INSERT_LAST(curelm->cont, opt);
        info->curopt = opt;
    } else if (tag->ttype == TT_HR) {
        /*
         * <hr> tag.
         */
        const char *s;
        ctr_halign_t align;
        int size;
        char *width;
        int color;
        CTR_CLASS_NAME(hrline) *hr;

        ctr_pbr_insert(info);

        size = -1;
        width = NULL;
        color = -1;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);
            size = ctr_style_height_get(styles, tag, info->warnout, r);
            width = ctr_style_width_get(styles, tag, info->warnout, r);
            color = ctr_style_bgcolor_get(styles, tag, info->warnout, r);
        }

        align = CTR_ATTRGET_SYMTOVAL("align", halign_map, AL_CENTER);
        if (size == -1)
            size = CTR_ATTRGET_PINT("size", -1);
        if (color == -1)
            color = CTR_ATTRGET_COLOR("color");
        if (color == -1) {
            /* default color is gray */
            color = 0x808080;
        }

        /*
         * Process width attribute.
         */
        if (width == NULL)
            width = ctr_attrget_lensize(tag->attrs, "width",
                                        tag, info->warnout, r);

        hr = ctr_hrline_new(curelm->cont, align, size, width, color);
        CTR_CHILD_INSERT_LAST(curelm->cont, hr);
    } else if (tag->ttype == TT_CENTER) {
        /*
         * <center> tag.
         */
        const char *s;
        ctr_halign_t align;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(block) *blk;

        align = AL_NONE;
        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }

            align = ctr_style_txtalign_get(styles, tag, info->warnout, r);
        }

        if (align == AL_NONE)
            align = AL_CENTER;

        blk = ctr_block_new(curelm->cont, align,
                            FALSE, DL_LEFT, BH_SCROLL, 0);
        CTR_CHILD_INSERT_LAST(curelm->cont, blk);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(blk),
                                       curelm->fsize);
    } else if (tag->ttype == TT_MARQUEE) {
        /*
         * <marquee> tag.
         */
        const char *s;
        static const ctr_symmap_t direct_map[] = {
            { "left", DL_LEFT }, { "right", DL_RIGHT }, { NULL, 0 }
        };
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(block) *blk;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);
            dec = ctr_gen_deco_get(styles, tag, curelm, info->warnout, r);
        }

        if (dec != NULL) {
            if (dec->bgcolor == -1)
                dec->bgcolor = CTR_ATTRGET_COLOR("bgcolor");
        } else {
            dec = ctr_deco_new(curelm->cont,
                               -1,
                               -1,
                               CTR_ATTRGET_COLOR("bgcolor"),
                               FS_NONE,
                               FALSE);
            CTR_CHILD_INSERT_LAST(curelm->cont, dec);
        }

        curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                       CTR_WCONTAINER_CAST(dec),
                                       dec->fsize != FS_NONE ?
                                       dec->fsize : curelm->fsize);

        blk = ctr_block_new(curelm->cont, AL_NONE, TRUE,
                            CTR_ATTRGET_SYMTOVAL("direction",
                                                 direct_map, DL_LEFT),
                            CTR_ATTRGET_SYMTOVAL("behavior",
                                                 mqebehav_map, BH_SCROLL),
                            CTR_ATTRGET_PINT("loop", 1));
        CTR_CHILD_INSERT_LAST(curelm->cont, blk);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(blk),
                                       curelm->fsize);
    } else if (tag->ttype == TT_FORM) {
        /*
         * <form> tag.
         */
        const char *s;
        static const ctr_symmap_t mtd_map[] = {
            { "get", FM_GET }, { "post", FM_POST }, { NULL, 0 }
        };
        ctr_halign_t align;
        apr_uri_t *action;
        char *query;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(form) *frm;

        align = AL_NONE;
        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }

            align = ctr_style_txtalign_get(styles, tag, info->warnout, r);
        }

        action = ctr_uri_absolute_convert(&info->base,
                                          CTR_ATTRGET_SYM("action"),
                                          CTR_ATTRGET_SYM("scheme"),
                                          CTR_ATTRGET_PINT("port", -1), r);
        if (action->query != NULL && strcasecmp(action->query, "guid=ON")) {
            query = action->query;
            action->query = NULL;
        } else
            query = NULL;

        frm = ctr_form_new(curelm->cont,
                           action,
                           CTR_ATTRGET_SYMTOVAL("method", mtd_map, FM_GET),
                           CTR_ATTRGET_FLG("utn"), align);
        CTR_CHILD_INSERT_LAST(curelm->cont, frm);
        info->curfrm = frm;
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(frm),
                                       curelm->fsize);

        /*
         * Convert the query string in the "action" attribute into hidden
         * values.
         */
        if (query != NULL) {
            char *tkn, *last;

            for (tkn = apr_strtok(query, "&", &last); tkn != NULL;
                 tkn = apr_strtok(NULL, "&", &last)) {
                char *key, *val, *last2;
                CTR_CLASS_NAME(hidden) *hdn;

                key = apr_strtok(tkn, "=", &last2);
                val = apr_strtok(NULL, "=", &last2);
                hdn = ctr_hidden_new(curelm->cont, key, val);
                CTR_CHILD_INSERT_LAST(curelm->cont, hdn);
            }
        }
    } else if (tag->ttype == TT_DIV || tag->ttype == TT_P) {
        /*
         * <div> or <p> tag.
         */
        const char *s;
        ctr_halign_t align;
        int marquee;
        ctr_direct_t mqedirect;
        ctr_behav_t mqebehav;
        int mqeloop;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(block) *blk;

        align = AL_NONE;
        marquee = FALSE;
        mqedirect = DL_LEFT;
        mqebehav = BH_SCROLL;
        mqeloop = 0;
        dec = NULL;

        if (tag->ttype == TT_P && info->pparent != NULL) {
            /*
             * Insert a brank line to emulate <p>.
             */
            CTR_CLASS_NAME(brank) *brk;

            brk = ctr_brank_new(info->pparent);

            if (curelm->cont == info->pparent)
                CTR_CHILD_INSERT_LAST(info->pparent, brk);
            else
                CTR_CHILD_INSERT_BEFORE(info->pparent->last_child, brk);

            info->needpbr = FALSE;
        }

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }

            align = ctr_style_txtalign_get(styles, tag, info->warnout, r);
            marquee = ctr_style_marquee_get(styles, tag, info->warnout, r);
            mqedirect = ctr_style_mqedirect_get(styles, tag, info->warnout, r);
            mqebehav =  ctr_style_mqebehav_get(styles, tag, info->warnout, r);
            mqeloop = ctr_style_mqeloop_get(styles, tag, info->warnout, r);
        }

        if (align == AL_NONE)
            align = CTR_ATTRGET_SYMTOVAL("align", halign_map, AL_NONE);

        blk = ctr_block_new(curelm->cont, align,
                            marquee, mqedirect, mqebehav, mqeloop);
        CTR_CHILD_INSERT_LAST(curelm->cont, blk);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(blk),
                                       curelm->fsize);
    } else if (tag->ttype == TT_PRE) {
        /*
         * <pre> tag.
         */
        const char *s;
        ctr_halign_t align;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(block) *blk;

        align = AL_NONE;
        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }

            align = ctr_style_txtalign_get(styles, tag, info->warnout, r);
        }

        blk = ctr_block_new(curelm->cont, align,
                            FALSE, DL_LEFT, BH_SCROLL, 0);
        CTR_CHILD_INSERT_LAST(curelm->cont, blk);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(blk),
                                       curelm->fsize);
        info->prenest++;
    } else if ((tag->ttype == TT_H1 && (hdgtype = 1)) ||
               (tag->ttype == TT_H2 && (hdgtype = 2)) ||
               (tag->ttype == TT_H3 && (hdgtype = 3)) ||
               (tag->ttype == TT_H4 && (hdgtype = 4)) ||
               (tag->ttype == TT_H5 && (hdgtype = 5)) ||
               (tag->ttype == TT_H6 && (hdgtype = 6))) {
        /*
         * <h1>, <h2>, <h3>, <h4>, <h5> or <h6> tag.
         */
        const char *s;
        apr_table_t *styles;
        ctr_halign_t align;
        CTR_CLASS_NAME(deco) *dec1;
        CTR_CLASS_NAME(heading) *hdg;

        styles = NULL;
        align = AL_NONE;
        dec1 = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            int bgcolor;

            styles = ctr_style_split(s, pool);

            if ((bgcolor = ctr_style_bgcolor_get(styles, tag,
                                                 info->warnout, r)) != -1) {
                dec1 = ctr_deco_new(curelm->cont, -1, -1,
                                    bgcolor, FS_NONE, FALSE);
                CTR_CHILD_INSERT_LAST(curelm->cont, dec1);
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec1),
                                               curelm->fsize);
            }

            align = ctr_style_txtalign_get(styles, tag, info->warnout, r);
        }

        if (align == AL_NONE)
            align = CTR_ATTRGET_SYMTOVAL("align", halign_map, AL_NONE);

        hdg = ctr_heading_new(curelm->cont, hdgtype, align);
        CTR_CHILD_INSERT_LAST(curelm->cont, hdg);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec1 != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(hdg),
                                       curelm->fsize);

        if (styles != NULL) {
            int txtcolor;
            int txtcolor2;
            ctr_fontsize_t fsize;
            int blink;

            txtcolor = ctr_style_txtcolor_get(styles, tag, info->warnout, r);
            txtcolor2 = ctr_style_txtcolor2_get(styles, tag, info->warnout, r);
            fsize = ctr_style_fontsize_get(styles, tag, curelm->fsize,
                                           info->warnout, r);
            blink = ctr_style_blink_get(styles, tag, info->warnout, r);

            if (txtcolor != -1 || txtcolor2 != -1 ||
                fsize != FS_NONE || blink) {
                CTR_CLASS_NAME(deco) *dec2;

                dec2 = ctr_deco_new(curelm->cont, txtcolor, txtcolor2,
                                    -1, fsize, blink);
                CTR_CHILD_INSERT_LAST(curelm->cont, dec2);
                curelm = ctr_nodewalk_elm_push(info, TT_NONE,
                                               CTR_WCONTAINER_CAST(dec2),
                                               fsize != FS_NONE ?
                                               fsize : curelm->fsize);
            }
        }
    } else if (tag->ttype == TT_UL || tag->ttype == TT_OL) {
        /*
         * <ul> or <ol> tag.
         */
        const char *s;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(list) *lst;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }
        }

        lst = ctr_list_new(curelm->cont, tag->ttype == TT_OL);
        CTR_CHILD_INSERT_LAST(curelm->cont, lst);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(lst),
                                       curelm->fsize);
    } else if (tag->ttype == TT_LI) {
        /*
         * <li> tag.
         */
        const char *s;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(listitem) *li;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }
        }

        li = ctr_listitem_new(curelm->cont);
        CTR_CHILD_INSERT_LAST(curelm->cont, li);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(li), curelm->fsize);
    } else if (tag->ttype == TT_DL) {
        /*
         * <dl> tag.
         */
        const char *s;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(deflist) *dlt;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }
        }

        dlt = ctr_deflist_new(curelm->cont);
        CTR_CHILD_INSERT_LAST(curelm->cont, dlt);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(dlt),
                                       curelm->fsize);
    } else if (tag->ttype == TT_DT) {
        /*
         * <dt> tag.
         */
        const char *s;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(defterm) *dtm;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }
        }

        dtm = ctr_defterm_new(curelm->cont);
        CTR_CHILD_INSERT_LAST(curelm->cont, dtm);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(dtm),
                                       curelm->fsize);
    } else if (tag->ttype == TT_DD) {
        /*
         * <dd> tag.
         */
        const char *s;
        CTR_CLASS_NAME(deco) *dec;
        CTR_CLASS_NAME(defdesc) *ddc;

        dec = NULL;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            if ((dec = ctr_gen_deco_get(styles, tag, curelm,
                                        info->warnout, r)) != NULL) {
                curelm = ctr_nodewalk_elm_push(info, tag->ttype,
                                               CTR_WCONTAINER_CAST(dec),
                                               dec->fsize != FS_NONE ?
                                               dec->fsize : curelm->fsize);
            }
        }

        ddc = ctr_defdesc_new(curelm->cont);
        CTR_CHILD_INSERT_LAST(curelm->cont, ddc);
        curelm = ctr_nodewalk_elm_push(info,
                                       dec != NULL ? TT_NONE : tag->ttype,
                                       CTR_WCONTAINER_CAST(ddc),
                                       curelm->fsize);
    } else if (tag->ttype == TT_BASE) {
        /*
         * <base> tag.
         */
        const char *href;
        const char *scheme;
        int port;
        apr_uri_t *hrefuri;

        href = CTR_ATTRGET_SYM("href");
        scheme = CTR_ATTRGET_SYM("scheme");
        port = CTR_ATTRGET_PINT("port", -1);

        if (href != NULL || scheme != NULL || port != -1) {
            if ((hrefuri = ctr_uri_absolute_convert(&r->parsed_uri,
                                                    href, scheme, port,
                                                    r)) == NULL ||
                ctr_startwith(hrefuri->path, "mailto:") ||
                ctr_startwith(hrefuri->path, "tel:")) {
                if (info->warnout) {
                    ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                                  "mod_cthree: %s line %d: invalid href [%s] "
                                  "in <%s>, ignore it",
                                  r->uri, tag->lineno, href, tag->name);
                }
            } else
                info->base = *hrefuri;
        }
    } else if (tag->ttype == TT_BODY) {
        /*
         * <body> tag.
         */
        const char *s;
        ctr_halign_t align;
        ctr_fontsize_t fsize;
        int blink;
        CTR_CLASS_NAME(deco) *dec;

        align = AL_NONE;
        fsize = FS_NONE;
        blink = FALSE;

        if ((s = apr_table_get(tag->attrs, "style")) != NULL) {
            apr_table_t *styles;

            styles = ctr_style_split(s, pool);

            info->page->bgcolor = ctr_style_bgcolor_get(styles, tag,
                                                        info->warnout, r);
            info->page->txtcolor = ctr_style_txtcolor_get(styles, tag,
                                                          info->warnout, r);
            info->page->txtcolor2 = ctr_style_txtcolor2_get(styles, tag,
                                                            info->warnout, r);

            align = ctr_style_txtalign_get(styles, tag, info->warnout, r);
            fsize = ctr_style_fontsize_get(styles, tag, curelm->fsize,
                                           info->warnout, r);
            blink = ctr_style_blink_get(styles, tag, info->warnout, r);
        }

        if (info->page->bgcolor == -1)
            info->page->bgcolor = CTR_ATTRGET_COLOR("bgcolor");
        if (info->page->txtcolor == -1)
            info->page->txtcolor = CTR_ATTRGET_COLOR("text");
        if (info->page->txtcolor2 == -1)
            info->page->txtcolor2 = CTR_ATTRGET_COLOR("text2");
        info->page->lnkcolor = CTR_ATTRGET_COLOR("link");
        info->page->vlnkcolor = CTR_ATTRGET_COLOR("vlink");

        if (fsize != FS_NONE || blink) {
            dec = ctr_deco_new(curelm->cont, -1, -1, -1, fsize, blink);
            CTR_CHILD_INSERT_LAST(curelm->cont, dec);
            curelm = ctr_nodewalk_elm_push(info, TT_NONE,
                                           CTR_WCONTAINER_CAST(dec),
                                           fsize != FS_NONE ?
                                           fsize : curelm->fsize);
        }
    } else if (tag->ttype == TT_C3RAW) {
        /*
         * <c3:raw> tag.
         */
        static const ctr_symmap_t rawmode_map[] = {
            { "inline", RT_INLINE }, { "block", RT_BLOCK }, { NULL, 0 }
        };
        CTR_CLASS_NAME(rawtext) *rtxt;

        ctr_pbr_insert(info);

        rtxt = ctr_rawtext_new(curelm->cont,
                               CTR_ATTRGET_SYMTOVAL("type",
                                                    rawmode_map, RT_INLINE),
                               NULL);
        CTR_CHILD_INSERT_LAST(curelm->cont, rtxt);
        info->currtxt = rtxt;
    }
}

static void ctr_html_end_handler(ctr_nodewalk_info_t *info,
                                 const ctr_html_tag_t *tag)
{
    apr_pool_t *pool;
    ctr_nodewalk_elm_t *elm;
    char *s;

    pool = info->r->pool;

    if (tag->ttype == TT_FORM)
        info->curfrm = NULL;
    else if (tag->ttype == TT_P)
        info->needpbr = TRUE;
    else if (tag->ttype == TT_PRE)
        info->prenest--;

    switch (tag->ttype) {
    case TT_SELECT:
        info->cursel = NULL;
        /* FALL THROUGH */
    case TT_CENTER:
    case TT_MARQUEE:
    case TT_FORM:
    case TT_DIV:
    case TT_P:
    case TT_PRE:
    case TT_H1:
    case TT_H2:
    case TT_H3:
    case TT_H4:
    case TT_H5:
    case TT_H6:
    case TT_UL:
    case TT_OL:
    case TT_LI:
    case TT_DL:
    case TT_DT:
    case TT_DD:
    case TT_A:
    case TT_SPAN:
    case TT_FONT:
    case TT_BLINK:
    case TT_PLAINTEXT:
    case TT_C3IF:
    case TT_C3UNLESS:
        do {
            elm = apr_array_pop(info->elms);
            if (elm->cont == info->pparent)
                info->pparent = elm->cont->parent;
        } while (elm->ttype != tag->ttype);
        break;
    case TT_TITLE:
        s = apr_pstrdup(pool, info->page->title);
        s = ctr_unite_spaces(s);
        s = ctr_trim(s);
        info->page->title = s;
        break;
    case TT_OPTION:
        s = apr_pstrdup(pool, info->curopt->txt);
        s = ctr_unite_spaces(s);
        s = ctr_trim(s);
        info->curopt->txt = s;
        info->curopt = NULL;
        break;
    case TT_TEXTAREA:
        info->curtxa = NULL;
        break;
    case TT_C3RAW:
        info->currtxt = NULL;
        break;
    default:
        /* NOTHING TO BE DONE */
        break;
    }
}

static void ctr_html_chars_handler(ctr_nodewalk_info_t *info,
                                   const ctr_html_chars_t *chars)
{
    apr_pool_t *pool;
    char *txt;

    pool = info->r->pool;
    txt = chars->s;

    if (chars->node.parent->ttype == TT_TITLE)
        info->page->title = apr_pstrcat(pool, info->page->title, txt, NULL);
    else if (chars->node.parent->ttype == TT_STYLE)
        ctr_html_style_parse(info, chars);
    else if (info->curopt != NULL)
        info->curopt->txt = apr_pstrcat(pool, info->curopt->txt, txt, NULL);
    else if (info->curtxa != NULL) {
        txt = ctr_trim_head_newline(txt);
        txt = ctr_trim_last_newline(txt);
        if (*txt != '\0')
            info->curtxa->txt = txt;
    } else if (info->currtxt != NULL) {
        txt = ctr_trim_head_newline(txt);
        txt = ctr_trim_last_newline(txt);
        info->currtxt->txt = txt;
    } else {
        /*
         * Probably normal text between <body> and </body>.
         */
        ctr_nodewalk_elm_t *curelm;
        unsigned char *p, *q;
        size_t len;
        char *s;

        curelm = (ctr_nodewalk_elm_t *)CTR_ARRAY_LAST(info->elms);

        if (chars->node.parent->children == (ctr_html_node_t *)chars) {
            /*
             * All new line characters just after a open tag are ignored in
             * HTML.
             */
            txt = ctr_trim_head_newline(txt);
        }

        if (chars->node.siblings == NULL) {
            /*
             * All new line characters just before a closure tag are ignored
             * in HTML.
             */
            txt = ctr_trim_last_newline(txt);
        }

        if (*txt == '\0')
            return;

        if (!ctr_isspace_str(txt))
            ctr_pbr_insert(info);

        p = q = (unsigned char *)txt;

        /*
         * Check if there are emoji characters in a string or white spaces that
         * must be converted into "&nbsp;" or "<br>" because they are in <pre>
         * tag.
         */
        while (*p) {
            if ((*p == 0xf8 && (0x9f <= *(p + 1) && *(p + 1) <= 0xfc)) ||
                (*p == 0xf9 &&
                 ((0x40 <= *(p + 1) && *(p + 1) <= 0x49) ||
                  (0x50 <= *(p + 1) && *(p + 1) <= 0x52) ||
                  (0x55 <= *(p + 1) && *(p + 1) <= 0x57) ||
                  (0x5b <= *(p + 1) && *(p + 1) <= 0x5e) ||
                  (0x72 <= *(p + 1) && *(p + 1) <= 0x7e) ||
                  (0x80 <= *(p + 1) && *(p + 1) <= 0xfc)))) {
                /*
                 * Found an emoji character.
                 *
                 * If there is normal characters before an emoji, create a
                 * text object from them.
                 */
                CTR_CLASS_NAME(emoji) *emj;

                if ((len = p - q) > 0) {
                    CTR_CLASS_NAME(text) *txt;

                    s = apr_pstrndup(pool, (char *)q, len);
                    txt = ctr_text_new(curelm->cont, s);
                    CTR_CHILD_INSERT_LAST(curelm->cont, txt);
                }

                /*
                 * Create an emoji object.
                 */
                s = apr_psprintf(pool, "%x", ((unsigned)*p) << 8 | *(p + 1));
                emj = ctr_emoji_new(curelm->cont, s);
                CTR_CHILD_INSERT_LAST(curelm->cont, emj);
                q = p + 2;
            }

            if (ctr_is2byteschar((char *)p)) {
                /* 2 bytes character */
                p += 2;
            } else {
                /* 1 byte character */

                if (info->prenest > 0 && apr_isspace(*p)) {
                    if (*p == '\r' || *p == '\n') {
                        int rflg;
                        CTR_CLASS_NAME(newline) *nl;

                        if (*p == '\r')
                            rflg = TRUE;
                        else
                            rflg = FALSE;

                        if ((len = p - q) > 0) {
                            CTR_CLASS_NAME(text) *txt;

                            s = apr_pstrndup(pool, (char *)q, len);
                            txt = ctr_text_new(curelm->cont, s);
                            CTR_CHILD_INSERT_LAST(curelm->cont, txt);
                        }
                        nl = ctr_newline_new(curelm->cont);
                        CTR_CHILD_INSERT_LAST(curelm->cont, nl);
                        p++;
                        q = p;

                        if (rflg && *p == '\n') {
                            p++;
                            q = p;
                        }
                    } else {
                        *p = 0xff;      /* should converted into "&nbsp;" */
                        p++;
                    }
                } else
                    p++;
            }
        }

        if ((len = p - q) > 0) {
            CTR_CLASS_NAME(text) *txt;

            s = apr_pstrndup(pool, (char *)q, len);
            txt = ctr_text_new(curelm->cont, s);
            CTR_CHILD_INSERT_LAST(curelm->cont, txt);
        }
    }
}

static void ctr_html_style_parse(ctr_nodewalk_info_t *info,
                                 const ctr_html_chars_t *chars)
{
    /* NOT IMPLEMENTED YET */
}

static ctr_nodewalk_elm_t *ctr_nodewalk_elm_push(ctr_nodewalk_info_t *info,
                                                 ctr_html_ttype_t ttype,
                                                 ctr_wcontainer_t *cont,
                                                 ctr_fontsize_t fsize)
{
    ctr_nodewalk_elm_t *newelm;

    newelm = apr_array_push(info->elms);
    newelm->ttype = ttype;
    newelm->cont = cont;
    newelm->fsize = fsize;

    return newelm;
}

static void ctr_pbr_insert(ctr_nodewalk_info_t *info)
{
    ctr_nodewalk_elm_t *curelm;

    curelm = (ctr_nodewalk_elm_t *)CTR_ARRAY_LAST(info->elms);

    if (info->needpbr && info->pparent != NULL) {
        /*
         * Insert a brank line to emulate <p>.
         */
        CTR_CLASS_NAME(brank) *brk;

        brk = ctr_brank_new(info->pparent);

        if (curelm->cont == info->pparent)
            CTR_CHILD_INSERT_LAST(info->pparent, brk);
        else
            CTR_CHILD_INSERT_BEFORE(info->pparent->last_child, brk);

        info->needpbr = FALSE;
    }

    info->pparent = curelm->cont;
}

static apr_uri_t *ctr_uri_absolute_convert(const apr_uri_t *base,
                                           const char *add,
                                           const char *scheme,
                                           int port,
                                           const request_rec *r)
{
    apr_uri_t *newbase, *uri;
    char *last_slash;
    char *root;
    char *newpath, *newquery, *newfragment;

    newbase = apr_palloc(r->pool, sizeof(*newbase));
    uri = apr_palloc(r->pool, sizeof(*uri));

    /*
     * Merge base, scheme and port parameters, and make newbase.
     */
    *newbase = *base;
    if (scheme != NULL || port != -1) {
        if (scheme != NULL)
            newbase->scheme = ctr_tolower_str(apr_pstrdup(r->pool, scheme));
        else if (newbase->scheme == NULL) {
#ifdef ap_http_scheme
            newbase->scheme = (char *)ap_http_scheme(r);
#else
            newbase->scheme = (char *)ap_http_method(r);
#endif
        }

        if (port != -1) {
            newbase->port = port;

            if (newbase->port == apr_uri_port_of_scheme(newbase->scheme)) {
                newbase->port = 0;
                newbase->port_str = NULL;
            } else
                newbase->port_str = apr_itoa(r->pool, port);
        }

        if (newbase->hostname == NULL) {
            if (r->hostname != NULL)
                newbase->hostname = apr_pstrdup(r->pool, r->hostname);
            else {
                newbase->hostname = apr_pstrdup(r->pool,
                                                r->server->server_hostname);
            }
        }
    }

    if (add == NULL) {
        if (scheme == NULL && port == -1)
            return NULL;
        else
            return newbase;
    }

    if (apr_uri_parse(r->pool, add, uri) != OK)
        return NULL;

    if (uri->scheme != NULL) {
        uri->scheme = ctr_tolower_str(uri->scheme);

        if (scheme != NULL)
            uri->scheme = newbase->scheme;

        if (port != -1) {
            uri->port = newbase->port;
            uri->port_str = newbase->port_str;
        }

        if (uri->path == NULL)
            uri->path = "/";
        else {
            /*
             * Convert "/aaa/../bbb" into "/bbb".
             */
            apr_filepath_merge(&uri->path, "", uri->path, 0, r->pool);
        }
        return uri;
    }

    if (uri->path == NULL) {
        if (uri->fragment == NULL) {
            /*
             * add is a null string (not NULL but "").
             */
            return newbase;
        } else if (newbase->scheme == NULL && newbase->port == 0) {
            /*
             * add is fragment only.
             */
            return uri;
        }

        newfragment = uri->fragment;
        *uri = *newbase;
        uri->fragment = newfragment;
        return uri;
    }

    if (ctr_casestartwith(uri->path, "tel:") ||
        ctr_casestartwith(uri->path, "mailto:")) {
        char *p;

        for (p = uri->path; *p != ':'; p++)
            *p = apr_tolower(*p);
        return uri;
    }

    if ((last_slash = strrchr(newbase->path, '/')) == NULL)
        root = "/";
    else {
        last_slash++;
        root = apr_pstrndup(r->pool, newbase->path,
                            last_slash - newbase->path);
    }

    apr_filepath_merge(&newpath, root, uri->path, 0, r->pool);
    newquery = uri->query;
    newfragment = uri->fragment;

    *uri = *newbase;
    uri->path = newpath;
    uri->query = newquery;
    uri->fragment = newfragment;

    return uri;
}

static CTR_CLASS_NAME(deco) *ctr_gen_deco_get(const apr_table_t *styles,
                                              const ctr_html_tag_t *tag,
                                              const ctr_nodewalk_elm_t *curelm,
                                              int warnout,
                                              const request_rec *r)
{
    int txtcolor;
    int txtcolor2;
    int bgcolor;
    ctr_fontsize_t fsize;
    int blink;

    txtcolor = ctr_style_txtcolor_get(styles, tag, warnout, r);
    txtcolor2 = ctr_style_txtcolor2_get(styles, tag, warnout, r);
    bgcolor = ctr_style_bgcolor_get(styles, tag, warnout, r);
    fsize = ctr_style_fontsize_get(styles, tag, curelm->fsize, warnout, r);
    blink = ctr_style_blink_get(styles, tag, warnout, r);

    if (txtcolor != -1 || txtcolor2 != -1 ||
        bgcolor != -1 || fsize != FS_NONE || blink) {
        CTR_CLASS_NAME(deco) *dec;

        dec =  ctr_deco_new(curelm->cont, txtcolor, txtcolor2,
                            bgcolor, fsize, blink);
        CTR_CHILD_INSERT_LAST(curelm->cont, dec);
        return dec;
    } else
        return NULL;
}

static apr_table_t *ctr_style_split(const char *style, apr_pool_t *pool)
{
    apr_table_t *styles;
    char *s, *tkn, *last;

    styles = apr_table_make(pool, 1);
    s = apr_pstrdup(pool, style);

    tkn = apr_strtok(s, ";", &last);
    while (tkn != NULL) {
        char *p;

        if ((p = strchr(tkn, ':')) != NULL) {
            char *key, *val;

            *p = '\0';
            key = ctr_trim(tkn);
            val = ctr_trim(p + 1);
            apr_table_setn(styles, key, val);
        }

        tkn = apr_strtok(NULL, ";", &last);
    }

    return styles;
}

static int ctr_style_txtcolor_get(const apr_table_t *styles,
                                  const ctr_html_tag_t *tag, int warnout,
                                  const request_rec *r)
{
    return ctr_attrget_color(styles, "color", tag, warnout, r);
}

static int ctr_style_txtcolor2_get(const apr_table_t *styles,
                                   const ctr_html_tag_t *tag, int warnout,
                                   const request_rec *r)
{
    return ctr_attrget_color(styles, "color2", tag, warnout, r);
}

static int ctr_style_bgcolor_get(const apr_table_t *styles,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r)
{
    return ctr_attrget_color(styles, "background-color", tag, warnout, r);
}

static ctr_halign_t ctr_style_txtalign_get(const apr_table_t *styles,
                                           const ctr_html_tag_t *tag,
                                           int warnout, const request_rec *r)
{
    return ctr_attrget_symtoval(styles, "text-align",
                                halign_map, AL_NONE, tag, warnout, r);
}

static ctr_fontsize_t ctr_style_fontsize_get(const apr_table_t *styles,
                                             const ctr_html_tag_t *tag,
                                             ctr_fontsize_t base, int warnout,
                                             const request_rec *r)
{
    static const ctr_symmap_t fontsize_map[] = {
        { "xx-small", FS_XXSMALL }, { "x-small", FS_XSMALL },
        { "small", FS_SMALL }, { "medium", FS_MEDIUM },
        { "large", FS_LARGE }, { "x-large", FS_XLARGE },
        { "xx-large", FS_XXLARGE }, { "larger", FS_NONE - 1 },
        { "smaller", FS_NONE - 2 }, { NULL, 0 }
    };
    ctr_fontsize_t fsize;

    fsize = ctr_attrget_symtoval(styles, "font-size",
                                fontsize_map, FS_NONE, tag, warnout, r);
    if (fsize == FS_NONE - 1) {
        fsize = base + 1;
        if (fsize > FS_XXLARGE)
            fsize = FS_XXLARGE;
    } else if (fsize == FS_NONE - 2) {
        fsize = base - 1;
        if (fsize < FS_XXSMALL)
            fsize = FS_XXSMALL;
    }

    return fsize;
}

static char *ctr_style_width_get(const apr_table_t *styles,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r)
{
    return ctr_attrget_lensize(styles, "width", tag, warnout, r);
}

static int ctr_style_height_get(const apr_table_t *styles,
                                const ctr_html_tag_t *tag, int warnout,
                                const request_rec *r)
{
    return ctr_attrget_pint(styles, "height", -1, tag, warnout, r);
}

static int ctr_style_blink_get(const apr_table_t *styles,
                               const ctr_html_tag_t *tag, int warnout,
                               const request_rec *r)
{
    static const ctr_symmap_t blink_map[] = {
        { "blink", TRUE }, { NULL, 0 }
    };

    return ctr_attrget_symtoval(styles, "text-decoration",
                                blink_map, FALSE, tag, warnout, r);
}

static int ctr_style_marquee_get(const apr_table_t *styles,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r)
{
    static const ctr_symmap_t marquee_map[] = {
        { "-wap-marquee", TRUE }, { NULL, 0 }
    };

    return ctr_attrget_symtoval(styles, "display",
                                marquee_map, FALSE, tag, warnout, r);
}

static ctr_direct_t ctr_style_mqedirect_get(const apr_table_t *styles,
                                            const ctr_html_tag_t *tag,
                                            int warnout, const request_rec *r)
{
    static const ctr_symmap_t mqedirect_map[] = {
        { "rtl", DL_LEFT }, { "ltr", DL_RIGHT }, { NULL, 0 }
    };

    return ctr_attrget_symtoval(styles, "-wap-marquee-dir",
                                mqedirect_map, DL_LEFT, tag, warnout, r);
}

static ctr_behav_t ctr_style_mqebehav_get(const apr_table_t *styles,
                                          const ctr_html_tag_t *tag,
                                          int warnout, const request_rec *r)
{
    return ctr_attrget_symtoval(styles, "-wap-marquee-style",
                                mqebehav_map, BH_SCROLL, tag, warnout, r);
}

static int ctr_style_mqeloop_get(const apr_table_t *styles,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r)
{
    return ctr_attrget_pint(styles, "-wap-marquee-loop", 1, tag, warnout, r);
}

static ctr_edit_style_t ctr_style_inpfmt_get(const apr_table_t *styles,
                                             const ctr_html_tag_t *tag,
                                             int warnout,
                                             const request_rec *r)
{
    static const ctr_symmap_t inpfmt_map[] = {
        { "\"*<ja:h>\"", ST_FULLKANA }, { "\"*<ja:hk>\"", ST_HALFKANA },
        { "\"*<ja:en>\"", ST_HALFALPHA }, { "\"*<ja:n>\"", ST_HALFNUM },
        { NULL, 0 }
    };

    return ctr_attrget_symtoval(styles, "-wap-input-format",
                                inpfmt_map, ST_NONE, tag, warnout, r);
}

static const char *ctr_attrget_raw(const apr_table_t *attrs,
                                   const char *attrname)
{
    return apr_table_get(attrs, attrname);
}

static char *ctr_attrget_sym(const apr_table_t *attrs, const char *attrname,
                             apr_pool_t *pool)
{
    const char *s;

    if ((s = ctr_attrget_raw(attrs, attrname)) == NULL)
        return NULL;

    return ctr_del_spaces(apr_pstrdup(pool, s));
}

static apr_table_t *ctr_attrget_symstable(const apr_table_t *attrs,
                                          const char *attrname,
                                          apr_pool_t *pool)
{
    char *s;
    apr_table_t *syms;
    char *tkn, *last;

    if ((s = ctr_attrget_sym(attrs, attrname, pool)) == NULL)
        return NULL;

    syms = apr_table_make(pool, 1);

    for (tkn = apr_strtok(s, ",", &last); tkn != NULL;
         tkn = apr_strtok(NULL, ",", &last))
        apr_table_setn(syms, tkn, tkn);

    return syms;
}

static int ctr_attrget_pint(const apr_table_t *attrs, const char *attrname,
                            int dflt, const ctr_html_tag_t *tag, int warnout,
                            const request_rec *r)
{
    const char *s;
    apr_int64_t v;

    if ((s = ctr_attrget_sym(attrs, attrname, r->pool)) == NULL)
        return dflt;

    if ((v = apr_atoi64(s)) <= 0 || v > INT_MAX) {
        if (warnout) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                          "mod_cthree: %s line %d: invalid %s [%s] in <%s>, "
                          "ignore it",
                          r->uri, tag->lineno, attrname, s, tag->name);
        }
        return dflt;
    }

    return (int)v;
}

static int ctr_attrget_pint2(const apr_table_t *attrs, const char *attrname,
                             int dflt, const ctr_html_tag_t *tag, int warnout,
                             const request_rec *r)
{
    const char *s;
    apr_int64_t v;

    if ((s = ctr_attrget_sym(attrs, attrname, r->pool)) == NULL)
        return dflt;

    if ((v = apr_atoi64(s)) < 0 || v > INT_MAX) {
        if (warnout) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                          "mod_cthree: %s line %d: invalid %s [%s] in <%s>, "
                          "ignore it",
                          r->uri, tag->lineno, attrname, s, tag->name);
        }
        return dflt;
    }

    return (int)v;
}

static int ctr_attrget_flg(const apr_table_t *attrs, const char *attrname)
{
    return ctr_attrget_raw(attrs, attrname) != NULL;
}

static int ctr_attrget_color(const apr_table_t *attrs, const char *attrname,
                             const ctr_html_tag_t *tag, int warnout,
                             const request_rec *r)
{
    static const ctr_symmap_t color_map[] = {
        { "black", 0x000000 }, { "gray", 0x808080 }, { "maroon", 0x800000 },
        { "purple", 0x800080 }, { "green", 0x008000 }, { "olive", 0x808000 },
        { "navy", 0x000080 }, { "teal", 0x008080 }, { "silver", 0xc0c0c0 },
        { "white", 0xffffff }, { "red", 0xff0000 }, { "fuchsia", 0xff00ff },
        { "lime", 0x00ff00 }, { "yellow", 0xffff00 }, { "blue", 0x0000ff },
        { "aqua", 0x00ffff }, { NULL, 0 }
    };
    char *s;
    const ctr_symmap_t *mapp;
    apr_int64_t v;

    if ((s = ctr_attrget_sym(attrs, attrname, r->pool)) == NULL)
        return -1;

    for (mapp = color_map; mapp->sym != NULL; mapp++) {
        if (strcasecmp(mapp->sym, s) == 0)
            return mapp->val;
    }

    if (*s == '#')
        s++;
    if (!ctr_isxdigit_str(s) || (v = apr_strtoi64(s, NULL, 16)) < 0 ||
        v > 0xffffff) {
        if (warnout) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                          "mod_cthree: %s line %d: invalid %s [%s] in <%s>, "
                          "ignore it",
                          r->uri, tag->lineno, attrname, s, tag->name);
        }
        return -1;
    }

    return (int)v;
}

static int ctr_attrget_symtoval(const apr_table_t *attrs, const char *attrname,
                                const ctr_symmap_t *map, int dflt,
                                const ctr_html_tag_t *tag, int warnout,
                                const request_rec *r)
{
    char *s;

    if ((s = ctr_attrget_sym(attrs, attrname, r->pool)) == NULL)
        return dflt;

    for (; map->sym != NULL; map++) {
        if (strcasecmp(s, map->sym) == 0)
            return map->val;
    }

    if (warnout) {
        ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                      "mod_cthree: %s line %d: invalid %s [%s] in <%s>, "
                      "ignore it",
                      r->uri, tag->lineno, attrname, s, tag->name);
    }

    return dflt;
}

static int ctr_attrget_symstoorval(const apr_table_t *attrs,
                                   const char *attrname,
                                   const ctr_symmap_t *map, int dflt,
                                   const ctr_html_tag_t *tag, int warnout,
                                   const request_rec *r)
{
    char *s;
    int val;
    char *tkn, *last;
    const ctr_symmap_t *mapp;
    int found;

    if ((s = ctr_attrget_sym(attrs, attrname, r->pool)) == NULL)
        return dflt;

    val = 0;

    for (tkn = apr_strtok(s, ",", &last); tkn != NULL;
         tkn = apr_strtok(NULL, ",", &last)) {
        found = FALSE;

        for (mapp = map; mapp->sym != NULL; mapp++) {
            if (strcasecmp(tkn, mapp->sym) == 0) {
                found = TRUE;
                val |= mapp->val;
                break;
            }
        }

        if (found == FALSE && warnout) {
            ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                          "mod_cthree: %s line %d: invalid %s [%s] in <%s>, "
                          "ignore it",
                          r->uri, tag->lineno, attrname, tkn, tag->name);
        }
    }

    if (val == 0)
        return dflt;

    return val;
}

static char *ctr_attrget_lensize(const apr_table_t *attrs,
                                 const char *attrname,
                                 const ctr_html_tag_t *tag, int warnout,
                                 const request_rec *r)
{
    char *lensize;

    if ((lensize = ctr_attrget_sym(attrs, attrname, r->pool)) != NULL) {
        char *p;
        apr_size_t len;
        int eflg = FALSE;

        p = lensize;
        len = strlen(lensize);
        if (len == 0 || (len == 1 && !apr_isdigit(*p)))
            eflg = TRUE;
        else {
            while (*p) {
                if (!apr_isdigit(*p)) {
                    if (*p == '%' && *(p + 1) == '\0')
                        break;
                    else {
                        eflg = TRUE;
                        break;
                    }
                }
                p++;
            }
        }
        if (eflg) {
            if (warnout) {
                ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, r,
                              "mod_cthree: %s line %d: invalid %s [%s] "
                              "in <%s>, ignore it",
                              r->uri, tag->lineno,
                              attrname, lensize, tag->name);
            }
            lensize = NULL;
        }
    } else
        lensize = NULL;

    return lensize;
}
