/*
 * XML/HTML parser for qemacs.
 *
 * Copyright (c) 2000-2002 Fabrice Bellard.
 * Copyright (c) 2007-2025 Charlie Gordon.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "util.h"
#include "charset.h"
#include "css.h"

// XXX: need fix for this: should use read function with opaque argument
struct EditBuffer;
char32_t eb_nextc(struct EditBuffer *b, int offset, int *next_ptr);

//#define DEBUG

#if 0
#ifdef DEBUG
#define dprintf(fmt, args...) printf(fmt, ##args)
#else
#define dprintf(fmt, args...)
#define NDEBUG
#endif
#define ddprintf(fmt, args...)
#else
#ifdef DEBUG
#define dprintf(fmt, ...)  printf(fmt, ##__VA_ARGS__)
#else
#define dprintf(...)
#define NDEBUG
#endif
#define ddprintf(...)
#endif

#include <assert.h>

/* entities definition */
typedef struct {
    const char *name;
    int val;
} XMLEntity;

#define ENTITY(name, value) { #name, value },

static XMLEntity const html_entities[] = {
#include "htmlent.h"
    { NULL, 0 },
};

/* return -1 if not found */
int find_entity(const char *str)
{
    const char *name;
    const XMLEntity *e;
    int code;

    if (str[0] == '#') {
        if (str[1] == 'x')
            code = strtol(str + 2, NULL, 16);
        else
            code = strtol(str + 1, NULL, 10);
        if (code <= 0)
            return -1;
        return code;
    }

    e = html_entities;
    for (;;) {
        name = e->name;
        if (!name)
            break;
        if (strequal(str, name))
            return e->val;
        e++;
    }
    return -1;
}

const char *find_entity_str(int code)
{
    const XMLEntity *e;

    e = html_entities;
    for (;;) {
        if (!e->name)
            break;
        if (e->val == code)
            return e->name;
        e++;
    }
    return NULL;
}

/* parse an entity or a normal char (only ASCII chars are expected) */
static int parse_entity(const char **pp)
{
    const char *p, *p1;
    char name[16], *q;
    int ch, ch1;

    p = *pp;
    ch = (u8)*p++;
    if (ch == '&') {
        p1 = p;
        q = name;
        for (;;) {
            ch1 = (u8)*p;
            if (ch1 == '\0')
                break;
            p++;
            if (ch1 == ';')
                break;
            *q++ = ch1;
            if (q >= name + sizeof(name) - 1)
                break;
        }
        *q = '\0';
        ch1 = find_entity(name);
        if (ch1 >= 0) {
            ch = ch1;
        } else {
            p = p1;
        }
    }
    *pp = p;
    return ch;
}

/***********************************************************/
/* XML parser */

enum XMLParseState {
    XML_STATE_TEXT,
    XML_STATE_TAG,
    XML_STATE_COMMENT,
    XML_STATE_COMMENT1,
    XML_STATE_COMMENT2,
    XML_STATE_PRETAG,
    XML_STATE_WAIT_EOT,
};

#define STRING_BUF_SIZE 4096
//#define STRING_BUF_SIZE 256

/* string buffer optimized for strings lengths <= STRING_BUF_SIZE */
typedef struct StringBuffer {
    char *buf;
    int allocated_size;
    int size;
    char buf1[STRING_BUF_SIZE];
} StringBuffer;

static inline void strbuf_init(StringBuffer *b)
{
    b->buf = b->buf1;
    b->allocated_size = STRING_BUF_SIZE;
    b->size = 0;
}

static inline void strbuf_reset(StringBuffer *b)
{
    if (b->buf != b->buf1)
        qe_free(&b->buf);
    strbuf_init(b);
}

static void strbuf_addch1(StringBuffer *b, char32_t ch)
{
    int size1;
    char *ptr;

    size1 = b->allocated_size + STRING_BUF_SIZE;
    ptr = b->buf;
    if (b->buf == b->buf1)
        ptr = NULL;
    if (qe_realloc_bytes(&ptr, size1)) {
        if (b->buf == b->buf1)
            memcpy(ptr, b->buf1, STRING_BUF_SIZE);
        b->buf = ptr;
        b->allocated_size = size1;

        b->size += utf8_encode(b->buf + b->size, ch);
    }
}

static inline void strbuf_addch(StringBuffer *b, char32_t ch)
{
    if (b->size + UTF8_CHAR_LEN_MAX < b->allocated_size) {
        /* fast case */
        b->size += utf8_encode(b->buf + b->size, ch);
    } else {
        strbuf_addch1(b, ch);
    }
}

#if 0

/* offset compression */

typedef struct OffsetBuffer {
    unsigned int *offsets;
    int nb_allocated_offsets;
    int nb_offsets;
    unsigned int last_offset;
} OffsetBuffer;

static void offsetbuf_init(OffsetBuffer *b)
{
    b->offsets = NULL;
    b->nb_allocated_offsets = 0;
    b->nb_offsets = 0;
    b->last_offset = -2; /* ensures that a normal offset will be added
                            first without adding more tests */
}

static void offsetbuf_reset(OffsetBuffer *b)
{
    qe_free(&b->offsets);
    offsetbuf_init(b);
}

/* XXX: return an error if memory fails ? */
static void offsetbuf_add(OffsetBuffer *b, unsigned int offset)
{
    int n;

    if (offset == (b->last_offset + 1)) {
        /* just indicate a run of offsets */
        b->offsets[b->nb_offsets - 1] |= 0x80000000;
    } else {
        if (b->nb_offsets >= b->nb_allocated_offsets) {
            n = b->nb_allocated_offsets;
            if (!n)
                n = 1;
            n = n * 2;
            if (!qe_realloc_array(&b->offsets, n))
                return;
            b->nb_allocated_offsets = n;
        }
        b->offsets[b->nb_offsets++] = offset;
    }
    b->last_offset = offset;
}
#endif

#define LOOKAHEAD_SIZE  16

struct XMLState {
    CSSBox *root_box;
    CSSBox *box;
    int is_html;
    int html_syntax;
    int ignore_case;
    int flags;
    CSSStyleSheet *style_sheet; /* if non NULL, all style sheets are
                                   add to that */
    enum XMLParseState state;
    int line_num;
    CSSAbortFunc *abort_func;
    void *abort_opaque;
    QECharset *charset;
    int base_font; /* XXX: is it correct ? */
    int lookahead_size;
    char lookahead_buf[2 * LOOKAHEAD_SIZE];
    int pretaglen;
    char pretag[32]; /* current tag in XML_STATE_PRETAG */
    StringBuffer str;
    void *error_opaque;
    char filename[MAX_FILENAME_SIZE];
    CharsetDecodeState charset_state;
};

/* start xml parsing */

XMLState *xml_begin(CSSStyleSheet *style_sheet, int flags,
                    CSSAbortFunc *abort_func, void *abort_opaque,
                    void *error_opaque, const char *filename,
                    QECharset *charset)
{
    XMLState *s;

    s = qe_mallocz(XMLState);
    if (!s)
        return NULL;
    s->flags = flags;
    s->is_html = flags & XML_HTML;
    s->html_syntax = flags & XML_HTML_SYNTAX;
    s->ignore_case = flags & XML_IGNORE_CASE;
    s->state = XML_STATE_TEXT;
    s->box = NULL;
    s->style_sheet = style_sheet;
    strbuf_init(&s->str);
    s->abort_func = abort_func;
    s->abort_opaque = abort_opaque;
    s->base_font = 3;
    s->line_num = 1;
    s->error_opaque = error_opaque;
    pstrcpy(s->filename, sizeof(s->filename), filename);
    s->charset = charset;
    if (charset) {
        charset_decode_init(&s->charset_state, charset, EOL_UNIX);
    }
    return s;
}

static CSSAttribute *box_new_attr(CSSIdent attr_id, const char *value)
{
    CSSAttribute *attr;
    int len = strlen(value);

    attr = qe_malloc_hack(CSSAttribute, len);
    if (!attr)
        return NULL;
    attr->attr = attr_id;
    attr->next = NULL;
    memcpy(attr->value, value, len + 1);
    return attr;
}

static const char *css_attr_str(CSSBox *box, CSSIdent attr_id)
{
    CSSAttribute *attr;

    attr = box->attrs;
    while (attr != NULL) {
        if (attr->attr == attr_id)
            return attr->value;
        attr = attr->next;
    }
    return NULL;
}

static const char *css_attr_strlower(CSSBox *box, CSSIdent attr_id)
{
    static char buf[200];   /* ugly patch to return a string */
    const char *value;

    value = css_attr_str(box, attr_id);
    if (!value)
        return NULL;
    qe_strtolower(buf, sizeof(buf), value);
    return buf;
}


static int css_attr_int(CSSBox *box, CSSIdent attr_id, int def_val)
{
    const char *str, *p;
    int val;

    str = css_attr_str(box, attr_id);
    if (!str)
        return def_val;
    val = strtol_c(str, &p, 10);
    /* exclude non numeric inputs (for example percentages) */
    if (*p != '\0')
        return def_val;
    return val;
}


/* simplistic HTML table border handling */
static void html_table_borders(CSSBox *box, int border, int padding)
{
    CSSProperty **last_prop;
    CSSBox *box1;

    if (box->tag == CSS_ID_td) {
        last_prop = &box->properties;
        while (*last_prop != NULL)
            last_prop = &(*last_prop)->next;
        if (border >= 1) {
            css_add_prop_unit(&last_prop, CSS_border_left_width,
                              CSS_UNIT_PIXEL, border);
            css_add_prop_unit(&last_prop, CSS_border_right_width,
                              CSS_UNIT_PIXEL, border);
            css_add_prop_unit(&last_prop, CSS_border_top_width,
                              CSS_UNIT_PIXEL, border);
            css_add_prop_unit(&last_prop, CSS_border_bottom_width,
                              CSS_UNIT_PIXEL, border);

            css_add_prop_int(&last_prop, CSS_border_left_style,
                             CSS_BORDER_STYLE_RIDGE);
            css_add_prop_int(&last_prop, CSS_border_right_style,
                             CSS_BORDER_STYLE_RIDGE);
            css_add_prop_int(&last_prop, CSS_border_top_style,
                             CSS_BORDER_STYLE_RIDGE);
            css_add_prop_int(&last_prop, CSS_border_bottom_style,
                             CSS_BORDER_STYLE_RIDGE);
        }
        if (padding >= 1) {
            css_add_prop_unit(&last_prop, CSS_padding_left,
                              CSS_UNIT_PIXEL, padding);
            css_add_prop_unit(&last_prop, CSS_padding_right,
                              CSS_UNIT_PIXEL, padding);
            css_add_prop_unit(&last_prop, CSS_padding_top,
                              CSS_UNIT_PIXEL, padding);
            css_add_prop_unit(&last_prop, CSS_padding_bottom,
                              CSS_UNIT_PIXEL, padding);
        }
    }

    if (box->content_type == CSS_CONTENT_TYPE_CHILDS) {
        for (box1 = box->u.child.first; box1 != NULL; box1 = box1->next) {
            html_table_borders(box1, border, padding);
        }
    }
}

#define DEFAULT_IMG_WIDTH  32
#define DEFAULT_IMG_HEIGHT 32

static void html_eval_tag(XMLState *s, CSSBox *box)
{
    const char *value;
    CSSProperty *first_prop, **last_prop;
    QEColor color;
    int width, height, val, type;
    int border, padding;
    CSSPropertyValue arg;
    CSSPropertyValue args[2];

    first_prop = NULL;
    last_prop = &first_prop;

    switch (box->tag) {
    case CSS_ID_img:
    parse_img:
        box->content_type = CSS_CONTENT_TYPE_IMAGE;
        box->u.image.content_alt = NULL;
        /* set alt content */
        value = css_attr_str(box, CSS_ID_alt);
        if (!value) {
            /* if no alt, display the name of the image */
            value = css_attr_str(box, CSS_ID_src);
            if (value)
                value = get_basename(value);
        }
        if (value && value[0] != '\0') {
            arg.type = CSS_VALUE_STRING;
            arg.u.str = qe_strdup(value);
            css_add_prop(&last_prop, CSS_content_alt, &arg);
        }

        width = css_attr_int(box, CSS_ID_width, 0);
        if (width <= 0)
            width = DEFAULT_IMG_WIDTH;
        height = css_attr_int(box, CSS_ID_height, 0);
        if (height <= 0)
            height = DEFAULT_IMG_HEIGHT;

        css_add_prop_unit(&last_prop, CSS_width,
                          CSS_UNIT_PIXEL, width);
        css_add_prop_unit(&last_prop, CSS_height,
                          CSS_UNIT_PIXEL, height);

        /* border */
        val = css_attr_int(box, CSS_ID_border, -1);
        if (val >= 0) {
            css_add_prop_unit(&last_prop, CSS_border_left_width,
                              CSS_UNIT_PIXEL, val);
            css_add_prop_unit(&last_prop, CSS_border_right_width,
                              CSS_UNIT_PIXEL, val);
            css_add_prop_unit(&last_prop, CSS_border_top_width,
                              CSS_UNIT_PIXEL, val);
            css_add_prop_unit(&last_prop, CSS_border_bottom_width,
                              CSS_UNIT_PIXEL, val);

            css_add_prop_int(&last_prop, CSS_border_left_style,
                             CSS_BORDER_STYLE_SOLID);
            css_add_prop_int(&last_prop, CSS_border_right_style,
                             CSS_BORDER_STYLE_SOLID);
            css_add_prop_int(&last_prop, CSS_border_top_style,
                             CSS_BORDER_STYLE_SOLID);
            css_add_prop_int(&last_prop, CSS_border_bottom_style,
                             CSS_BORDER_STYLE_SOLID);
        }
        /* margins */
        val = css_attr_int(box, CSS_ID_hspace, -1);
        if (val >= 0) {
            css_add_prop_unit(&last_prop, CSS_margin_left,
                              CSS_UNIT_PIXEL, val);
            css_add_prop_unit(&last_prop, CSS_margin_right,
                              CSS_UNIT_PIXEL, val);
        }
        val = css_attr_int(box, CSS_ID_vspace, -1);
        if (val >= 0) {
            css_add_prop_unit(&last_prop, CSS_margin_top,
                              CSS_UNIT_PIXEL, val);
            css_add_prop_unit(&last_prop, CSS_margin_bottom,
                              CSS_UNIT_PIXEL, val);
        }
        break;
    case CSS_ID_body:
        value = css_attr_str(box, CSS_ID_text);
        if (value && !css_get_color(&color, value)) {
            css_add_prop_int(&last_prop, CSS_color, color);
        }
        /* we handle link by adding a new stylesheet entry */
        value = css_attr_str(box, CSS_ID_link);
        if (value && !css_get_color(&color, value)) {
            CSSStyleSheetEntry *e;
            CSSSimpleSelector ss1, *ss = &ss1;
            CSSProperty **last_prop1;
            CSSStyleSheetAttributeEntry **plast_attr;

            /* specific to <a href="xxx"> tag */
            memset(ss, 0, sizeof(CSSSimpleSelector));
            ss->tag = CSS_ID_a;
            plast_attr = &ss->attrs;
            add_attribute(&plast_attr, CSS_ID_href, CSS_ATTR_OP_SET, "");

            e = add_style_entry(s->style_sheet, ss, CSS_MEDIA_ALL);

            /* add color property */
            last_prop1 = &e->props;
            css_add_prop_int(&last_prop1, CSS_color, color);
        }
        break;
    case CSS_ID_font:
    case CSS_ID_basefont:
        /* size */
        value = css_attr_str(box, CSS_ID_size);
        if (value) {
            val = strtol(value, NULL, 10);
            if (value[0] == '+' || value[0] == '-') {
                /* relative size */
                val += s->base_font;
            }
            if (val < 1)
                val = 1;
            else if (val > 7)
                val = 7;
            if (box->tag == CSS_ID_basefont)
                s->base_font = val;
            /* XXX: incorrect for basefont */
            css_add_prop_unit(&last_prop, CSS_font_size,
                              CSS_UNIT_IN, get_font_size(val - 1));
        }

        /* color */
        value = css_attr_str(box, CSS_ID_color);
        if (value && !css_get_color(&color, value)) {
            css_add_prop_int(&last_prop, CSS_color, color);
        }
        break;
    case CSS_ID_br:
        value = css_attr_strlower(box, CSS_ID_clear);
        if (value) {
            val = css_get_enum(value, "none,left,right,all");
            if (val >= 0) {
                css_add_prop_int(&last_prop, CSS_clear, val + CSS_CLEAR_NONE);
            }
        }
        break;
    case CSS_ID_table:
        val = css_attr_int(box, CSS_ID_width, -1);
        if (val >= 0) {
            css_add_prop_unit(&last_prop, CSS_width,
                              CSS_UNIT_PIXEL, val);
        }
        border = css_attr_int(box, CSS_ID_border, -1);
        if (border >= 0) {
            css_add_prop_unit(&last_prop, CSS_border_left_width,
                              CSS_UNIT_PIXEL, border);
            css_add_prop_unit(&last_prop, CSS_border_right_width,
                              CSS_UNIT_PIXEL, border);
            css_add_prop_unit(&last_prop, CSS_border_top_width,
                              CSS_UNIT_PIXEL, border);
            css_add_prop_unit(&last_prop, CSS_border_bottom_width,
                              CSS_UNIT_PIXEL, border);
            css_add_prop_int(&last_prop, CSS_border_left_style,
                             CSS_BORDER_STYLE_GROOVE);
            css_add_prop_int(&last_prop, CSS_border_right_style,
                             CSS_BORDER_STYLE_GROOVE);
            css_add_prop_int(&last_prop, CSS_border_top_style,
                             CSS_BORDER_STYLE_GROOVE);
            css_add_prop_int(&last_prop, CSS_border_bottom_style,
                             CSS_BORDER_STYLE_GROOVE);
            /* cell have a border of 1 pixel width */
            if (border > 1)
                border = 1;
        }
        val = css_attr_int(box, CSS_ID_cellspacing, -1);
        if (val >= 0) {
            css_add_prop_unit(&last_prop, CSS_border_spacing_horizontal,
                              CSS_UNIT_PIXEL, val);
            css_add_prop_unit(&last_prop, CSS_border_spacing_vertical,
                              CSS_UNIT_PIXEL, val);
        }
        padding = css_attr_int(box, CSS_ID_cellpadding, -1);
        /* apply border styles to each cell (cannot be done exactly by
           CSS) */
        if (border >= 1 || padding >= 1)
            html_table_borders(box, border, padding);
        break;

    case CSS_ID_col:
    case CSS_ID_colgroup:
        val = css_attr_int(box, CSS_ID_width, -1);
        if (val >= 0) {
            css_add_prop_unit(&last_prop, CSS_width, CSS_UNIT_PIXEL, val);
        }
        break;
    case CSS_ID_td:
        val = css_attr_int(box, CSS_ID_width, -1);
        if (val >= 0) {
            css_add_prop_unit(&last_prop, CSS_width, CSS_UNIT_PIXEL, val);
        }
        val = css_attr_int(box, CSS_ID_height, -1);
        if (val >= 0) {
            css_add_prop_unit(&last_prop, CSS_height, CSS_UNIT_PIXEL, val);
        }
        break;

    case CSS_ID_ol:
    case CSS_ID_li:
        /* NOTE: case is important */
        /* XXX: currently cannot propagate for LI tag */
        value = css_attr_str(box, CSS_ID_type);
        if (value) {
            val = css_get_enum(value, "1,a,A,i,I");
            if (val >= 0) {
                val += CSS_LIST_STYLE_TYPE_DECIMAL;
                css_add_prop_int(&last_prop, CSS_list_style_type, val);
            }
        }
        /* XXX: add value, but needs a css extension */
        if (box->tag == CSS_ID_ol)
            val = CSS_ID_start;
        else
            val = CSS_ID_value;
        /* NOTE: only works with digits */
        val = css_attr_int(box, val, 0);
        if (val > 0) {
            args[0].type = CSS_VALUE_IDENT;
            args[0].u.val = CSS_ID_list_item;
            args[1].type = CSS_VALUE_INTEGER;
            args[1].u.val = val - 1; /* currently needs minus 1 */
            css_add_prop_values(&last_prop, CSS_counter_reset, 2, args);
        }
        break;
        /* controls */
    case CSS_ID_button:
        type = CSS_ID_submit;
        value = css_attr_strlower(box, CSS_ID_type);
        if (value)
            type = css_new_ident(value);
        if (type != CSS_ID_button && type != CSS_ID_reset)
            type = CSS_ID_submit;
        goto parse_input;
    case CSS_ID_input:
        type = CSS_ID_text;
        value = css_attr_strlower(box, CSS_ID_type);
        if (value) {
            type = css_new_ident(value);
        } else {
            CSSAttribute *attr;
            /* NOTE: we add an attribute for css rules */
            attr = box_new_attr(CSS_ID_type, "text");
            if (attr) {
                attr->next = box->attrs;
                box->attrs = attr;
            }
        }

    parse_input:
        if (type == CSS_ID_image)
            goto parse_img;
        if (type == CSS_ID_button ||
            type == CSS_ID_reset ||
            type == CSS_ID_submit ||
            type == CSS_ID_text ||
            type == CSS_ID_password ||
            type == CSS_ID_file) {
            /* put text inside the box (XXX: use attr() in content
               attribute ? */
            value = css_attr_str(box, CSS_ID_value);
            if (value) {
                css_set_text_string(box, value);
            }
        }
        /* size */
        if (type == CSS_ID_text ||
            type == CSS_ID_password) {
            val = css_attr_int(box, CSS_ID_size, 10);
            css_add_prop_unit(&last_prop, CSS_width,
                              CSS_UNIT_EM, val << CSS_LENGTH_FRAC_BITS);
        }
        break;
    case CSS_ID_textarea:
        val = css_attr_int(box, CSS_ID_cols, 10);
        if (val < 1)
            val = 1;
        css_add_prop_unit(&last_prop, CSS_width,
                          CSS_UNIT_EM, val << CSS_LENGTH_FRAC_BITS);

        val = css_attr_int(box, CSS_ID_rows, 1);
        if (val < 1)
            val = 1;
        css_add_prop_unit(&last_prop, CSS_height,
                          CSS_UNIT_EM, val << CSS_LENGTH_FRAC_BITS);
        break;
    case CSS_ID_select:
        val = css_attr_int(box, CSS_ID_size, 1);
        if (val < 1)
            val = 1;
        css_add_prop_unit(&last_prop, CSS_height,
                          CSS_UNIT_EM, val << CSS_LENGTH_FRAC_BITS);
        break;
    default:
        break;
    }

    /* generic attributes */
    value = css_attr_str(box, CSS_ID_bgcolor);
    if (value && !css_get_color(&color, value)) {
        css_add_prop_int(&last_prop, CSS_background_color, color);
    }
    value = css_attr_strlower(box, CSS_ID_align);
    if (value) {
        switch (box->tag) {
        case CSS_ID_caption:
            /* use caption-side property for captions */
            val = css_get_enum(value, "top,bottom,left,right");
            if (val >= 0) {
                css_add_prop_int(&last_prop, CSS_caption_side, val);
            }
            break;
        case CSS_ID_img:
            /* floating images */
            val = css_get_enum(value, "left,right");
            if (val >= 0) {
                css_add_prop_int(&last_prop, CSS_float, val + CSS_FLOAT_LEFT);
            }
            break;
        case CSS_ID_table:
            val = css_get_enum(value, "left,right,center");
            if (val == CSS_TEXT_ALIGN_LEFT || val == CSS_TEXT_ALIGN_RIGHT) {
                css_add_prop_int(&last_prop, CSS_float, val + CSS_FLOAT_LEFT);
            } else if (val == CSS_TEXT_ALIGN_CENTER) {
                css_add_prop_int(&last_prop, CSS_margin_left, CSS_AUTO);
                css_add_prop_int(&last_prop, CSS_margin_right, CSS_AUTO);
            }
            break;
        default:
            val = css_get_enum(value, "left,right,center");
            if (val >= 0) {
                css_add_prop_int(&last_prop, CSS_text_align, val);
            }
            break;
        }
    }
    value = css_attr_strlower(box, CSS_ID_valign);
    if (value) {
        val = css_get_enum(value, "baseline,,,top,,middle,bottom");
        if (val >= 0) {
            css_add_prop_int(&last_prop, CSS_vertical_align, val);
        }
    }

    val = css_attr_int(box, CSS_ID_colspan, 1);
    if (val > 1) {
        css_add_prop_unit(&last_prop, CSS_column_span, CSS_VALUE_INTEGER, val);
    }
    val = css_attr_int(box, CSS_ID_rowspan, 1);
    if (val > 1) {
        css_add_prop_unit(&last_prop, CSS_row_span, CSS_VALUE_INTEGER, val);
    }

    value = css_attr_str(box, CSS_ID_style);
    if (value) {
        CSSParseState b1, *b = &b1;
        b->error_opaque = s->error_opaque;
        b->ptr = NULL;
        b->filename = s->filename;
        b->line_num = s->line_num; /* XXX: slightly incorrect */
        b->ignore_case = s->ignore_case;
        *last_prop = css_parse_properties(b, value);
    }
    box->properties = first_prop;
}

static void xml_error(XMLState *s, const char *fmt, ...)
    qe__attr_printf(2,3);

static void xml_error(XMLState *s, const char *fmt, ...)
{
    char buf[1024];
    va_list ap;

    va_start(ap, fmt);
    vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);
    css_error(s->error_opaque, s->filename, s->line_num, buf);
}

/* XXX: avoid using strings in tag_closed and generalize */
typedef struct {
    CSSIdent tag;    /* tag to handle */
    const char *tag_closed;  /* all the following tags can be closed
                                automatically */
} HTMLClosedTags;

static const HTMLClosedTags html_closed_tags[] = {
    { CSS_ID_li, "li,b,i,em,s,u,strike,strong,a" },
    { CSS_ID_td, "td,th,b,i,em,s,u,strike,strong,a,li" },
    { CSS_ID_th, "td,th,b,i,em,s,u,strike,strong,a,li" },
    { CSS_ID_tr, "tr,td,th,b,i,em,s,u,strike,strong,a,li" },
    { CSS_ID_dt, "dd,b,i,em,s,u,strike,strong,a" },
    { CSS_ID_dd, "dt,b,i,em,s,u,strike,strong,a" },
    { CSS_ID_b, "i,em,s,u,strike,strong" },
    { CSS_ID_table, "font" },
    { CSS_ID_NIL, NULL },
};

static int parse_tag(XMLState *s, const char *buf)
{
    char tag[256], *q, len, eot;
    char attr_name[256];
    char value[2048];
    const char *p;
    CSSIdent css_tag;
    CSSBox *box, *box1;
    CSSAttribute *first_attr, **pattr, *attr;

    p = buf;

    /* ignore XML commands */
    if (p[0] == '!' || p[0] == '?')
        return XML_STATE_TEXT;

    /* end of tag check */
    eot = 0;
    if (*p == '/') {
        p++;
        eot = 1;
    }

    /* parse the tag name */
    get_str(&p, tag, sizeof(tag), "/");
    if (tag[0] == '\0') {
        /* special closing tag */
        if (eot) {
            css_tag = CSS_ID_NIL;
            goto end_of_tag;
        } else {
            xml_error(s, "invalid null tag");
            return XML_STATE_TEXT;
        }
    }
    if (s->ignore_case)
        qe_strtolower(tag, sizeof(tag), tag);
    css_tag = css_new_ident(tag);

    /* XXX: should test html_syntax, but need more patches */
    if (s->is_html && (css_tag == CSS_ID_style ||
                       css_tag == CSS_ID_script))
        goto pretag;
    if (eot)
        goto end_of_tag;

    /* parse attributes */
    first_attr = NULL;
    pattr = &first_attr;
    for (;;) {
        qe_skip_spaces(&p);
        if (*p == '\0' || *p == '/')
            break;
        get_str(&p, attr_name, sizeof(attr_name), "=/");
        if (s->ignore_case)
            qe_strtolower(attr_name, sizeof(attr_name), attr_name);
        if (*p == '=') {
            int och, ch;
            p++;
            qe_skip_spaces(&p);
            och = *p;
            /* in html, we can put non string values */
            if (och != '\'' && och != '\"') {
                if (!s->html_syntax)
                    xml_error(s, "string expected for attribute '%s'", attr_name);
                q = value;
                while (*p != '\0' && !strchr(" \t\n\r<>", *p)) {
                    ch = parse_entity(&p);
                    if ((q - value) < (int)sizeof(value) - 1)
                        *q++ = ch;
                }
                *q = '\0';
            } else {
                p++;
                q = value;
                while (*p != och && *p != '\0' && *p != '<') {
                    ch = parse_entity(&p);
                    if ((q - value) < (int)sizeof(value) - 1)
                        *q++ = ch;
                }
                *q = '\0';
                if (*p != och) {
                    xml_error(s, "malformed string in attribute '%s'", attr_name);
                } else {
                    p++;
                }
            }
        } else {
            value[0] = '\0';
        }
        attr = box_new_attr(css_new_ident(attr_name), value);
        if (attr) {
            *pattr = attr;
            pattr = &attr->next;
        }
    }

    /* close some tags (correct HTML mistakes) */
    if (s->html_syntax) {
        const HTMLClosedTags *ct;
        ct = html_closed_tags;
        for (;;) {
            if (!ct->tag)
                break;
            if (css_tag == ct->tag) {
                box1 = s->box;
                while (box1 != NULL &&
                       css_get_enum(css_ident_str(box1->tag), ct->tag_closed) >= 0) {
                    html_eval_tag(s, box1);
                    box1 = box1->parent;
                }
                if (box1) {
                    s->box = box1;
                }
                break;
            }
            ct++;
        }
    }

    /* create the new box and add it */
    box = css_new_box(css_tag, NULL);
    box->attrs = first_attr;
    if (!s->box) {
        s->root_box = box;
    } else {
        css_make_child_box(s->box);
        css_add_box(s->box, box);
    }
    s->box = box;

    if ((s->flags & XML_DOCBOOK) &&
        css_tag == CSS_ID_programlisting) {
    pretag:
        pstrcpy(s->pretag, sizeof(s->pretag), tag);
        s->pretaglen = strlen(s->pretag);
        return XML_STATE_PRETAG;
    }

    len = strlen(buf);
    /* end of tag. If html, check also some common mistakes. FORM is
       considered as self closing to avoid any content problems */
    if ((len > 0 && buf[len - 1] == '/') ||
        (s->html_syntax && (css_tag == CSS_ID_br ||
                            css_tag == CSS_ID_hr ||
                            css_tag == CSS_ID_meta ||
                            css_tag == CSS_ID_link ||
                            css_tag == CSS_ID_form ||
                            css_tag == CSS_ID_base ||
                            css_tag == CSS_ID_input ||
                            css_tag == CSS_ID_basefont ||
                            css_tag == CSS_ID_img))) {
    end_of_tag:
        box1 = s->box;
        if (box1) {
            if (s->html_syntax) {
                if (css_tag != CSS_ID_NIL) {
                    /* close all non matching tags */
                    while (box1 != NULL && box1->tag != css_tag) {
                        html_eval_tag(s, box1);
                        box1 = box1->parent;
                    }
                }
                if (!box1) {
                    if (css_tag != CSS_ID_form)
                        xml_error(s, "unmatched closing tag </%s>",
                                  css_ident_str(css_tag));
                } else {
                    html_eval_tag(s, box1);
                    s->box = box1->parent;
                }
            } else {
                if (css_tag != CSS_ID_NIL && box1->tag != css_tag) {
                    xml_error(s, "unmatched closing tag </%s> for <%s>",
                              css_ident_str(css_tag), css_ident_str(box1->tag));
                } else {
                    if (s->is_html)
                        html_eval_tag(s, box1);
                    s->box = box1->parent;
                }
            }
        }
    }
    return XML_STATE_TEXT;
}

// XXX: should take length?
static void flush_text(XMLState *s, const char *buf)
{
    CSSBox *box, *box1;

    box = s->box;
    /* if no current box, discard */
    if (!box)
        return;

    if (buf[0] == '\0')
        return;

    css_make_child_box(box);

    if (box->u.child.first != NULL) {
        /* non empty box: we add an anonymous box */
        box1 = css_new_box(CSS_ID_NIL, NULL);
        css_add_box(box, box1);
    } else {
        box1 = box;
    }
    // XXX: should pass length?
    css_set_text_string(box1, buf);
}

static void flush_text_buffer(XMLState *s, struct EditBuffer *b,
                              int offset0, int offset1)
{
    CSSBox *box, *box1;

    box = s->box;
    /* if no current box, discard */
    if (!box)
        return;

    if (offset0 >= offset1)
        return;

    css_make_child_box(box);

    if (box->u.child.first != NULL) {
        /* non empty box: we add an anonymous box */
        box1 = css_new_box(CSS_ID_NIL, NULL);
        css_add_box(box, box1);
    } else {
        box1 = box;
    }
    css_set_text_buffer(box1, b, offset0, offset1, 1);
}

static int xml_tagcmp(const char *s1, const char *s2) {
    while (*s2) {
        unsigned char uc1 = *s1++;
        unsigned char uc2 = *s2++;
        int d = uc2 - qe_tolower(uc1);
        if (d)
            return d;
    }
    /* XXX: Should only return 0 if (*s1) is separator */
    return 0;
}

static int xml_parse_internal(XMLState *s, const char *buf_start, int buf_len,
                              struct EditBuffer *b, int offset_start)
{
    int offset, offset0, text_offset_start, ret, offset_end;
    const char *buf_end, *buf;
    char32_t ch;

    buf = buf_start;
    buf_end = buf + buf_len;
    offset = offset_start;
    offset_end = offset_start + buf_len;
    offset0 = 0; /* not used */
    text_offset_start = 0; /* not used */
    for (;;) {
        if (buf) {
            if (buf >= buf_end)
                break;
            s->charset_state.p = (const u8*)buf;
            ch = s->charset_state.decode_func(&s->charset_state);
            buf = (const char *)s->charset_state.p;
        } else {
            if (offset >= offset_end)
                break;
            offset0 = offset;
            ch = eb_nextc(b, offset, &offset);
        }
        /* increment line number to signal errors */
        if (ch == '\n') {
            /* well, should add counter, but we test abort here */
            if (s->abort_func(s->abort_opaque))
                return -1;
            s->line_num++;
        }

        switch (s->state) {
        case XML_STATE_TAG:
            if (ch == '>') {
                strbuf_addch(&s->str, '\0');
                ret = parse_tag(s, s->str.buf);
                switch (ret) {
                default:
                case XML_STATE_TEXT:
                xml_text:
                    strbuf_reset(&s->str);
                    s->state = XML_STATE_TEXT;
                    text_offset_start = offset;
                    break;
                case XML_STATE_PRETAG:
                    strbuf_reset(&s->str);
                    s->state = XML_STATE_PRETAG;
                    text_offset_start = offset;
                    break;
                }
            } else {
                strbuf_addch(&s->str, ch);
                /* test comment */
                if (s->str.size == 3 &&
                    s->str.buf[0] == '!' &&
                    s->str.buf[1] == '-' &&
                    s->str.buf[2] == '-') {
                    s->state = XML_STATE_COMMENT;
                }
            }
            break;
        case XML_STATE_TEXT:
            if (ch == '<') {
                /* XXX: not strictly correct with comments : should
                   not flush if comment */
                if (buf) {
                    strbuf_addch(&s->str, '\0');
                    flush_text(s, s->str.buf);
                    strbuf_reset(&s->str);
                } else {
                    flush_text_buffer(s, b, text_offset_start, offset0);
                }
                s->state = XML_STATE_TAG;
            } else {
                if (buf) {
                    /* evaluate entities */
                    if (ch == '&') {
                        buf--;
                        ch = parse_entity(&buf);
                    }
                    strbuf_addch(&s->str, ch);
                }
            }
            break;
        case XML_STATE_COMMENT:
            if (ch == '-')
                s->state = XML_STATE_COMMENT1;
            break;
        case XML_STATE_COMMENT1:
            if (ch == '-')
                s->state = XML_STATE_COMMENT2;
            else
                s->state = XML_STATE_COMMENT;
            break;
        case XML_STATE_COMMENT2:
            if (ch == '>') {
                goto xml_text;
            } else if (ch != '-') {
                s->state = XML_STATE_COMMENT;
            }
            break;
        case XML_STATE_PRETAG:
            {
                int len, taglen;

                strbuf_addch(&s->str, ch);
                taglen = s->pretaglen + 2;
                len = s->str.size - taglen;
                if (len >= 0 &&
                    s->str.buf[len] == '<' &&
                    s->str.buf[len + 1] == '/' &&
                    !xml_tagcmp(s->str.buf + len + 2, s->pretag)) {
                    s->str.buf[len] = '\0';

                    if (!xml_tagcmp(s->pretag, "style")) {
                        if (s->style_sheet) {
                            CSSParseState b1, *bp = &b1;
                            bp->error_opaque = s->error_opaque;
                            bp->ptr = s->str.buf;
                            bp->filename = s->filename;
                            bp->line_num = s->line_num; /* XXX: incorrect */
                            bp->ignore_case = s->ignore_case;
                            css_parse_style_sheet(s->style_sheet, bp);
                        }
                    } else if (!xml_tagcmp(s->pretag, "script")) {
                        /* XXX: handle script */
                    } else {
                        /* just add the content */
                        if (buf) {
                            flush_text(s, s->str.buf);
                        } else {
                            /* XXX: would be incorrect if non ascii chars */
                            flush_text_buffer(s, b, text_offset_start, offset - taglen);
                        }
                        strbuf_reset(&s->str);
                        if (s->box)
                            s->box = s->box->parent;
                    }
                    s->state = XML_STATE_WAIT_EOT;
                }
            }
            break;
        case XML_STATE_WAIT_EOT:
            /* wait end of tag */
            if (ch == '>')
                    goto xml_text;
            break;
        }
    }
    /* CG: incorrect if parsing from buffer */
    return buf - buf_start;
}

int xml_parse(XMLState *s, char *buf, int buf_len)
{
    int len, len1;

    if (s->lookahead_size > 0) {
        /* fill the lookahead buffer with the content of 'buf' and
           handle all chars which are in lookahead_buf */
        len = LOOKAHEAD_SIZE - 1;
        if (len > buf_len)
            len = buf_len;
        memcpy(s->lookahead_buf + s->lookahead_size, buf, len);
        len += s->lookahead_size; /* total chars in lookahead buffer */

        /* parse only if enough chars for lookahead */
        len -= (LOOKAHEAD_SIZE - 1);
        if (len > 0) {
            len = xml_parse_internal(s, s->lookahead_buf, len, NULL, 0);
            if (len < 0)
                return -1;
            len1 = len - s->lookahead_size;
            if (len1 <= 0) {
                s->lookahead_size -= len;
                memmove(s->lookahead_buf, s->lookahead_buf + len, s->lookahead_size);
            } else {
                buf_len -= len1;
                buf += len1;
                s->lookahead_size = 0;
            }
            /* if could not parse everything, no need to go further */
            if (s->lookahead_size > 0)
                return 0;
        } else {
            /* all chars were put in lookahead buffer */
            s->lookahead_size = len + (LOOKAHEAD_SIZE - 1);
            return 0;
        }
    }

    /* now no chars are left in the lookahead buffer so we can parse at full speed */
    len1 = buf_len - (LOOKAHEAD_SIZE - 1);
    if (len1 > 0) {
        len = xml_parse_internal(s, buf, len1, NULL, 0);
        if (len < 0)
            return -1;
        buf_len -= len;
        buf += len;
    }

    /* put all the remaining chars in the lookahead buffer */
    memcpy(s->lookahead_buf, buf, buf_len);
    s->lookahead_size = buf_len;
    return 0;
}


CSSBox *xml_end(XMLState **sp)
{
    XMLState *s = *sp;
    CSSBox *root_box;

    if (!s)
        return NULL;

    /* flush the lookahead buffer */
    if (s->lookahead_size > 0) {
        /* mark the end to stop parsing function */
        s->lookahead_buf[s->lookahead_size] = '\0';
        xml_parse_internal(s, s->lookahead_buf, s->lookahead_size, NULL, 0);
    }

    if (s->charset) {
        charset_decode_close(&s->charset_state);
        s->charset = NULL;
    }

    strbuf_reset(&s->str);
    root_box = s->root_box;

    qe_free(&s);
    return root_box;
}

// XXX: move to qe source tree
// XXX: should pass pass function to read a code point to xml_parse_internal, using an opaque
/* XML in edit buffer parsing */
CSSBox *xml_parse_buffer(struct EditBuffer *b, const char *name,
                         int offset_start, int offset_end,
                         CSSStyleSheet *style_sheet, int flags,
                         CSSAbortFunc *abort_func, void *abort_opaque,
                         void *error_opaque)
{
    XMLState *s;
    CSSBox *box;
    int ret;

    s = xml_begin(style_sheet, flags, abort_func, abort_opaque, error_opaque, name, NULL);
    ret = xml_parse_internal(s, NULL, offset_end - offset_start, b, offset_start);
    box = xml_end(&s);
    if (ret < 0) {
        css_delete_box(&box);
        return NULL;
    }
    return box;
}
