/***************************************************************************
 *   crengine-ng                                                           *
 *   Copyright (C) 2007-2012,2014,2015 Vadim Lopatin <coolreader.org@gmail.com>
 *   Copyright (C) 2012 flobber                                            *
 *   Copyright (C) 2014 Huang Xin <chrox.huang@gmail.com>                  *
 *   Copyright (C) 2015-2017 Yifei(Frank) ZHU <fredyifei@gmail.com>        *
 *   Copyright (C) 2016 Bob Gordon <bobgordon62@gmail.com>                 *
 *   Copyright (C) 2018 EXL <exlmotodev@gmail.com>                         *
 *   Copyright (C) 2019 fenuks <fenuks@o2.pl>                              *
 *   Copyright (C) 2021 ourairquality <info@ourairquality.org>             *
 *   Copyright (C) 2017-2021 poire-z <poire-z@users.noreply.github.com>    *
 *   Copyright (C) 2018,2020,2021 Aleksey Chernov <valexlin@gmail.com>     *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or         *
 *   modify it under the terms of the GNU General Public License           *
 *   as published by the Free Software Foundation; either version 2        *
 *   of the License, or (at your option) any later version.                *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the Free Software           *
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,            *
 *   MA 02110-1301, USA.                                                   *
 ***************************************************************************/

/**
 * \file lvstsheet.cpp
 * \brief style sheet implementation
 */

#include <lvstsheet.h>
#include <ldomnode.h>
#include <lxmldocbase.h>
#include <fb2def.h>
#include <lvstreamutils.h>
#include <lvrend.h> // for -cr-only-if:
#include <lvdocprops.h>
#include <lvserialbuf.h>
#include <crbookformats.h>

#include "lvxml/lvxmlutils.h"
#include "lvtinydom/renderrectaccessor.h"
#include "textlang.h"
#include "lvrend.h" // for getGenericFontFamilyFace()

// define to dump all tokens
//#define DUMP_CSS_PARSING

#define IMPORTANT_DECL_HIGHER ((lUInt32)0x80000000U) // | to prop_code
#define IMPORTANT_DECL_SET    ((lUInt32)0x40000000U) // | to prop_code
#define IMPORTANT_DECL_REMOVE ((lUInt32)0x3FFFFFFFU) // & to prop_code
#define IMPORTANT_DECL_SHIFT  30                     // >> from prop_code to get 2 bits (importance, is_important)

enum css_decl_code
{
    cssd_unknown,
    cssd_display,
    cssd_white_space,
    cssd_text_align,
    cssd_text_align_last,
    cssd_text_align_last2, // -epub-text-align-last (mentioned in early versions of EPUB3)
    cssd_text_decoration,
    cssd_text_decoration2, // -epub-text-decoration (WebKit css extension)
    cssd_text_transform,
    cssd_hyphenate,  // hyphens (proper css property name)
    cssd_hyphenate2, // -webkit-hyphens (used by authors as an alternative to adobe-hyphenate)
    cssd_hyphenate3, // adobe-hyphenate (used by late Adobe RMSDK)
    cssd_hyphenate4, // adobe-text-layout (used by earlier Adobe RMSDK)
    cssd_hyphenate5, // hyphenate (fb2? used in obsoleted css files))
    cssd_hyphenate6, // -epub-hyphens (mentioned in early versions of EPUB3)
    cssd_color,
    cssd_border_top_color,
    cssd_border_right_color,
    cssd_border_bottom_color,
    cssd_border_left_color,
    cssd_background_color,
    cssd_vertical_align,
    cssd_font_family, // id families like serif, sans-serif
    cssd_font_names,  // string font name like Arial, Courier
    cssd_font_size,
    cssd_font_style,
    cssd_font_weight,
    cssd_font_features,           // font-feature-settings (not yet parsed)
    cssd_font_variant,            // all these are parsed specifically and mapped into
    cssd_font_variant_ligatures,  // the same style->font_features 31 bits bitmap
    cssd_font_variant_ligatures2, // -webkit-font-variant-ligatures (former Webkit property)
    cssd_font_variant_caps,
    cssd_font_variant_position,
    cssd_font_variant_numeric,
    cssd_font_variant_east_asian,
    cssd_font_variant_alternates,
    cssd_text_indent,
    cssd_line_height,
    cssd_letter_spacing,
    cssd_width,
    cssd_height,
    cssd_min_width,
    cssd_min_height,
    cssd_max_width,
    cssd_max_height,
    cssd_margin_left,
    cssd_margin_right,
    cssd_margin_top,
    cssd_margin_bottom,
    cssd_margin,
    cssd_padding_left,
    cssd_padding_right,
    cssd_padding_top,
    cssd_padding_bottom,
    cssd_padding,
    cssd_page_break_before, // Historical, but common, page break properties names
    cssd_page_break_after,
    cssd_page_break_inside,
    cssd_break_before, // Newest page break properties names
    cssd_break_after,
    cssd_break_inside,
    cssd_list_style,
    cssd_list_style_type,
    cssd_list_style_position,
    cssd_list_style_image,
    cssd_border_top_style,
    cssd_border_top_width,
    cssd_border_right_style,
    cssd_border_right_width,
    cssd_border_bottom_style,
    cssd_border_bottom_width,
    cssd_border_left_style,
    cssd_border_left_width,
    cssd_border_style,
    cssd_border_width,
    cssd_border_color,
    cssd_border,
    cssd_border_top,
    cssd_border_right,
    cssd_border_bottom,
    cssd_border_left,
    cssd_background,
    cssd_background_image,
    cssd_background_repeat,
    cssd_background_position,
    cssd_background_size,
    cssd_background_size2, // -webkit-background-size (former Webkit property)
    cssd_border_collapse,
    cssd_border_spacing,
    cssd_orphans,
    cssd_widows,
    cssd_float,
    cssd_clear,
    cssd_direction,
    cssd_visibility,
    cssd_line_break,
    cssd_line_break2,
    cssd_line_break3,
    cssd_word_break,
    cssd_content,
    cssd_cr_ignore_if_dom_version_greater_or_equal,
    cssd_cr_hint,
    cssd_cr_only_if,
    cssd_stop
};

static const char* css_decl_name[] = {
    "",
    "display",
    "white-space",
    "text-align",
    "text-align-last",
    "-epub-text-align-last",
    "text-decoration",
    "-epub-text-decoration",
    "text-transform",
    "hyphens",
    "-webkit-hyphens",
    "adobe-hyphenate",
    "adobe-text-layout",
    "hyphenate",
    "-epub-hyphens",
    "color",
    "border-top-color",
    "border-right-color",
    "border-bottom-color",
    "border-left-color",
    "background-color",
    "vertical-align",
    "font-family",
    "$dummy-for-font-names$",
    "font-size",
    "font-style",
    "font-weight",
    "font-feature-settings",
    "font-variant",
    "font-variant-ligatures",
    "-webkit-font-variant-ligatures",
    "font-variant-caps",
    "font-variant-position",
    "font-variant-numeric",
    "font-variant-east-asian",
    "font-variant-alternates",
    "text-indent",
    "line-height",
    "letter-spacing",
    "width",
    "height",
    "min-width",
    "min-height",
    "max-width",
    "max-height",
    "margin-left",
    "margin-right",
    "margin-top",
    "margin-bottom",
    "margin",
    "padding-left",
    "padding-right",
    "padding-top",
    "padding-bottom",
    "padding",
    "page-break-before",
    "page-break-after",
    "page-break-inside",
    "break-before",
    "break-after",
    "break-inside",
    "list-style",
    "list-style-type",
    "list-style-position",
    "list-style-image",
    "border-top-style",
    "border-top-width",
    "border-right-style",
    "border-right-width",
    "border-bottom-style",
    "border-bottom-width",
    "border-left-style",
    "border-left-width",
    "border-style",
    "border-width",
    "border-color",
    "border",
    "border-top",
    "border-right",
    "border-bottom",
    "border-left",
    "background",
    "background-image",
    "background-repeat",
    "background-position",
    "background-size",
    "-webkit-background-size",
    "border-collapse",
    "border-spacing",
    "orphans",
    "widows",
    "float",
    "clear",
    "direction",
    "visibility",
    "line-break",
    "-epub-line-break",
    "-webkit-line-break",
    "word-break",
    "content",
    "-cr-ignore-if-dom-version-greater-or-equal",
    "-cr-hint",
    "-cr-only-if",
    NULL
};

// See https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes
enum LVCssSelectorPseudoClass
{
    csspc_root,          // :root
    csspc_dir,           // :dir(rtl), :dir(ltr)
    csspc_first_child,   // :first-child
    csspc_first_of_type, // :first-of-type
    csspc_nth_child,     // :nth-child(even), :nth-child(3n+4)
    csspc_nth_of_type,   // :nth-of-type()
    // Those after this won't be valid when checked in the initial
    // document loading phase when the XML is being parsed, as at
    // this point, the checked node is always the last node as we
    // haven't yet parsed its following siblings. When meeting one,
    // we'll need to re-render and re-check styles after load with
    // a fully built DOM.
    csspc_last_child,       // :last-child
    csspc_last_of_type,     // :last-of-type
    csspc_nth_last_child,   // :nth-last-child()
    csspc_nth_last_of_type, // :nth-last-of-type()
    csspc_only_child,       // :only-child
    csspc_only_of_type,     // :only-of-type
    csspc_empty,            // :empty
};

static const char* css_pseudo_classes[] = {
    "root",
    "dir",
    "first-child",
    "first-of-type",
    "nth-child",
    "nth-of-type",
    "last-child",
    "last-of-type",
    "nth-last-child",
    "nth-last-of-type",
    "only-child",
    "only-of-type",
    "empty",
    NULL
};

// https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
enum LVCssSelectorPseudoElement
{
    csspe_before = 1, // ::before
    csspe_after = 2,  // ::after
};

static const char* css_pseudo_elements[] = {
    "before",
    "after",
    NULL
};

inline bool css_is_alpha(char ch) {
    return ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch == '-') || (ch == '_'));
}

inline bool css_is_alnum(char ch) {
    return (css_is_alpha(ch) || (ch >= '0' && ch <= '9'));
}

static int substr_compare(const char* sub, const char*& str) {
    int j;
    for (j = 0; sub[j] == str[j] && sub[j] && str[j]; j++)
        ;
    if (!sub[j]) {
        //bool last_alpha = css_is_alpha( sub[j-1] );
        //bool next_alnum = css_is_alnum( str[j] );
        if (!css_is_alpha(sub[j - 1]) || !css_is_alnum(str[j])) {
            str += j;
            return j;
        }
    }
    return 0;
}

inline char toLower(char c) {
    if (c >= 'A' && c <= 'Z')
        return c - 'A' + 'a';
    return c;
}

static int substr_icompare(const char* sub, const char*& str) {
    int j;

    // Small optimisation: don't toLower() 'sub', as all the harcoded values
    // we compare with are lowercase in this file.
    // for ( j=0; toLower(sub[j]) == toLower(str[j]) && sub[j] && str[j]; j++)
    for (j = 0; sub[j] == toLower(str[j]) && sub[j] && str[j]; j++)
        ;
    if (!sub[j]) {
        //bool last_alpha = css_is_alpha( sub[j-1] );
        //bool next_alnum = css_is_alnum( str[j] );
        if (!css_is_alpha(sub[j - 1]) || !css_is_alnum(str[j])) {
            str += j;
            return j;
        }
    }
    return 0;
}

static bool skip_spaces(const char*& str) {
    const char* oldpos = str;
    for (;;) {
        while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r')
            str++;
        if (*str == '/' && str[1] == '*') {
            // comment found
            while (*str && str[1] && (str[0] != '*' || str[1] != '/'))
                str++;
            if (*str == '*' && str[1] == '/')
                str += 2;
        }
        while (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r')
            str++;
        if (oldpos == str)
            break;
        if (*str == 0)
            return false;
        oldpos = str;
    }
    return *str != 0;
}

static css_decl_code parse_property_name(const char*& res) {
    const char* str = res;
    for (int i = 1; css_decl_name[i]; i++) {
        if (substr_icompare(css_decl_name[i], str)) // css property case should not matter (eg: "Font-Weight:")
        {
            // found!
            skip_spaces(str);
            if (substr_compare(":", str)) {
#ifdef DUMP_CSS_PARSING
                CRLog::trace("property name: %s", lString8(res, str - res).c_str());
#endif
                skip_spaces(str);
                res = str;
                return (css_decl_code)i;
            }
        }
    }
    return cssd_unknown;
}

static int parse_name(const char*& str, const char** names, int def_value) {
    for (int i = 0; names[i]; i++) {
        if (substr_icompare(names[i], str)) // css named value case should not matter (eg: "BOLD")
        {
            // found!
            return i;
        }
    }
    return def_value;
}

static lUInt32 parse_important(const char* str) // does not advance the original *str
{
    skip_spaces(str);
    // "!  important", with one or more spaces in between, is valid
    if (*str == '!') {
        str++;
        skip_spaces(str);
        if (substr_icompare("important", str)) {
            // returns directly what should be | to prop_code
            return IMPORTANT_DECL_SET;
        }
    }
    return 0;
}

static bool next_property(const char*& str) {
    // todo:
    // https://www.w3.org/TR/CSS2/syndata.html#parsing-errors
    // User agents must handle unexpected tokens encountered while
    // parsing a declaration by reading until the end of the
    // declaration, while observing the rules for matching pairs
    // of (), [], {}, "", and '', and correctly handling escapes.
    while (*str && *str != ';' && *str != '}')
        str++;
    if (*str == ';')
        str++;
    return skip_spaces(str);
}

static bool next_token(const char*& str) {
    // todo: as for next_property()
    while (*str && *str != ';' && *str != '}' && *str != ' ')
        str++;
    if (*str == ' ') {
        if (skip_spaces(str)) {
            if (*str && *str != ';' && *str != '}')
                // Something else before next property or end of declaration
                return true;
        }
    }
    return false;
}

static bool parse_uint(const char*& str, unsigned int& value) {
    skip_spaces(str);
    if (*str < '0' || *str > '9') {
        return false; // not a number
    }
    value = 0;
    while (*str >= '0' && *str <= '9') {
        value = value * 10 + (*str - '0');
        str++;
    }
    return true;
}

bool parse_number_value(const char*& str, css_length_t& value,
                        bool accept_percent,  // Defaults to true
                        bool accept_negative, // This and next ones default to false
                        bool accept_auto,
                        bool accept_none,
                        bool accept_normal,
                        bool accept_contain_cover,
                        bool is_font_size) {
    const char* orig_pos = str;
    value.type = css_val_unspecified;
    skip_spaces(str);
    // Here and below: named values and unit case should not matter
    if (substr_icompare("inherit", str)) {
        value.type = css_val_inherited;
        value.value = 0;
        return true;
    }
    if (accept_auto && substr_icompare("auto", str)) {
        value.type = css_val_unspecified;
        value.value = css_generic_auto;
        return true;
    }
    if (accept_none && substr_icompare("none", str)) {
        value.type = css_val_unspecified;
        value.value = css_generic_none;
        return true;
    }
    if (accept_normal && substr_icompare("normal", str)) {
        value.type = css_val_unspecified;
        value.value = css_generic_normal;
        return true;
    }
    if (accept_contain_cover) {
        if (substr_icompare("contain", str)) {
            value.type = css_val_unspecified;
            value.value = css_generic_contain;
            return true;
        }
        if (substr_icompare("cover", str)) {
            value.type = css_val_unspecified;
            value.value = css_generic_cover;
            return true;
        }
    }
    if (is_font_size) {
        // Absolute-size keywords, based on the default font size (which is medium)
        // Factors as suggested in https://drafts.csswg.org/css-fonts-3/#absolute-size-value
        if (substr_icompare("medium", str)) {
            value.type = css_val_rem;
            value.value = 256;
            return true;
        } else if (substr_icompare("small", str)) {
            value.type = css_val_rem;
            value.value = (int)(256 * 8 / 9);
            return true;
        } else if (substr_icompare("x-small", str)) {
            value.type = css_val_rem;
            value.value = (int)(256 * 3 / 4);
            return true;
        } else if (substr_icompare("xx-small", str)) {
            value.type = css_val_rem;
            value.value = (int)(256 * 3 / 5);
            return true;
        } else if (substr_icompare("large", str)) {
            value.type = css_val_rem;
            value.value = (int)(256 * 6 / 5);
            return true;
        } else if (substr_icompare("x-large", str)) {
            value.type = css_val_rem;
            value.value = (int)(256 * 3 / 2);
            return true;
        } else if (substr_icompare("xx-large", str)) {
            value.type = css_val_rem;
            value.value = (int)(256 * 2);
            return true;
        }
        // Approximate the (usually uneven) gaps between named sizes.
        else if (substr_icompare("smaller", str)) {
            value.type = css_val_percent;
            value.value = 80 << 8;
            return true;
        } else if (substr_icompare("larger", str)) {
            value.type = css_val_percent;
            value.value = 125 << 8;
            return true;
        }
    }
    bool negative = false;
    if (accept_negative) {
        if (*str == '-') {
            str++;
            negative = true;
        }
    }
    int n = 0;
    if (*str != '.') {
        if (*str < '0' || *str > '9') {
            str = orig_pos; // revert our possible str++
            return false;   // not a number
        }
        while (*str >= '0' && *str <= '9') {
            n = n * 10 + (*str - '0');
            str++;
        }
    }
    int frac = 0;
    int frac_div = 1;
    if (*str == '.') {
        str++;
        while (*str >= '0' && *str <= '9') {
            // don't process more than 6 digits after decimal point
            // to avoid overflow in case of very long values
            if (frac_div < 1000000) {
                frac = frac * 10 + (*str - '0');
                frac_div *= 10;
            }
            str++;
        }
    }
    if (substr_icompare("em", str))
        value.type = css_val_em;
    else if (substr_icompare("pt", str))
        value.type = css_val_pt;
    else if (substr_icompare("ex", str))
        value.type = css_val_ex;
    else if (substr_icompare("ch", str))
        value.type = css_val_ch;
    else if (substr_icompare("rem", str))
        value.type = css_val_rem;
    else if (substr_icompare("px", str))
        value.type = css_val_px;
    else if (substr_icompare("in", str))
        value.type = css_val_in;
    else if (substr_icompare("cm", str))
        value.type = css_val_cm;
    else if (substr_icompare("mm", str))
        value.type = css_val_mm;
    else if (substr_icompare("pc", str))
        value.type = css_val_pc;
    else if (substr_icompare("%", str)) {
        if (accept_percent)
            value.type = css_val_percent;
        else {
            str = orig_pos; // revert our possible str++
            return false;
        }
    } else if (substr_icompare("vw", str))
        value.type = css_val_vw;
    else if (substr_icompare("vh", str))
        value.type = css_val_vh;
    else if (substr_icompare("vmin", str))
        value.type = css_val_vmin;
    else if (substr_icompare("vmax", str))
        value.type = css_val_vmax;
    else if (n == 0 && frac == 0)
        value.type = css_val_px;
    // allow unspecified unit (for line-height)
    // else
    //    return false;

    // The largest frac here is 999999, limited above, with a frac_div of
    // 1000000, and even scaling it by 256 it does not overflow a 32 bit
    // integer. The frac_div is a power of 10 so always divisible by 2 without
    // loss when frac is non-zero.
    value.value = n * 256 + (256 * frac + frac_div / 2) / frac_div; // *256
    if (negative)
        value.value = -value.value;
    return true;
}

static lString32 parse_nth_value(const lString32 value) {
    // https://developer.mozilla.org/en-US/docs/Web/CSS/:nth-child
    // Parse "even", "odd", "5", "5n", "5n+2", "-n"...
    // Pack 3 numbers, enough to check if match, into another lString32
    // for quicker checking:
    // - a tuple of 3 lChar32: (negative, n-step, offset)
    // - or the empty string when invalid or if it would never match
    // (Note that we get the input already trimmed and lowercased.)
    lString32 ret = lString32(); // empty string = never match
    if (value == "even") {       //  = "2n"
        ret << lChar32(0) << lChar32(2) << lChar32(0);
        return ret;
    }
    if (value == "odd") { // = "2n+1"
        ret << lChar32(0) << lChar32(2) << lChar32(1);
        return ret;
    }
    int len = value.length();
    if (len == 0)   // empty value
        return ret; // invalid
    bool negative = false;
    int first = 0;
    int second = 0;
    int i = 0;
    lChar32 c;
    c = value[i];
    if (c == '-') {
        negative = true;
        i++;
    }
    if (i == len)   // no follow up content
        return ret; // invalid
    c = value[i];
    if (c == 'n') { // 'n' or '-n' without a leading number
        first = 1;
    } else {
        // Parse first number
        if (c < '0' || c > '9') // not a digit
            return ret;         // invalid
        while (true) {          // grab digit(s)
            first = first * 10 + (c - '0');
            i++;                // pass by this digit
            if (i == len) {     // single number seen: this parsed number is actually the offset
                if (negative)   // "-4"
                    return ret; // never match
                ret << lChar32(0) << lChar32(0) << lChar32(first);
                return ret;
            }
            c = value[i];
            if (c < '0' || c > '9') // done grabbing first digits
                break;
        }
        if (c != 'n')   // invalid char after first number
            return ret; // invalid
    }
    i++;                            // pass by that 'n'
    if (i == len) {                 // ends with that 'n'
        if (negative || first == 0) // valid, but would never match anything
            return ret;             // never match
        ret << lChar32(0) << lChar32(first) << lChar32(0);
        return ret;
    }
    c = value[i];
    if (c != '+')   // follow up content must start with a '+'
        return ret; // invalid
    i++;            // pass b y that '+'
    if (i == len)   // ends with that '+'
        return ret; // invalid
    // Parse second number
    c = value[i];
    if (c < '0' || c > '9') // not a digit
        return ret;         // invalid
    while (true) {          // grab digit(s)
        second = second * 10 + (c - '0');
        i++;          // pass by this digit
        if (i == len) // end of string, fully valid
            break;
        c = value[i];
        if (c < '0' || c > '9') // expected a digit (invalid stuff at end of value)
            return ret;         // invalid
    }
    // Valid, and we parsed everything
    ret << lChar32(negative) << lChar32(first) << lChar32(second);
    return ret;
}

static bool match_nth_value(const lString32 value, int n) {
    // Apply packed parsed value (parsed by above function) to n
    if (value.empty()) // invalid, or never match
        return false;
    bool negative = value[0];
    int step = value[1];
    int offset = value[2];
    if (step == 0)
        return n == offset;
    if (negative)
        n = offset - n;
    else
        n = n - offset;
    if (n < 0)
        return false;
    return n % step == 0;
}

// For some expensive LVCssSelectorRule::check() checks, that might
// be done on a node for multiple rules and would give the same
// result, we can cache the result in the node's RenderRectAccessor(),
// which is not used at this point and will be reset and cleared after
// all styles have been applied, before rendering methods are set.
// This is mostly useful for the :zzz-child pseudoclasses checks
// that involve using the expensive getUnboxedSibling methods; we
// are sure that no boxing is done when applying stylesheets, so
// the position among the parent children collection is stable
// and can be cached.
// Note that we can't cache the value 0: a field with value 0 means
// it has not yet been cached (we could tweak it before caching if
// storing 0 is needed).
static void cache_node_checked_property(const ldomNode* node, int property, int value) {
    RenderRectAccessor fmt((ldomNode*)node);
    if (!RENDER_RECT_HAS_FLAG(fmt, TEMP_USED_AS_CSS_CHECK_CACHE)) {
        // Clear it from past rendering stuff: we're processing stylesheets,
        // which means we will soon re-render the whole DOM and have it cleared
        // and updated. We can use it for caching other stuff until then.
        fmt.clear();
        RENDER_RECT_SET_FLAG(fmt, TEMP_USED_AS_CSS_CHECK_CACHE);
    }
    switch (property) {
        // Positive integer >= 1: needs a int field
        case csspc_nth_child:
            fmt.setY(value);
            break;
        case csspc_nth_of_type:
            fmt.setHeight(value);
            break;
        case csspc_nth_last_child:
            fmt.setTopOverflow(value);
            break;
        case csspc_nth_last_of_type:
            fmt.setBottomOverflow(value);
            break;
        // Boolean (1 means false, 2 means true): fine in a short int field
        case csspc_first_child:
            fmt.setX(value);
            break;
        case csspc_first_of_type:
            fmt.setWidth(value);
            break;
        case csspc_last_child:
            fmt.setInnerWidth(value);
            break;
        case csspc_last_of_type:
            fmt.setInnerX(value);
            break;
        case csspc_only_child:
            fmt.setInnerY(value);
            break;
        case csspc_only_of_type:
            fmt.setBaseline(value);
            break;
        default:
            break;
    }
}

static bool get_cached_node_checked_property(const ldomNode* node, int property, int& value) {
    RenderRectAccessor fmt((ldomNode*)node);
    if (!RENDER_RECT_HAS_FLAG(fmt, TEMP_USED_AS_CSS_CHECK_CACHE))
        return false; // nothing cached yet
    bool res = false;
    switch (property) {
        // Positive integer >= 1
        case csspc_nth_child:
            value = fmt.getY();
            res = value != 0;
            break;
        case csspc_nth_of_type:
            value = fmt.getHeight();
            res = value != 0;
            break;
        case csspc_nth_last_child:
            value = fmt.getTopOverflow();
            res = value != 0;
            break;
        case csspc_nth_last_of_type:
            value = fmt.getBottomOverflow();
            res = value != 0;
            break;
        // Boolean (1 means false, 2 means true)
        case csspc_first_child:
            value = fmt.getX();
            res = value != 0;
            break;
        case csspc_first_of_type:
            value = fmt.getWidth();
            res = value != 0;
            break;
        case csspc_last_child:
            value = fmt.getInnerWidth();
            res = value != 0;
            break;
        case csspc_last_of_type:
            value = fmt.getInnerX();
            res = value != 0;
            break;
        case csspc_only_child:
            value = fmt.getInnerY();
            res = value != 0;
            break;
        case csspc_only_of_type:
            value = fmt.getBaseline();
            res = value != 0;
            break;
        default:
            break;
    }
    return res;
}

struct standard_color_t
{
    const char* name;
    lUInt32 color;
};

standard_color_t standard_color_table[] = {
    { "aliceblue", 0xf0f8ff },
    { "antiquewhite", 0xfaebd7 },
    { "aqua", 0x00ffff },
    { "aquamarine", 0x7fffd4 },
    { "azure", 0xf0ffff },
    { "beige", 0xf5f5dc },
    { "bisque", 0xffe4c4 },
    { "black", 0x000000 },
    { "blanchedalmond", 0xffebcd },
    { "blue", 0x0000ff },
    { "blueviolet", 0x8a2be2 },
    { "brown", 0xa52a2a },
    { "burlywood", 0xdeb887 },
    { "cadetblue", 0x5f9ea0 },
    { "chartreuse", 0x7fff00 },
    { "chocolate", 0xd2691e },
    { "coral", 0xff7f50 },
    { "cornflowerblue", 0x6495ed },
    { "cornsilk", 0xfff8dc },
    { "crimson", 0xdc143c },
    { "cyan", 0x00ffff },
    { "darkblue", 0x00008b },
    { "darkcyan", 0x008b8b },
    { "darkgoldenrod", 0xb8860b },
    { "darkgray", 0xa9a9a9 },
    { "darkgreen", 0x006400 },
    { "darkgrey", 0xa9a9a9 },
    { "darkkhaki", 0xbdb76b },
    { "darkmagenta", 0x8b008b },
    { "darkolivegreen", 0x556b2f },
    { "darkorange", 0xff8c00 },
    { "darkorchid", 0x9932cc },
    { "darkred", 0x8b0000 },
    { "darksalmon", 0xe9967a },
    { "darkseagreen", 0x8fbc8f },
    { "darkslateblue", 0x483d8b },
    { "darkslategray", 0x2f4f4f },
    { "darkslategrey", 0x2f4f4f },
    { "darkturquoise", 0x00ced1 },
    { "darkviolet", 0x9400d3 },
    { "deeppink", 0xff1493 },
    { "deepskyblue", 0x00bfff },
    { "dimgray", 0x696969 },
    { "dimgrey", 0x696969 },
    { "dodgerblue", 0x1e90ff },
    { "firebrick", 0xb22222 },
    { "floralwhite", 0xfffaf0 },
    { "forestgreen", 0x228b22 },
    { "fuchsia", 0xff00ff },
    { "gainsboro", 0xdcdcdc },
    { "ghostwhite", 0xf8f8ff },
    { "gold", 0xffd700 },
    { "goldenrod", 0xdaa520 },
    { "gray", 0x808080 },
    { "green", 0x008000 },
    { "grey", 0x808080 },
    { "greenyellow", 0xadff2f },
    { "honeydew", 0xf0fff0 },
    { "hotpink", 0xff69b4 },
    { "indianred", 0xcd5c5c },
    { "indigo", 0x4b0082 },
    { "ivory", 0xfffff0 },
    { "khaki", 0xf0e68c },
    { "lavender", 0xe6e6fa },
    { "lavenderblush", 0xfff0f5 },
    { "lawngreen", 0x7cfc00 },
    { "lemonchiffon", 0xfffacd },
    { "lightblue", 0xadd8e6 },
    { "lightcoral", 0xf08080 },
    { "lightcyan", 0xe0ffff },
    { "lightgoldenrodyellow", 0xfafad2 },
    { "lightgray", 0xd3d3d3 },
    { "lightgreen", 0x90ee90 },
    { "lightgrey", 0xd3d3d3 },
    { "lightpink", 0xffb6c1 },
    { "lightsalmon", 0xffa07a },
    { "lightseagreen", 0x20b2aa },
    { "lightskyblue", 0x87cefa },
    { "lightslategray", 0x778899 },
    { "lightslategrey", 0x778899 },
    { "lightsteelblue", 0xb0c4de },
    { "lightyellow", 0xffffe0 },
    { "lime", 0x00ff00 },
    { "limegreen", 0x32cd32 },
    { "linen", 0xfaf0e6 },
    { "magenta", 0xff00ff },
    { "maroon", 0x800000 },
    { "mediumaquamarine", 0x66cdaa },
    { "mediumblue", 0x0000cd },
    { "mediumorchid", 0xba55d3 },
    { "mediumpurple", 0x9370db },
    { "mediumseagreen", 0x3cb371 },
    { "mediumslateblue", 0x7b68ee },
    { "mediumspringgreen", 0x00fa9a },
    { "mediumturquoise", 0x48d1cc },
    { "mediumvioletred", 0xc71585 },
    { "midnightblue", 0x191970 },
    { "mintcream", 0xf5fffa },
    { "mistyrose", 0xffe4e1 },
    { "moccasin", 0xffe4b5 },
    { "navajowhite", 0xffdead },
    { "navy", 0x000080 },
    { "oldlace", 0xfdf5e6 },
    { "olive", 0x808000 },
    { "olivedrab", 0x6b8e23 },
    { "orange", 0xffa500 },
    { "orangered", 0xff4500 },
    { "orchid", 0xda70d6 },
    { "palegoldenrod", 0xeee8aa },
    { "palegreen", 0x98fb98 },
    { "paleturquoise", 0xafeeee },
    { "palevioletred", 0xdb7093 },
    { "papayawhip", 0xffefd5 },
    { "peachpuff", 0xffdab9 },
    { "peru", 0xcd853f },
    { "pink", 0xffc0cb },
    { "plum", 0xdda0dd },
    { "powderblue", 0xb0e0e6 },
    { "purple", 0x800080 },
    { "rebeccapurple", 0x663399 },
    { "red", 0xff0000 },
    { "rosybrown", 0xbc8f8f },
    { "royalblue", 0x4169e1 },
    { "saddlebrown", 0x8b4513 },
    { "salmon", 0xfa8072 },
    { "sandybrown", 0xf4a460 },
    { "seagreen", 0x2e8b57 },
    { "seashell", 0xfff5ee },
    { "sienna", 0xa0522d },
    { "silver", 0xc0c0c0 },
    { "skyblue", 0x87ceeb },
    { "slateblue", 0x6a5acd },
    { "slategray", 0x708090 },
    { "slategrey", 0x708090 },
    { "snow", 0xfffafa },
    { "springgreen", 0x00ff7f },
    { "steelblue", 0x4682b4 },
    { "tan", 0xd2b48c },
    { "teal", 0x008080 },
    { "thistle", 0xd8bfd8 },
    { "tomato", 0xff6347 },
    { "turquoise", 0x40e0d0 },
    { "violet", 0xee82ee },
    { "wheat", 0xf5deb3 },
    { "white", 0xffffff },
    { "whitesmoke", 0xf5f5f5 },
    { "yellow", 0xffff00 },
    { "yellowgreen", 0x9acd32 },
    { NULL, 0 }
};

static int hexDigit(char c) {
    if (c >= '0' && c <= '9')
        return c - '0';
    if (c >= 'A' && c <= 'F')
        return c - 'A' + 10;
    if (c >= 'a' && c <= 'f')
        return c - 'a' + 10;
    return -1;
}

bool parse_color_value(const char*& str, css_length_t& value) {
    // Does not support "rgb(0, 127, 255)" nor "rgba(0,127,255)"
    const char* orig_pos = str;
    value.type = css_val_unspecified;
    skip_spaces(str);
    if (substr_icompare("transparent", str)) {
        // Make it an invalid color, but a valid parsing so it
        // can be inherited or flagged with !important
        value.type = css_val_unspecified;
        value.value = css_generic_transparent;
        return true;
    }
    if (substr_icompare("currentcolor", str)) {
        value.type = css_val_unspecified;
        value.value = css_generic_currentcolor;
        return true;
    }
    if (substr_compare("inherit", str)) {
        value.type = css_val_inherited;
        value.value = 0;
        return true;
    }
    if (substr_compare("none", str)) {
        value.type = css_val_unspecified;
        value.value = 0;
        return true;
    }
    if (*str == '#') {
        // #rgb or #rrggbb colors
        str++;
        int nDigits = 0;
        for (; hexDigit(str[nDigits]) >= 0; nDigits++)
            ;
        if (nDigits == 3) {
            int r = hexDigit(*str++);
            int g = hexDigit(*str++);
            int b = hexDigit(*str++);
            value.type = css_val_color;
            value.value = (((r + r * 16) * 256) | (g + g * 16)) * 256 | (b + b * 16);
            return true;
        } else if (nDigits == 6) {
            int r = hexDigit(*str++) * 16;
            r += hexDigit(*str++);
            int g = hexDigit(*str++) * 16;
            g += hexDigit(*str++);
            int b = hexDigit(*str++) * 16;
            b += hexDigit(*str++);
            value.type = css_val_color;
            value.value = ((r * 256) | g) * 256 | b;
            return true;
        } else {
            str = orig_pos; // revert our possible str++
            return false;
        }
    }
    for (int i = 0; standard_color_table[i].name != NULL; i++) {
        if (substr_icompare(standard_color_table[i].name, str)) {
            value.type = css_val_color;
            value.value = standard_color_table[i].color;
            return true;
        }
    }
    str = orig_pos; // revert our possible str++
    return false;
}

// Parse a CSS "content:" property into an intermediate format single string.
bool parse_content_property(const char*& str, lString32& parsed_content) {
    // https://developer.mozilla.org/en-US/docs/Web/CSS/content
    // The property may have multiple tokens:
    //   p::before { content: "[" attr(n) "]"; }
    //               content: "Qq. " attr(qq)
    //               content: '\201D\ In: ';
    // We can meet some bogus values: content: "&#x2219; ";
    // or values we don't support: Firefox would drop the whole
    // declaration, but, as we don't support all those from the
    // specs, we'll just ignore the tokens we don't support.
    // We parse the original content into a "parsed content" string,
    // consisting of a first letter, indicating its type, and if some
    // data: its length (+1 to avoid NULLs in strings) and that data.
    // parsed_content may contain multiple values, in the format
    //   'X' for 'none' (or 'normal', = none with pseudo elements)
    //   's' + <len> + string32 (string content) for ""
    //   'a' + <len> + string32 (attribute name) for attr()
    //   'Q' for 'open-quote'
    //   'q' for 'close-quote'
    //   'N' for 'no-open-quote'
    //   'n' for 'no-close-quote'
    //   'u' for 'url()', that we don't support
    //   'z' for unsupported tokens, like gradient()...
    //   '$' (at start) this content needs post processing before
    //       being applied to a node's style (needed with quotes,
    //       to get the correct char for the current nested level).
    // Note: this parsing might not be super robust with
    // convoluted declarations...
    parsed_content.clear();
    const char* orig_pos = str;
    // The presence of a single 'none' or 'normal' among multiple
    // values make the whole thing 'none'.
    bool has_none = false;
    bool needs_processing_when_applying = false;
    while (skip_spaces(str) && *str != ';' && *str != '}' && *str != '!') {
        if (substr_icompare("none", str)) {
            has_none = true;
            continue; // continue parsing
        } else if (substr_icompare("normal", str)) {
            // Computes to 'none' for pseudo elements
            has_none = true;
            continue; // continue parsing
        } else if (substr_icompare("open-quote", str)) {
            parsed_content << U'Q';
            needs_processing_when_applying = true;
            continue;
        } else if (substr_icompare("close-quote", str)) {
            parsed_content << U'q';
            needs_processing_when_applying = true;
            continue;
        } else if (substr_icompare("no-open-quote", str)) {
            parsed_content << U'N';
            needs_processing_when_applying = true;
            continue;
        } else if (substr_icompare("no-close-quote", str)) {
            parsed_content << U'n';
            needs_processing_when_applying = true;
            continue;
        } else if (substr_icompare("attr", str)) {
            if (*str == '(') {
                str++;
                skip_spaces(str);
                lString8 attr8;
                while (*str && *str != ')') {
                    attr8 << *str;
                    str++;
                }
                if (*str == ')') {
                    str++;
                    lString32 attr = Utf8ToUnicode(attr8);
                    attr.trim();
                    parsed_content << U'a';
                    parsed_content << lChar32(attr.length() + 1); // (+1 to avoid storing \x00)
                    parsed_content << attr;
                    continue;
                }
                // No closing ')': invalid
            }
        } else if (substr_icompare("url", str)) {
            // Unsupported for now, but parse it
            if (*str == '(') {
                str++;
                skip_spaces(str);
                lString8 url8;
                while (*str && *str != ')') {
                    url8 << *str;
                    str++;
                }
                if (*str == ')') {
                    str++;
                    parsed_content << U'u';
                    continue;
                }
                // No closing ')': invalid
            }
        } else if (*str == '"' || *str == '\'') {
            // https://developer.mozilla.org/en-US/docs/Web/CSS/string
            // https://www.w3.org/TR/CSS2/syndata.html#strings
            // https://drafts.csswg.org/css-values-3/#strings
            char quote_ch = *str;
            str++;
            lString8 str8; // quoted string content (as UTF8, like original stylesheet)
            while (*str && *str != quote_ch) {
                if (*str == '\\') {
                    // https://www.w3.org/TR/CSS2/syndata.html#characters
                    str++;
                    if (hexDigit(*str) >= 0) {
                        lUInt32 codepoint = 0;
                        int num_digits = 0;
                        while (num_digits < 6) {
                            int v = hexDigit(*str);
                            if (v >= 0) {
                                codepoint = (codepoint << 4) + v;
                                num_digits++;
                                str++;
                                continue;
                            }
                            // Not a hex digit
                            break;
                        }
                        if (num_digits < 6 && *str == ' ') // skip space following a non-6-hex-digits
                            str++;
                        if (codepoint == 0 || codepoint > 0x10FFFF) {
                            // zero not allowed, and should be under max valid unicode codepoint
                            codepoint = 0xFFFD; // replacement character
                        }
                        // Serialize it as UTF-8
                        lString32 c;
                        c << (lChar32)codepoint;
                        str8 << UnicodeToUtf8(c);
                    } else if (*str == '\r' && *(str + 1) == '\n') {
                        // Ignore \ at end of CRLF line
                        str += 2;
                    } else if (*str == '\n') {
                        // Ignore \ at end of line
                        str++;
                    } else {
                        // Accept next char as is
                        str8 << *str;
                        str++;
                    }
                } else {
                    str8 << *str;
                    str++;
                }
                // todo:
                // https://www.w3.org/TR/CSS2/syndata.html#parsing-errors
                // "User agents must close strings upon reaching the end
                // of a line (i.e., before an unescaped line feed, carriage
                // return or form feed character), but then drop the construct
                // (declaration or rule) in which the string was found."
            }
            if (*str == quote_ch) {
                lString32 str32 = Utf8ToUnicode(str8);
                parsed_content << U's';
                parsed_content << lChar32(str32.length() + 1); // (+1 to avoid storing \x00)
                parsed_content << str32;
                str++;
                continue;
            }
        } else {
            // Not supported
            parsed_content << U'z';
            next_token(str);
        }
    }
    if (has_none) {
        // Forget all other tokens parsed
        parsed_content.clear();
        parsed_content << U'X';
    } else if (needs_processing_when_applying) {
        parsed_content.insert(0, 1, U'$');
    }
    if (*str) // something (;, } or !important) follows
        return true;
    // Restore original position if we reach end of CSS string,
    // as it might just be missing a ')' or closing quote: we'll
    // be skipping up to next ; or }, and might manage with
    // the rest of the string.
    str = orig_pos;
    return false;
}

/// Update a style->content, post processed for its node
void update_style_content_property(css_style_rec_t* style, ldomNode* node) {
    // We don't want to update too much: styles are hashed and shared by
    // multiple nodes. We don't resolve "attr()" here as attributes are
    // stable (and "attr(id)" would make all style->content different
    // and prevent styles from being shared, increasing the number
    // of styles to cache).
    // But we need to resolve quotes, according to their nesting level,
    // and transform them into a litteral string 's'.

    if (style->content.empty() || style->content[0] != U'$') {
        // No update needed
        return;
    }

    // We need to know if this node is visible: if not, quotes nested
    // level should not be updated. We might want to still include
    // the computed quote (with quote char for level 1) for it to be
    // displayed by writeNodeEx() when displaying the HTML, even if
    // the node is invisible.
    bool visible = style->display != css_d_none;
    if (visible) {
        ldomNode* n = node->getParentNode();
        for (; !n->isRoot(); n = n->getParentNode()) {
            if (n->getStyle()->display == css_d_none) {
                visible = false;
                break;
            }
        }
    }

    // We do not support specifying quote chars to be used via CSS "quotes":
    //     :root { quotes: '\201c' '\201d' '\2018' '\2019'; }
    // We use the ones hardcoded for the node lang tag language (or default
    // typography language) provided by TextLangCfg.
    // HTML5 default CSS specifies them with:
    //   :root:lang(af), :not(:lang(af)) > :lang(af) { quotes: '\201c' '\201d' '\2018' '\2019' }
    // This might (or not) implies that nested levels are reset when entering
    // text with another language, so this new language first level quote is used.
    // We can actually get that same behaviour by having each TextLangCfg manage
    // its own nesting level (which won't be reset when en>fr>en, though).
    // But all this is quite rare, so don't bother about it much.
    TextLangCfg* lang_cfg = TextLangMan::getTextLangCfg(node);

    // Note: some quote char like (U+201C / U+201D) seem to not be mirrored
    // (when using HarfBuzz) when added to some RTL arabic text. But it
    // appears that way with Firefox too!
    // But if we use another char (U+00AB / U+00BB), it gets mirrored correctly.
    // Might be that HarfBuzz first substitute it with arabic quotes (which
    // happen to look inverted), and then mirror that?

    lString32 res;
    lString32 parsed_content = style->content;
    lString32 quote;
    int i = 1; // skip initial '$'
    int parsed_content_len = parsed_content.length();
    while (i < parsed_content_len) {
        lChar32 ctype = parsed_content[i];
        if (ctype == 's') {                      // literal string: copy as-is
            lChar32 len = parsed_content[i] - 1; // (remove added +1)
            res.append(parsed_content, i, len + 2);
            i += len + 2;
        } else if (ctype == 'a') {               // attribute value: copy as-is
            lChar32 len = parsed_content[i] - 1; // (remove added +1)
            res.append(parsed_content, i, len + 2);
            i += len + 2;
        } else if (ctype == 'Q') { // open-quote
            quote = lang_cfg->getOpeningQuote(visible);
            // length+1 as expected with 's' by get_applied_content_property()
            res << U's' << lChar32(quote.length() + 1) << quote;
            i += 1;
        } else if (ctype == 'q') { // close-quote
            quote = lang_cfg->getClosingQuote(visible);
            // length+1 as expected with 's' by get_applied_content_property()
            res << U's' << lChar32(quote.length() + 1) << quote;
            i += 1;
        } else if (ctype == 'N') { // no-open-quote
            // This should just increment nested quote level and output nothing.
            lang_cfg->getOpeningQuote(visible);
            i += 1;
        } else if (ctype == 'n') { // no-close-quote
            // This should just increment nested quote level and output nothing.
            lang_cfg->getClosingQuote(visible);
            i += 1;
        } else {
            // All other stuff are single char (u, z, X) or unsupported/bogus char.
            res.append(parsed_content, i, 1);
            i += 1;
        }
    }
    // Replace style->content with what we built
    style->content = res;
}

/// Returns the computed value for a node from its parsed CSS "content:" value
lString32 get_applied_content_property(ldomNode* node) {
    lString32 res;
    css_style_ref_t style = node->getStyle();
    lString32 parsed_content = style->content;
    if (parsed_content.empty())
        return res;
    int i = 0;
    int parsed_content_len = parsed_content.length();
    while (i < parsed_content_len) {
        lChar32 ctype = parsed_content[i++];
        if (ctype == 's') {                        // literal string
            lChar32 len = parsed_content[i++] - 1; // (remove added +1)
            res << parsed_content.substr(i, len);
            i += len;
        } else if (ctype == 'a') {                 // attribute value
            lChar32 len = parsed_content[i++] - 1; // (remove added +1)
            lString32 attr_name = parsed_content.substr(i, len);
            i += len;
            ldomNode* attrNode = node;
            if (node->getNodeId() == el_pseudoElem) {
                // For attributes, we should pick them from the parent of the added pseudo element
                attrNode = node->getUnboxedParent();
            }
            if (attrNode)
                res << attrNode->getAttributeValue(attr_name.c_str());
        } else if (ctype == 'u') { // url
            // Url to image: we can't easily support that, as our
            // image support needs a reference to a node, and we
            // don't have a node here.
            // Show a small square so one can see there's something
            // that is missing, something different enough from the
            // classic tofu char so we can distinguish it.
            // res << 0x25FD; // WHITE MEDIUM SMALL SQUARE
            res << 0x2B26;         // WHITE MEDIUM DIAMOND
        } else if (ctype == 'X') { // 'none'
            res.clear();           // should be standalone, but let's be sure
            break;
        } else if (ctype == 'z') { // unsupported token
            // Just ignore it, don't show anything
        } else if (ctype == 'Q') { // open-quote
            // Shouldn't happen: replaced earlier by update_style_content_property()
        } else if (ctype == 'q') { // close-quote
            // Shouldn't happen: replaced earlier by update_style_content_property()
        } else if (ctype == 'N') { // no-open-quote
            // Shouldn't happen: replaced earlier by update_style_content_property()
        } else if (ctype == 'n') { // no-close-quote
            // Shouldn't happen: replaced earlier by update_style_content_property()
        } else { // unexpected
            break;
        }
    }
    if (style->white_space < css_ws_pre_line) {
        // Remove consecutive spaces (although this might be handled well by
        // lvtextfm) and '\n' - but we should keep leading and trailing spaces.
        res.trimDoubleSpaces(true, true, false);
    }
    return res;
}

static void resolve_url_path(lString8& str, lString32 codeBase) {
    // A URL (path to local or container's file) must be resolved
    // at parsing time, as it is related to this stylesheet file
    // path (and not to the HTML files that are linking to this
    // stylesheet) - it wouldn't be possible to resolve it later.
    lString32 path = Utf8ToUnicode(str);
    path.trim();
    if (path.startsWithNoCase(lString32("url")))
        path = path.substr(3);
    path.trim();
    if (path.startsWith(U"("))
        path = path.substr(1);
    if (path.endsWith(U")"))
        path = path.substr(0, path.length() - 1);
    path.trim();
    if (path.startsWith(U"\"") || path.startsWith(U"'"))
        path = path.substr(1);
    if (path.endsWith(U"\"") || path.endsWith(U"'"))
        path = path.substr(0, path.length() - 1);
    path.trim();
    if (path.startsWith(lString32("data:image"))) {
        // base64 encoded image: leave as-is
    } else {
        // We assume it's a path to a local file in the container, so we don't try
        // to check if it's a remote url (as we can't fetch its content anyway).
        if (!codeBase.empty()) {
            path = LVCombinePaths(codeBase, path);
        }
    }
    // printf("url: [%s]+%s => %s\n", UnicodeToLocal(codeBase).c_str(), str.c_str(), UnicodeToUtf8(path).c_str());
    str = UnicodeToUtf8(path);
}

// The order of items in following tables should match the order in the enums in include/cssdef.h
static const char* css_d_names[] = {
    "inherit",
    "ruby",
    "run-in",
    "inline",
    "block",
    "-cr-list-item-final", // non-standard, legacy crengine rendering of list items as final: css_d_list_item_legacy
    "list-item",           // correct rendering of list items as block: css_d_list_item_block
    "inline-block",
    "inline-table",
    "table",
    "table-row-group",
    "table-header-group",
    "table-footer-group",
    "table-row",
    "table-column-group",
    "table-column",
    "table-cell",
    "table-caption",
    "none",
    NULL
};

static const char* css_ws_names[] = {
    "inherit",
    "normal",
    "nowrap",
    "pre-line",
    "pre",
    "pre-wrap",
    "break-spaces",
    NULL
};

static const char* css_ta_names[] = {
    "inherit",
    "left",
    "right",
    "center",
    "justify",
    "start",
    "end",
    "auto",
    "-cr-left-if-not-first",
    "-cr-right-if-not-first",
    "-cr-center-if-not-first",
    "-cr-justify-if-not-first",
    "-cr-start-if-not-first",
    "-cr-end-if-not-first",
    NULL
};

static const char* css_td_names[] = {
    "inherit",
    "none",
    "underline",
    "overline",
    "line-through",
    "blink",
    NULL
};

static const char* css_tt_names[] = {
    "inherit",
    "none",
    "uppercase",
    "lowercase",
    "capitalize",
    "full-width",
    NULL
};

// All these css_hyph_names* should map original properties in this order:
//  1st value: inherit
//  2nd value: no hyphenation
//  3rd value: use the hyphenation method set to HyphMan
// See https://github.com/readium/readium-css/blob/master/docs/CSS21-epub_compat.md
// for documentation about the obscure properties.
//
// For "hyphens:", "hyphenate:" (fb2? also used by obsoleted css files)
// No support for "hyphens: manual" (this would involve toggling the hyphenation
// method from what it is set to SoftHyphensHyph locally)
static const char* css_hyph_names[] = {
    "inherit",
    "none",
    "auto",
    NULL
};
// For "adobe-text-layout:" (for documents made for Adobe RMSDK)
static const char* css_hyph_names2[] = {
    "inherit",
    "optimizespeed",
    "optimizequality",
    NULL
};
// For "adobe-hyphenate:"
static const char* css_hyph_names3[] = {
    "inherit",
    "none",
    "explicit", // this may wrong, as it's supposed to be like "hyphens: manual"
    NULL
};

static const char* css_pb_names[] = {
    "inherit",
    "auto",
    "avoid", // those after this one are not supported by page-break-inside
    "always",
    "left",
    "right",
    "page",
    "recto",
    "verso",
    NULL
};

static const char* css_fs_names[] = {
    "inherit",
    "normal",
    "italic",
    "oblique",
    NULL
};

static const char* css_fw_names[] = {
    "inherit",
    "normal",
    "bold",
    "bolder",
    "lighter",
    "100",
    "200",
    "300",
    "400",
    "500",
    "600",
    "700",
    "800",
    "900",
    NULL
};
static const char* css_va_names[] = {
    "inherit",
    "baseline",
    "sub",
    "super",
    "top",
    "text-top",
    "middle",
    "bottom",
    "text-bottom",
    NULL
};

static const char* css_ti_attribute_names[] = {
    "hanging",
    NULL
};

static const char* css_ff_names[] = {
    "inherit",
    "serif",
    "sans-serif",
    "cursive",
    "fantasy",
    "monospace",
    NULL
};

static const char* css_lst_names[] = {
    "inherit",
    "disc",
    "circle",
    "square",
    "decimal",
    "lower-roman",
    "upper-roman",
    "lower-alpha",
    "upper-alpha",
    "none",
    NULL
};

static const char* css_lsp_names[] = {
    "inherit",
    "inside",
    "outside",
    NULL
};
///border style names
static const char* css_bst_names[] = {
    "solid",
    "dotted",
    "dashed",
    "double",
    "groove",
    "ridge",
    "inset",
    "outset",
    "none",
    NULL
};
///border width value names
static const char* css_bw_names[] = {
    "thin",
    "medium",
    "thick",
    "initial",
    "inherit",
    NULL
};

//background repeat names
static const char* css_bg_repeat_names[] = {
    "repeat",
    "repeat-x",
    "repeat-y",
    "no-repeat",
    "initial",
    "inherit",
    NULL
};
// TODO: delete css_bg_attachment_names?
#if 0
//background attachment names
static const char* css_bg_attachment_names[] = {
    "scroll",
    "fixed",
    "local",
    "initial",
    "inherit",
    NULL
};
#endif
//background position names
static const char* css_bg_position_names[] = {
    "left top", // 0
    "left center",
    "left bottom",
    "right top",
    "right center",
    "right bottom",
    "center top",
    "center center",
    "center bottom", // 8
    "top left",      // 9
    "center left",
    "bottom left",
    "top right",
    "center right",
    "bottom right",
    "top center",
    "center center",
    "bottom center", // 17
    "center",        // 18
    "left",
    "right",
    "top",
    "bottom",
    "initial", // 23
    "inherit", // 24
    NULL
};

//border-collpase names
static const char* css_bc_names[] = {
    "separate",
    "collapse",
    "initial",
    "inherit",
    NULL
};

// orphans and widows values (supported only if in range 1-9)
// https://drafts.csswg.org/css-break-3/#widows-orphans
//   "Negative values and zero are invalid and must cause the declaration to be ignored."
static const char* css_orphans_widows_names[] = {
    "inherit",
    "1",
    "2",
    "3",
    "4",
    "5",
    "6",
    "7",
    "8",
    "9",
    NULL
};

// float value names
static const char* css_f_names[] = {
    "inherit",
    "none",
    "left",
    "right",
    NULL
};

// clear value names
static const char* css_c_names[] = {
    "inherit",
    "none",
    "left",
    "right",
    "both",
    NULL
};

// direction value names
static const char* css_dir_names[] = {
    "inherit",
    "unset",
    "ltr",
    "rtl",
    NULL
};

// visibility value names
static const char* css_v_names[] = {
    "inherit",
    "visible",
    "hidden",
    "collapse",
    NULL
};

// line-break value names
static const char* css_lb_names[] = {
    "inherit",
    "auto",
    "normal",
    "loose",
    "strict",
    "anywhere",
    NULL
};

// word-break value names
static const char* css_wb_names[] = {
    "inherit",
    "normal",
    "break-word",
    "break-all",
    "keep-all",
    NULL
};

static const char* css_cr_only_if_names[] = {
    "any",
    "always",
    "never",
    "legacy",
    "enhanced",
    "float-floatboxes",
    "box-inlineboxes",
    "ensure-style-width",
    "ensure-style-height",
    "allow-style-w-h-absolute-units",
    "full-featured",
    "epub-document",
    "fb2-document",
    NULL
};
enum cr_only_if_t
{
    cr_only_if_any,    // always true, don't ignore
    cr_only_if_always, // always true, don't ignore
    cr_only_if_never,  // always false, do ignore
    cr_only_if_legacy,
    cr_only_if_enhanced,
    cr_only_if_float_floatboxes,
    cr_only_if_box_inlineboxes,
    cr_only_if_ensure_style_width,
    cr_only_if_ensure_style_height,
    cr_only_if_allow_style_w_h_absolute_units,
    cr_only_if_full_featured,
    cr_only_if_epub_document,
    cr_only_if_fb2_document, // fb2 or fb3
};

bool LVCssDeclaration::parse(const char*& decl, lUInt32 domVersionRequested, bool higher_importance, lxmlDocBase* doc, lString32 codeBase) {
    if (!decl)
        return false;
    skip_spaces(decl);
    if (*decl != '{')
        return false;
    decl++;
    SerialBuf buf(512, true);

    bool ignoring = false;
    while (*decl && *decl != '}') {
        skip_spaces(decl);
        css_decl_code prop_code = parse_property_name(decl);
        if (ignoring && prop_code != cssd_cr_only_if) {
            // Skip until next -cr-only-if:
            next_property(decl);
            continue;
        }
        skip_spaces(decl);
        lString8 strValue;
        lUInt32 importance = higher_importance ? IMPORTANT_DECL_HIGHER : 0;
        lUInt32 parsed_important = 0; // for !important that may be parsed along the way
        if (prop_code != cssd_unknown) {
            // parsed ok
            int n = -1;
            switch (prop_code) {
                // non standard property to ignore declaration depending on gDOMVersionRequested
                case cssd_cr_ignore_if_dom_version_greater_or_equal: {
                    unsigned int dom_version;
                    if (parse_uint(decl, dom_version)) {
                        if (domVersionRequested >= dom_version) {
                            return false; // ignore the whole declaration
                        }
                    } else { // ignore the whole declaration too if not an integer
                        return false;
                    }
                } break;
                // non standard property to only apply next properties if rendering option enabled
                case cssd_cr_only_if: {
                    // We may have multiple names, and they must all match
                    ignoring = false;
                    while (*decl != ';') {
                        skip_spaces(decl);
                        bool invert = false;
                        if (*decl == '-') {
                            invert = true;
                            decl++;
                        }
                        bool match = false;
                        int name = parse_name(decl, css_cr_only_if_names, -1);
                        if (name == cr_only_if_any || name == cr_only_if_always) {
                            match = !invert;
                        } else if (name == cr_only_if_never) {
                            match = invert;
                        } else if (!doc) {
                            // Without a doc, we don't have access to any of the following properties
                            match = false;
                        } else if (name == cr_only_if_legacy) {
                            match = BLOCK_RENDERING_D(doc, ENHANCED) == invert;
                        } else if (name == cr_only_if_enhanced) {
                            match = BLOCK_RENDERING_D(doc, ENHANCED) != invert;
                        } else if (name == cr_only_if_float_floatboxes) {
                            match = BLOCK_RENDERING_D(doc, FLOAT_FLOATBOXES) != invert;
                        } else if (name == cr_only_if_box_inlineboxes) {
                            match = BLOCK_RENDERING_D(doc, BOX_INLINE_BLOCKS) != invert;
                        } else if (name == cr_only_if_ensure_style_width) {
                            match = BLOCK_RENDERING_D(doc, ENSURE_STYLE_WIDTH) != invert;
                        } else if (name == cr_only_if_ensure_style_height) {
                            match = BLOCK_RENDERING_D(doc, ENSURE_STYLE_HEIGHT) != invert;
                        } else if (name == cr_only_if_allow_style_w_h_absolute_units) {
                            match = BLOCK_RENDERING_D(doc, ALLOW_STYLE_W_H_ABSOLUTE_UNITS) != invert;
                        } else if (name == cr_only_if_full_featured) {
                            match = (doc->getRenderBlockRenderingFlags() == BLOCK_RENDERING_FULL_FEATURED) != invert;
                        } else if (name == cr_only_if_epub_document) {
                            match = doc->getProps()->getIntDef(DOC_PROP_FILE_FORMAT_ID, doc_format_none) == doc_format_epub;
                            if (invert) {
                                match = !match;
                            }
                        } else if (name == cr_only_if_fb2_document) {
                            int doc_format = doc->getProps()->getIntDef(DOC_PROP_FILE_FORMAT_ID, doc_format_none);
                            match = (doc_format == doc_format_fb2) || (doc_format == doc_format_fb3);
                            if (invert) {
                                match = !match;
                            }
                        } else { // unknown option: ignore
                            match = false;
                        }
                        if (!match) {
                            ignoring = true;
                            break; // no need to look at others
                        }
                        skip_spaces(decl);
                    }
                } break;
                // non standard property for providing hints via style tweaks
                case cssd_cr_hint: {
                    // All values are mapped into a single style->cr_hint 31 bits bitmap
                    int hints = 0; // "none" = no hint
                    int nb_parsed = 0;
                    int nb_invalid = 0;
                    while (*decl && *decl != ';' && *decl != '}') {
                        // Details in crengine/include/cssdef.h (checks ordered by most likely to be seen)
                        if (substr_icompare("none", decl)) {
                            // Forget everything parsed previously, and prevent inheritance
                            hints = CSS_CR_HINT_NONE_NO_INHERIT;
                        } else if (substr_icompare("footnote-inpage", decl))
                            hints |= CSS_CR_HINT_FOOTNOTE_INPAGE;
                        else if (substr_icompare("strut-confined", decl))
                            hints |= CSS_CR_HINT_STRUT_CONFINED;
                        else if (substr_icompare("fit-glyphs", decl))
                            hints |= CSS_CR_HINT_FIT_GLYPHS;
                        else if (substr_icompare("text-selection-skip", decl))
                            hints |= CSS_CR_HINT_TEXT_SELECTION_SKIP;
                        else if (substr_icompare("text-selection-inline", decl))
                            hints |= CSS_CR_HINT_TEXT_SELECTION_INLINE;
                        else if (substr_icompare("text-selection-block", decl))
                            hints |= CSS_CR_HINT_TEXT_SELECTION_BLOCK;
                        else if (substr_icompare("toc-level1", decl))
                            hints |= CSS_CR_HINT_TOC_LEVEL1;
                        else if (substr_icompare("toc-level2", decl))
                            hints |= CSS_CR_HINT_TOC_LEVEL2;
                        else if (substr_icompare("toc-level3", decl))
                            hints |= CSS_CR_HINT_TOC_LEVEL3;
                        else if (substr_icompare("toc-level4", decl))
                            hints |= CSS_CR_HINT_TOC_LEVEL4;
                        else if (substr_icompare("toc-level5", decl))
                            hints |= CSS_CR_HINT_TOC_LEVEL5;
                        else if (substr_icompare("toc-level6", decl))
                            hints |= CSS_CR_HINT_TOC_LEVEL6;
                        else if (substr_icompare("toc-ignore", decl))
                            hints |= CSS_CR_HINT_TOC_IGNORE;
                        else if (substr_icompare("noteref", decl))
                            hints |= CSS_CR_HINT_NOTEREF;
                        else if (substr_icompare("noteref-ignore", decl))
                            hints |= CSS_CR_HINT_NOTEREF_IGNORE;
                        else if (substr_icompare("footnote", decl))
                            hints |= CSS_CR_HINT_FOOTNOTE;
                        else if (substr_icompare("footnote-ignore", decl))
                            hints |= CSS_CR_HINT_FOOTNOTE_IGNORE;
                        //
                        else if (parse_important(decl)) {
                            parsed_important = IMPORTANT_DECL_SET;
                            break; // stop looking for more
                        } else {   // unsupported or invalid named value
                            nb_invalid++;
                            // Walk over unparsed value, and continue checking
                            while (*decl && *decl != ' ' && *decl != ';' && *decl != '}')
                                decl++;
                        }
                        nb_parsed++;
                        skip_spaces(decl);
                    }
                    if (nb_parsed - nb_invalid > 0) { // at least one valid named value seen
                        buf << (lUInt32)(prop_code | importance | parsed_important);
                        buf << (lUInt32)css_val_unspecified; // len.type
                        buf << (lUInt32)hints;               // len.value
                        // (css_val_unspecified just says this value has no unit)
                    }
                } break;
                case cssd_display:
                    n = parse_name(decl, css_d_names, -1);
                    if (domVersionRequested < 20180524 && n == css_d_list_item_block) {
                        n = css_d_list_item_legacy; // legacy rendering of list-item
                    }
                    break;
                case cssd_white_space:
                    n = parse_name(decl, css_ws_names, -1);
                    break;
                case cssd_text_align:
                    n = parse_name(decl, css_ta_names, -1);
                    if (n >= css_ta_auto) // only accepted with text-align-last
                        n = -1;
                    break;
                case cssd_text_align_last:
                case cssd_text_align_last2:
                    prop_code = cssd_text_align_last;
                    n = parse_name(decl, css_ta_names, -1);
                    break;
                case cssd_text_decoration:
                case cssd_text_decoration2:
                    prop_code = cssd_text_decoration;
                    n = parse_name(decl, css_td_names, -1);
                    break;
                case cssd_text_transform:
                    n = parse_name(decl, css_tt_names, -1);
                    break;
                case cssd_hyphenate:
                case cssd_hyphenate2:
                case cssd_hyphenate3:
                case cssd_hyphenate4:
                case cssd_hyphenate5:
                case cssd_hyphenate6:
                    prop_code = cssd_hyphenate;
                    n = parse_name(decl, css_hyph_names, -1);
                    if (n == -1)
                        n = parse_name(decl, css_hyph_names2, -1);
                    if (n == -1)
                        n = parse_name(decl, css_hyph_names3, -1);
                    break;
                case cssd_page_break_before:
                case cssd_break_before:
                    prop_code = cssd_page_break_before;
                    n = parse_name(decl, css_pb_names, -1);
                    break;
                case cssd_page_break_inside:
                case cssd_break_inside:
                    prop_code = cssd_page_break_inside;
                    n = parse_name(decl, css_pb_names, -1);
                    // Only a subset of css_pb_names are accepted
                    if (n > css_pb_avoid)
                        n = -1;
                    break;
                case cssd_page_break_after:
                case cssd_break_after:
                    prop_code = cssd_page_break_after;
                    n = parse_name(decl, css_pb_names, -1);
                    break;
                case cssd_list_style_type:
                    n = parse_name(decl, css_lst_names, -1);
                    break;
                case cssd_list_style_position:
                    n = parse_name(decl, css_lsp_names, -1);
                    break;
                case cssd_list_style: {
                    // The list-style property is specified as one, two, or three keywords in any order,
                    // the keywords being those of list-style-type, list-style-position and list-style-image.
                    // We don't support (and will fail parsing the declaration) a list-style-image url(...)
                    // component, but we can parse the declaration when it contains a type (square, decimal) and/or
                    // a position (inside, outside) in any order.
                    int ntype = -1;
                    int nposition = -1;
                    // check order "type position"
                    ntype = parse_name(decl, css_lst_names, -1);
                    skip_spaces(decl);
                    nposition = parse_name(decl, css_lsp_names, -1);
                    skip_spaces(decl);
                    if (ntype == -1) { // check again if order was "position type"
                        ntype = parse_name(decl, css_lst_names, -1);
                        skip_spaces(decl);
                    }
                    parsed_important = parse_important(decl);
                    if (ntype != -1) {
                        buf << (lUInt32)(cssd_list_style_type | importance | parsed_important);
                        buf << (lUInt32)ntype;
                    }
                    if (nposition != -1) {
                        buf << (lUInt32)(cssd_list_style_position | importance | parsed_important);
                        buf << (lUInt32)nposition;
                    }
                } break;
                case cssd_vertical_align: {
                    css_length_t len;
                    int n1 = parse_name(decl, css_va_names, -1);
                    if (n1 != -1) {
                        len.type = css_val_unspecified;
                        len.value = n1;
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        buf << (lUInt32)len.type;
                        buf << (lUInt32)len.value;
                    } else {
                        if (parse_number_value(decl, len, true, true)) { // accepts a negative value
                            buf << (lUInt32)(prop_code | importance | parse_important(decl));
                            buf << (lUInt32)len.type;
                            buf << (lUInt32)len.value;
                        }
                    }
                } break;
                case cssd_font_family: {
                    lString8Collection list;
                    int processed = splitPropertyValueList(decl, list);
                    // printf("font-family: %s\n", lString8(decl, processed).c_str());
                    decl += processed;
                    n = -1;
                    bool check_for_important = true;
                    if (list.length()) {
                        for (int i = list.length() - 1; i >= 0; i--) {
                            const char* name = list[i].c_str();
                            // printf("  %d: #%s#\n", i, name);
                            if (check_for_important) {
                                check_for_important = false;
                                // The last item from splitPropertyValueList may be or include '!important':
                                //   "serif !important"      (when generic family name)
                                //   "Bitter !important"     (when unquoted font name)
                                //   "Noto Sans !important"  (when unquoted font name)
                                //   "!important"            (when preceded by a quoted font name)
                                // We want to notice it and clean the previous part
                                const char* str = name;
                                bool drop_item = false;
                                while (*str) {
                                    parsed_important = parse_important(str);
                                    if (parsed_important) {
                                        // Found it
                                        if (name == str) {
                                            // No advance: standalone "!important"
                                            // remove it from the list of font names
                                            drop_item = true;
                                        } else {
                                            // Otherwise, truncate name up to where we
                                            // were when finding !important
                                            list[i] = lString8(name, str - name);
                                            name = list[i].c_str();
                                        }
                                        break;
                                    } else { // skip current char that might be a space
                                        str++;
                                    }
                                    // skip next char until we find a space, that would start a new token
                                    while (*str && *str != ' ') {
                                        str++;
                                    }
                                }
                                if (drop_item) {
                                    list.erase(i, 1);
                                    continue;
                                }
                            }
                            int nn = parse_name(name, css_ff_names, -1);
                            if (nn != -1) {
                                if (nn == css_ff_inherit) {
                                    // "inherit" is invalid when not standalone
                                    // We have seen books with 'font-family: "Some Font", inherit',
                                    // that Calibre 3.x would render with "Some Font", while Firefox
                                    // and Calibre 5.x would consider the whole declaration invalid.
                                    // So, best to just ignore any non-standalone "inherit", and
                                    // keep parsing the font names.
                                    if (i == 0 && list.length() == 1) {
                                        // At start and single (we have removed "!important" from the list
                                        // above, as well as any other generic name that was after)
                                        n = css_ff_inherit;
                                    }
                                    list.erase(i, 1);
                                } else {
                                    // As we browse list from the right, keep replacing
                                    // the generic family name with the left most one
                                    n = nn;
                                    // Replace generic family name with specific font from settings
                                    lString8 genericFontFamilyFace = getGenericFontFamilyFace((css_font_family_t)nn);
                                    if (!genericFontFamilyFace.empty())
                                        list[i] = genericFontFamilyFace;
                                    else
                                        list.erase(i, 1);
                                }
                            }
                        }
                        strValue = joinPropertyValueList(list);
                    }
                    // printf("  n=%d imp=%x strValue=%s\n", n, parsed_important, strValue.c_str());
                    // Default to sans-serif generic font-family (the default
                    // in lvfntman.cpp, as FreeType can't know the family of
                    // a font)
                    if (n == -1)
                        n = css_ff_sans_serif;
                } break;
                case cssd_font_style:
                    n = parse_name(decl, css_fs_names, -1);
                    break;
                case cssd_font_weight:
                    n = parse_name(decl, css_fw_names, -1);
                    break;
                case cssd_font_features: // font-feature-settings
                    // Not (yet) implemented.
                    // We map font-variant(|-*) values into the style->font_features bitmap,
                    // that is associated to cssd_font_features, as "font-feature-settings" looks
                    // nearer (than font-variant) to how we handle internally OpenType feature tags.
                    // But font-variant and font-feature-settings, even if they enable the same
                    // OpenType feature tags, should have each a life (inheritance) of their own,
                    // which we won't really ensure by mapping all of them into style->font_features.
                    // Also, font-feature-settings is quite more complicated to parse (optional
                    // arguments, 0|1|2|3|on|off...), and we would only support up to the 31 tags
                    // that can be stored in the bitmap, so ignoring all possible others.
                    // As font-feature-settings is quite new, let's not support it (quite
                    // often, publishers will include both font-variant and font-feature-settings
                    // in a same declaration, so we should be fine).
                    break;
                case cssd_font_variant:
                case cssd_font_variant_ligatures:
                case cssd_font_variant_ligatures2:
                case cssd_font_variant_caps:
                case cssd_font_variant_position:
                case cssd_font_variant_numeric:
                case cssd_font_variant_east_asian:
                case cssd_font_variant_alternates: {
                    // https://drafts.csswg.org/css-fonts-3/#propdef-font-variant
                    // https://developer.mozilla.org/en-US/docs/Web/CSS/font-variant
                    bool parse_ligatures = prop_code == cssd_font_variant || prop_code == cssd_font_variant_ligatures || prop_code == cssd_font_variant_ligatures2;
                    bool parse_caps = prop_code == cssd_font_variant || prop_code == cssd_font_variant_caps;
                    bool parse_position = prop_code == cssd_font_variant || prop_code == cssd_font_variant_position;
                    bool parse_numeric = prop_code == cssd_font_variant || prop_code == cssd_font_variant_numeric;
                    bool parse_eastasian = prop_code == cssd_font_variant || prop_code == cssd_font_variant_east_asian;
                    bool parse_alternates = prop_code == cssd_font_variant || prop_code == cssd_font_variant_alternates;
                    // All values are mapped into a single style->font_features 31 bits bitmap
                    prop_code = cssd_font_features;
                    int features = 0; // "normal" = no extra feature
                    int nb_parsed = 0;
                    int nb_invalid = 0;
                    while (*decl && *decl != ';' && *decl != '}') {
                        if (substr_icompare("normal", decl)) {
                            features = 0;
                        } else if (substr_icompare("none", decl)) {
                            features = 0;
                        }
                        // Details in crengine/include/lvfntman.h
                        else if (parse_ligatures && substr_icompare("no-common-ligatures", decl))
                            features |= LFNT_OT_FEATURES_M_LIGA;
                        else if (parse_ligatures && substr_icompare("no-contextual", decl))
                            features |= LFNT_OT_FEATURES_M_CALT;
                        else if (parse_ligatures && substr_icompare("discretionary-ligatures", decl))
                            features |= LFNT_OT_FEATURES_P_DLIG;
                        else if (parse_ligatures && substr_icompare("no-discretionary-ligatures", decl))
                            features |= LFNT_OT_FEATURES_M_DLIG;
                        else if (parse_ligatures && substr_icompare("historical-ligatures", decl))
                            features |= LFNT_OT_FEATURES_P_HLIG;
                        else if (parse_ligatures && substr_icompare("no-historical-ligatures", decl))
                            features |= LFNT_OT_FEATURES_M_HLIG;
                        else if (parse_alternates && substr_icompare("historical-forms", decl))
                            features |= LFNT_OT_FEATURES_P_HIST;
                        else if (parse_eastasian && substr_icompare("ruby", decl))
                            features |= LFNT_OT_FEATURES_P_RUBY;
                        else if (parse_caps && substr_icompare("small-caps", decl))
                            features |= LFNT_OT_FEATURES_P_SMCP;
                        else if (parse_caps && substr_icompare("all-small-caps", decl))
                            features |= LFNT_OT_FEATURES_P_C2SC;
                        else if (parse_caps && substr_icompare("petite-caps", decl))
                            features |= LFNT_OT_FEATURES_P_PCAP;
                        else if (parse_caps && substr_icompare("all-petite-caps", decl))
                            features |= LFNT_OT_FEATURES_P_C2PC;
                        else if (parse_caps && substr_icompare("unicase", decl))
                            features |= LFNT_OT_FEATURES_P_UNIC;
                        else if (parse_caps && substr_icompare("titling-caps", decl))
                            features |= LFNT_OT_FEATURES_P_TITL;
                        else if (parse_position && substr_icompare("super", decl))
                            features |= LFNT_OT_FEATURES_P_SUPS;
                        else if (parse_position && substr_icompare("sub", decl))
                            features |= LFNT_OT_FEATURES_P_SUBS;
                        else if (parse_numeric && substr_icompare("lining-nums", decl))
                            features |= LFNT_OT_FEATURES_P_LNUM;
                        else if (parse_numeric && substr_icompare("oldstyle-nums", decl))
                            features |= LFNT_OT_FEATURES_P_ONUM;
                        else if (parse_numeric && substr_icompare("proportional-nums", decl))
                            features |= LFNT_OT_FEATURES_P_PNUM;
                        else if (parse_numeric && substr_icompare("tabular-nums", decl))
                            features |= LFNT_OT_FEATURES_P_TNUM;
                        else if (parse_numeric && substr_icompare("slashed-zero", decl))
                            features |= LFNT_OT_FEATURES_P_ZERO;
                        else if (parse_numeric && substr_icompare("ordinal", decl))
                            features |= LFNT_OT_FEATURES_P_ORDN;
                        else if (parse_numeric && substr_icompare("diagonal-fractions", decl))
                            features |= LFNT_OT_FEATURES_P_FRAC;
                        else if (parse_numeric && substr_icompare("stacked-fractions", decl))
                            features |= LFNT_OT_FEATURES_P_AFRC;
                        else if (parse_eastasian && substr_icompare("simplified", decl))
                            features |= LFNT_OT_FEATURES_P_SMPL;
                        else if (parse_eastasian && substr_icompare("traditional", decl))
                            features |= LFNT_OT_FEATURES_P_TRAD;
                        else if (parse_eastasian && substr_icompare("full-width", decl))
                            features |= LFNT_OT_FEATURES_P_FWID;
                        else if (parse_eastasian && substr_icompare("proportional-width", decl))
                            features |= LFNT_OT_FEATURES_P_PWID;
                        else if (parse_eastasian && substr_icompare("jis78", decl))
                            features |= LFNT_OT_FEATURES_P_JP78;
                        else if (parse_eastasian && substr_icompare("jis83", decl))
                            features |= LFNT_OT_FEATURES_P_JP83;
                        else if (parse_eastasian && substr_icompare("jis04", decl))
                            features |= LFNT_OT_FEATURES_P_JP04;

                        else if (parse_important(decl)) {
                            parsed_important = IMPORTANT_DECL_SET;
                            break; // stop looking for more
                        } else {   // unsupported or invalid named value
                            nb_invalid++;
                            // Firefox would ignore the whole declaration if it contains a non-standard named value.
                            // As we don't parse all valid values (eg. styleset(user-defined-ident)), we just skip
                            // them without failing the whole.
                            // Walk over unparsed value
                            while (*decl && *decl != ' ' && *decl != ';' && *decl != '}')
                                decl++;
                        }
                        nb_parsed++;
                        skip_spaces(decl);
                    }
                    if (nb_parsed - nb_invalid > 0) { // at least one valid named value seen
                        buf << (lUInt32)(prop_code | importance | parsed_important);
                        buf << (lUInt32)css_val_unspecified; // len.type
                        buf << (lUInt32)features;            // len.value
                        // css_val_unspecified just says this value has no unit
                        // For cssd_font_features, it actually means there is a value specified.
                        // The default of (css_val_inherited, 0) is what means there was no
                        // value specified, and that it should be inherited, from possibly
                        // the root note that has (css_val_unspecified, 0).
                    }
                } break;
                case cssd_text_indent: {
                    // read length
                    css_length_t len;
                    const char* orig_pos = decl;
                    if (parse_number_value(decl, len, true, true)) { // accepts % and negative values
                        // Read optional "hanging" flag
                        // Note: "1em hanging" is not the same as "-1em"; the former shifts
                        // all other but first line by 1em to the right, while the latter
                        // shifts the first  by 1em to the left. Visually, lines would
                        // look the same relative to each other, but the whole block would
                        // appear shifted to the left with the latter.
                        // Little hack here: to be able to store the presence of "hanging" as
                        // a flag in the css_length_t, we reset the lowest bit to 0, which
                        // shouldn't really have a visual impact on the computed value (as
                        // the parsed number is stored *256 to allow fractional value, so
                        // we're losing 0.004em, 0.004px, 0.004%...)
                        len.value &= 0xFFFFFFFE; // set lowest bit to 0
                                                 // printf("3: %x -3: %x => %x %x %d\n", (lInt16)(3), (lInt16)(-3),
                                                 //    (lInt16)(3&0xFFFFFFFE), (lInt16)((-3)&0xFFFFFFFE), (lInt16)((-3)&0xFFFFFFFE));
                                                 // outputs: 3: 3 -3: fffffffd => 2 fffffffc -4
                        skip_spaces(decl);
                        int attr = parse_name(decl, css_ti_attribute_names, -1);
                        if (attr == 0) {             // "hanging" found
                            len.value |= 0x00000001; // set lowest bit to 1
                        }
                        // Note: if needed, we could parse the "each-line" keyword to be able
                        // to bring back the legacy behaviour (where indent was applied after
                        // a <br>) with CSS, and put this fact in the 2nd lowest bit.
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        buf << (lUInt32)len.type;
                        buf << (lUInt32)len.value;
                    } else {
                        decl = orig_pos; // revert any decl++
                    }
                } break;

                // Next ones accept 1 length value (with possibly named values for borders
                // that we map to a length)
                case cssd_border_bottom_width:
                case cssd_border_top_width:
                case cssd_border_left_width:
                case cssd_border_right_width: {
                    int n1 = parse_name(decl, css_bw_names, -1);
                    if (n1 != -1) {
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        switch (n1) {
                            case 0: // thin
                                buf << (lUInt32)css_val_px;
                                buf << (lUInt32)(1 * 256);
                                break;
                            case 1: // medium
                                buf << (lUInt32)css_val_px;
                                buf << (lUInt32)(3 * 256);
                                break;
                            case 2: // thick
                                buf << (lUInt32)css_val_px;
                                buf << (lUInt32)(5 * 256);
                                break;
                            case 3: // initial
                                buf << (lUInt32)css_val_px;
                                buf << (lUInt32)(3 * 256);
                                break;
                            case 4: // inherit
                            default:
                                buf << (lUInt32)css_val_inherited;
                                buf << (lUInt32)0;
                                break;
                        }
                        break; // We found a named border-width, we're done
                    }
                }
                    // no named value found, don't break: continue checking if value is a number
                case cssd_line_height:
                case cssd_letter_spacing:
                case cssd_font_size:
                case cssd_width:
                case cssd_height:
                case cssd_min_width:
                case cssd_min_height:
                case cssd_max_width:
                case cssd_max_height:
                case cssd_margin_left:
                case cssd_margin_right:
                case cssd_margin_top:
                case cssd_margin_bottom:
                case cssd_padding_left:
                case cssd_padding_right:
                case cssd_padding_top:
                case cssd_padding_bottom: {
                    // borders don't accept length in %
                    bool accept_percent = true;
                    if (prop_code == cssd_border_bottom_width || prop_code == cssd_border_top_width ||
                        prop_code == cssd_border_left_width || prop_code == cssd_border_right_width)
                        accept_percent = false;
                    // only margin accepts negative values
                    bool accept_negative = false;
                    if (prop_code == cssd_margin_bottom || prop_code == cssd_margin_top ||
                        prop_code == cssd_margin_left || prop_code == cssd_margin_right)
                        accept_negative = true;
                    // only margin, width, height, min-width, min-height accept keyword "auto"
                    // (also accept it with max-width, max-height for style tweaks user sake)
                    bool accept_auto = false;
                    if (prop_code == cssd_margin_bottom || prop_code == cssd_margin_top ||
                        prop_code == cssd_margin_left || prop_code == cssd_margin_right ||
                        prop_code == cssd_width || prop_code == cssd_height ||
                        prop_code == cssd_min_width || prop_code == cssd_min_height ||
                        prop_code == cssd_max_width || prop_code == cssd_max_height)
                        accept_auto = true;
                    // only max-width, max-height accept keyword "none"
                    // (also accepts it with min-width, min-height for style tweaks user sake)
                    bool accept_none = false;
                    if (prop_code == cssd_max_width || prop_code == cssd_max_height ||
                        prop_code == cssd_min_width || prop_code == cssd_min_height)
                        accept_none = true;
                    // only line-height and letter-spacing accept keyword "normal"
                    bool accept_normal = false;
                    if (prop_code == cssd_line_height || prop_code == cssd_letter_spacing)
                        accept_normal = true;
                    // only font-size is... font-size
                    bool is_font_size = false;
                    if (prop_code == cssd_font_size)
                        is_font_size = true;
                    css_length_t len;
                    if (parse_number_value(decl, len, accept_percent, accept_negative, accept_auto, accept_none, accept_normal, false, is_font_size)) {
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        buf << (lUInt32)len.type;
                        buf << (lUInt32)len.value;
                    }
                } break;
                // Done with those that accept 1 length value.

                // Next ones accept 1 to 4 length values (with possibly named values for borders
                // that we map to a length)
                case cssd_border_width: {
                    int n1 = parse_name(decl, css_bw_names, -1);
                    if (n1 != -1) {
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        switch (n1) {
                            case 0: // thin
                                for (int i = 0; i < 4; i++) {
                                    buf << (lUInt32)css_val_px;
                                    buf << (lUInt32)(1 * 256);
                                }
                                break;
                            case 1: // medium
                                for (int i = 0; i < 4; i++) {
                                    buf << (lUInt32)css_val_px;
                                    buf << (lUInt32)(3 * 256);
                                }
                                break;
                            case 2: // thick
                                for (int i = 0; i < 4; i++) {
                                    buf << (lUInt32)css_val_px;
                                    buf << (lUInt32)(5 * 256);
                                }
                                break;
                            case 3: // initial
                                for (int i = 0; i < 4; i++) {
                                    buf << (lUInt32)css_val_px;
                                    buf << (lUInt32)(3 * 256);
                                }
                                break;
                            case 4: // inherit
                            default:
                                for (int i = 0; i < 4; i++) {
                                    buf << (lUInt32)css_val_inherited;
                                    buf << (lUInt32)0;
                                }
                                break;
                        }
                        break; // We found a named border-width, we're done
                    }
                }
                    // no named value found, don't break: continue checking if value is a number
                case cssd_margin:
                case cssd_padding: {
                    bool accept_percent = true;
                    if (prop_code == cssd_border_width)
                        accept_percent = false;
                    bool accept_auto = false;
                    bool accept_negative = false;
                    if (prop_code == cssd_margin) {
                        accept_auto = true;
                        accept_negative = true;
                    }
                    css_length_t len[4];
                    int i;
                    for (i = 0; i < 4; i++) {
                        if (!parse_number_value(decl, len[i], accept_percent, accept_negative, accept_auto))
                            break;
                    }
                    if (i) {
                        // If we found 1, it applies to 4 edges
                        // If we found 2, 1st one apply to top and bottom, 2nd to right and left
                        // If we found 3, 1st one apply to top 2nd to right and left, 3rd to bottom
                        switch (i) {
                            case 1:
                                len[1] = len[0]; /* fall through */
                            case 2:
                                len[2] = len[0]; /* fall through */
                            case 3:
                                len[3] = len[1];
                        }
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        for (i = 0; i < 4; i++) {
                            buf << (lUInt32)len[i].type;
                            buf << (lUInt32)len[i].value;
                        }
                    }
                } break;
                    // Done with those that accept 1 to 4 length values.

                case cssd_color:
                case cssd_background_color:
                case cssd_border_top_color:
                case cssd_border_right_color:
                case cssd_border_bottom_color:
                case cssd_border_left_color: {
                    css_length_t len;
                    if (parse_color_value(decl, len)) {
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        buf << (lUInt32)len.type;
                        buf << (lUInt32)len.value;
                    }
                } break;
                case cssd_border_color: {
                    // Accepts 1 to 4 color values
                    css_length_t len[4];
                    int i;
                    for (i = 0; i < 4; i++)
                        if (!parse_color_value(decl, len[i]))
                            break;
                    if (i) {
                        switch (i) {
                            case 1:
                                len[1] = len[0]; /* fall through */
                            case 2:
                                len[2] = len[0]; /* fall through */
                            case 3:
                                len[3] = len[1];
                        }
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        for (i = 0; i < 4; i++) {
                            buf << (lUInt32)len[i].type;
                            buf << (lUInt32)len[i].value;
                        }
                    }
                } break;
                case cssd_border_top_style:
                case cssd_border_right_style:
                case cssd_border_bottom_style:
                case cssd_border_left_style:
                    n = parse_name(decl, css_bst_names, -1);
                    break;
                case cssd_border_style: {
                    // Accepts 1 to 4 named values
                    int name[4];
                    int i;
                    for (i = 0; i < 4; i++) {
                        int n1 = parse_name(decl, css_bst_names, -1);
                        if (n1 != -1) {
                            name[i] = n1;
                            skip_spaces(decl);
                            continue;
                        }
                        break;
                    }
                    if (i) {
                        switch (i) {
                            case 1:
                                name[1] = name[0]; /* fall through */
                            case 2:
                                name[2] = name[0]; /* fall through */
                            case 3:
                                name[3] = name[1];
                        }
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        for (i = 0; i < 4; i++) {
                            buf << (lUInt32)name[i];
                        }
                    }
                } break;

                // Next ones accept a triplet (possibly incomplete) like "2px solid blue".
                // Borders don't accept length in %, and Firefox ignores the whole
                // individual declaration when that happens (with "10% dotted blue", and
                // later a style="border-width: 5px", Firefox shows it solid and black).
                case cssd_border:
                case cssd_border_top:
                case cssd_border_right:
                case cssd_border_bottom:
                case cssd_border_left: {
                    bool found_style = false;
                    bool found_width = false;
                    bool found_color = false;
                    int style_val = -1;
                    css_length_t width;
                    css_length_t color;
                    // https://developer.mozilla.org/en-US/docs/Web/CSS/border-right
                    // We look for 3 values at most, which are allowed to be in any order
                    // and be missing.
                    // If they are missing, we should set them to the default value:
                    //   width: medium, style: none, color: currentColor
                    // Note that the parse_* functions only advance the string when they
                    // match. When they don't match, we stay at the position we were.
                    for (int i = 0; i < 3; i++) {
                        skip_spaces(decl);
                        if (!found_width) {
                            if (parse_number_value(decl, width, false)) { // accept_percent=false
                                found_width = true;
                                continue;
                            } else {
                                int num = parse_name(decl, css_bw_names, -1);
                                if (num != -1) {
                                    width.type = css_val_px;
                                    switch (num) {
                                        case 0: // thin
                                            width.value = 1 * 256;
                                            break;
                                        case 1: // medium
                                            width.value = 3 * 256;
                                            break;
                                        case 2: // thick
                                            width.value = 5 * 256;
                                            break;
                                        case 3: // initial
                                            width.value = 3 * 256;
                                            break;
                                        case 4: // inherit
                                        default:
                                            width.type = css_val_inherited;
                                            width.value = 0;
                                            break;
                                    }
                                    found_width = true;
                                    continue;
                                }
                            }
                        }
                        if (!found_style) {
                            style_val = parse_name(decl, css_bst_names, -1);
                            if (style_val != -1) {
                                found_style = true;
                                continue;
                            }
                        }
                        if (!found_color) {
                            if (parse_color_value(decl, color)) {
                                found_color = true;
                                continue;
                            }
                        }
                        // We have not found any usable name/color/width
                        // in this loop: no need for more
                        break;
                    }
                    parsed_important = parse_important(decl);

                    // We expect to have at least found one of them
                    if (found_style || found_width || found_color) {
                        // We must set the not found properties to their default values
                        if (!found_style) {
                            // Default to "none"
                            style_val = css_border_none;
                        }
                        if (!found_width) {
                            // Default to "medium"
                            width.type = css_val_px;
                            width.value = 3 * 256;
                        }
                        if (!found_color) {
                            color.type = css_val_unspecified;
                            color.value = css_generic_currentcolor;
                        }
                        if (prop_code == cssd_border) {
                            buf << (lUInt32)(cssd_border_style | importance | parsed_important);
                            for (int i = 0; i < 4; i++) {
                                buf << (lUInt32)style_val;
                            }
                            buf << (lUInt32)(cssd_border_width | importance | parsed_important);
                            for (int i = 0; i < 4; i++) {
                                buf << (lUInt32)width.type;
                                buf << (lUInt32)width.value;
                            }
                            buf << (lUInt32)(cssd_border_color | importance | parsed_important);
                            for (int i = 0; i < 4; i++) {
                                buf << (lUInt32)color.type;
                                buf << (lUInt32)color.value;
                            }
                        } else {
                            css_decl_code prop_style, prop_width, prop_color;
                            switch (prop_code) {
                                case cssd_border_top:
                                    prop_style = cssd_border_top_style;
                                    prop_width = cssd_border_top_width;
                                    prop_color = cssd_border_top_color;
                                    break;
                                case cssd_border_right:
                                    prop_style = cssd_border_right_style;
                                    prop_width = cssd_border_right_width;
                                    prop_color = cssd_border_right_color;
                                    break;
                                case cssd_border_bottom:
                                    prop_style = cssd_border_bottom_style;
                                    prop_width = cssd_border_bottom_width;
                                    prop_color = cssd_border_bottom_color;
                                    break;
                                case cssd_border_left:
                                default:
                                    prop_style = cssd_border_left_style;
                                    prop_width = cssd_border_left_width;
                                    prop_color = cssd_border_left_color;
                                    break;
                            }
                            buf << (lUInt32)(prop_style | importance | parsed_important);
                            buf << (lUInt32)style_val;
                            buf << (lUInt32)(prop_width | importance | parsed_important);
                            buf << (lUInt32)width.type;
                            buf << (lUInt32)width.value;
                            buf << (lUInt32)(prop_color | importance | parsed_important);
                            buf << (lUInt32)color.type;
                            buf << (lUInt32)color.value;
                        }
                    }
                } break;
                    // Done with those that accepts a triplet.

                case cssd_background_image: {
                    lString8 str;
                    const char* tmp = decl;
                    int len = 0;
                    while (*tmp && *tmp != ';' && *tmp != '}' && *tmp != '!') {
                        if (*tmp == '(' && *(tmp - 3) == 'u' && *(tmp - 2) == 'r' && *(tmp - 1) == 'l') {
                            // Accepts everything until ')' after 'url(', including ';'
                            // needed when parsing: url("data:image/png;base64,abcd...")
                            tmp++;
                            len++;
                            while (*tmp && *tmp != ')') {
                                tmp++;
                                len++;
                            }
                        } else {
                            tmp++;
                            len++;
                        }
                    }
                    str.append(decl, len);
                    decl += len;
                    resolve_url_path(str, codeBase);
                    len = str.length();
                    buf << (lUInt32)(prop_code | importance | parse_important(decl));
                    buf << (lUInt32)len;
                    for (int i = 0; i < len; i++)
                        buf << (lUInt32)str[i];
                } break;
                case cssd_background_repeat:
                    n = parse_name(decl, css_bg_repeat_names, -1);
                    break;
                case cssd_background_position:
                    n = parse_name(decl, css_bg_position_names, -1);
                    // Only values between 0 and 8 will be checked by the background drawing code
                    if (n > 8) {
                        if (n < 18)
                            n = n - 9; // "top left" = "left top"
                        else if (n == 18)
                            n = 7; // "center" = "center center"
                        else if (n == 19)
                            n = 1; // "left" = "left center"
                        else if (n == 20)
                            n = 4; // "right" = "right center"
                        else if (n == 21)
                            n = 6; // "top" = "center top"
                        else if (n == 22)
                            n = 8; // "bottom" = "center bottom"
                        else if (n == 23)
                            n = 0; // "initial" = "left top"
                        else if (n == 24)
                            n = 0; // "inherit" = "left top"
                    }
                    break;
                case cssd_background: {
                    // Limited parsing of this possibly complex property
                    // We only support a single layer in these orders:
                    //   - color
                    //   - url(...) repeat position
                    //   - color url(...) repeat position
                    //   - color url(...) position repeat
                    // (with repeat and position possibly absent or re-ordered)
                    css_length_t color;
                    bool has_color = parse_color_value(decl, color);
                    skip_spaces(decl);
                    const char* tmp = decl;
                    int len = 0;
                    while (*tmp && *tmp != ';' && *tmp != '}' && *tmp != '!') {
                        if (*tmp == '(' && *(tmp - 3) == 'u' && *(tmp - 2) == 'r' && *(tmp - 1) == 'l') {
                            // Accepts everything until ')' after 'url(', including ';'
                            // needed when parsing: url("data:image/png;base64,abcd...")
                            tmp++;
                            len++;
                            while (*tmp && *tmp != ')') {
                                tmp++;
                                len++;
                            }
                        } else {
                            tmp++;
                            len++;
                        }
                    }
                    lString8 str;
                    str.append(decl, len);
                    if (Utf8ToUnicode(str).lowercase().startsWith("url(")) {
                        tmp = str.c_str();
                        len = 0;
                        while (*tmp && *tmp != ')') {
                            tmp++;
                            len++;
                        }
                        len = len + 1;
                        str.clear();
                        str.append(decl, len);
                        decl += len;
                        resolve_url_path(str, codeBase);
                        len = str.length();
                        // Try parsing following repeat and position
                        skip_spaces(decl);
                        int repeat = parse_name(decl, css_bg_repeat_names, -1);
                        if (repeat != -1) {
                            skip_spaces(decl);
                        }
                        int position = parse_name(decl, css_bg_position_names, -1);
                        if (position != -1) {
                            // Only values between 0 and 8 will be checked by the background drawing code
                            if (position > 8) {
                                if (position < 18)
                                    position -= 9; // "top left" = "left top"
                                else if (position == 18)
                                    position = 7; // "center" = "center center"
                                else if (position == 19)
                                    position = 1; // "left" = "left center"
                                else if (position == 20)
                                    position = 4; // "right" = "right center"
                                else if (position == 21)
                                    position = 6; // "top" = "center top"
                                else if (position == 22)
                                    position = 8; // "bottom" = "center bottom"
                                else if (position == 23)
                                    position = 0; // "initial" = "left top"
                                else if (position == 24)
                                    position = 0; // "inherit" = "left top"
                            }
                        }
                        if (repeat == -1) { // Try parsing repeat after position
                            skip_spaces(decl);
                            repeat = parse_name(decl, css_bg_repeat_names, -1);
                        }
                        parsed_important = parse_important(decl);
                        buf << (lUInt32)(cssd_background_image | importance | parsed_important);
                        buf << (lUInt32)len;
                        for (int i = 0; i < len; i++)
                            buf << (lUInt32)str[i];
                        if (repeat != -1) {
                            buf << (lUInt32)(cssd_background_repeat | importance | parsed_important);
                            buf << (lUInt32)repeat;
                        }
                        if (position != -1) {
                            buf << (lUInt32)(cssd_background_position | importance | parsed_important);
                            buf << (lUInt32)position;
                        }
                    } else {         // no url, only color
                        decl += len; // skip any unsupported stuff until !
                        parsed_important = parse_important(decl);
                    }
                    if (has_color) {
                        buf << (lUInt32)(cssd_background_color | importance | parsed_important);
                        buf << (lUInt32)color.type;
                        buf << (lUInt32)color.value;
                    }
                } break;
                case cssd_background_size:
                case cssd_background_size2: {
                    prop_code = cssd_background_size;
                    // https://developer.mozilla.org/en-US/docs/Web/CSS/background-size
                    css_length_t len[2];
                    int i;
                    for (i = 0; i < 2; i++) {
                        if (!parse_number_value(decl, len[i], true, false, true, false, false, true))
                            break;
                    }
                    if (i) {
                        if (i == 1) {                                 // Only 1 value parsed
                            if (len[0].type == css_val_unspecified) { // "auto", "contain" or "cover"
                                len[1].type = css_val_unspecified;
                                len[1].value = len[0].value;
                            } else { // first value is a length: second value should be "auto"
                                len[1].type = css_val_unspecified;
                                len[1].value = css_generic_auto;
                            }
                        }
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        for (i = 0; i < 2; i++) {
                            buf << (lUInt32)len[i].type;
                            buf << (lUInt32)len[i].value;
                        }
                    }
                } break;
                case cssd_border_spacing: {
                    css_length_t len[2];
                    int i;
                    for (i = 0; i < 2; i++) {
                        // border-spacing doesn't accept values in %
                        if (!parse_number_value(decl, len[i], false))
                            break;
                    }
                    if (i) {
                        if (i == 1)
                            len[1] = len[0];
                        buf << (lUInt32)(prop_code | importance | parse_important(decl));
                        for (i = 0; i < 2; i++) {
                            buf << (lUInt32)len[i].type;
                            buf << (lUInt32)len[i].value;
                        }
                    }
                } break;
                case cssd_border_collapse:
                    n = parse_name(decl, css_bc_names, -1);
                    break;
                case cssd_orphans:
                    n = parse_name(decl, css_orphans_widows_names, -1);
                    break;
                case cssd_widows:
                    n = parse_name(decl, css_orphans_widows_names, -1);
                    break;
                case cssd_float:
                    n = parse_name(decl, css_f_names, -1);
                    break;
                case cssd_clear:
                    n = parse_name(decl, css_c_names, -1);
                    break;
                case cssd_direction:
                    n = parse_name(decl, css_dir_names, -1);
                    break;
                case cssd_visibility:
                    n = parse_name(decl, css_v_names, -1);
                    break;
                case cssd_line_break:
                case cssd_line_break2:
                case cssd_line_break3:
                    prop_code = cssd_line_break;
                    n = parse_name(decl, css_lb_names, -1);
                    break;
                case cssd_word_break:
                    n = parse_name(decl, css_wb_names, -1);
                    break;
                case cssd_content: {
                    lString32 parsed_content;
                    if (parse_content_property(decl, parsed_content)) {
                        buf << (lUInt32)(cssd_content | importance | parsed_important | parse_important(decl));
                        buf << (lUInt32)parsed_content.length();
                        for (int i = 0; i < parsed_content.length(); i++) {
                            buf << (lUInt32)parsed_content[i];
                        }
                    }
                } break;
                case cssd_stop:
                case cssd_unknown:
                default:
                    break;
            }
            if (n != -1) {
                // add enum property
                buf << (lUInt32)(prop_code | importance | parsed_important | parse_important(decl));
                buf << (lUInt32)n;
            }
            if (!strValue.empty()) {
                // add string property
                if (prop_code == cssd_font_family) {
                    // font names
                    buf << (lUInt32)(cssd_font_names | importance | parsed_important | parse_important(decl));
                    buf << (lUInt32)strValue.length();
                    for (int i = 0; i < strValue.length(); i++)
                        buf << (lUInt32)strValue[i];
                }
            }
        } else {
            // skip unknown property
        }
        next_property(decl);
    }

    // store parsed result
    if (buf.pos()) {
        buf << (lUInt32)cssd_stop; // add end marker
        int sz = buf.pos() / 4;
        _data = new int[sz];
        // Could that cause problem with different endianess?
        buf.copyTo((lUInt8*)_data, buf.pos());
        // Alternative:
        //   buf.setPos(0);
        //   for (int i=0; i<sz; i++)
        //      buf >> _data[i];
    }

    // skip }
    skip_spaces(decl);
    if (*decl == '}') {
        decl++;
        return true;
    }
    return false;
}

static css_length_t read_length(int*& data) {
    css_length_t len;
    len.type = (css_value_type_t)(*data++);
    len.value = (*data++);
    return len;
}

void LVCssDeclaration::apply(css_style_rec_t* style) {
    if (!_data)
        return;
    int* p = _data;
    for (;;) {
        lUInt32 prop_code = *p++;
        lUInt8 is_important = prop_code >> IMPORTANT_DECL_SHIFT; // 2 bits (importance, is_important)
        prop_code = prop_code & IMPORTANT_DECL_REMOVE;
        switch (prop_code) {
            case cssd_display:
                style->Apply((css_display_t)*p++, &style->display, imp_bit_display, is_important);
                break;
            case cssd_white_space:
                style->Apply((css_white_space_t)*p++, &style->white_space, imp_bit_white_space, is_important);
                break;
            case cssd_text_align:
                style->Apply((css_text_align_t)*p++, &style->text_align, imp_bit_text_align, is_important);
                break;
            case cssd_text_align_last:
                style->Apply((css_text_align_t)*p++, &style->text_align_last, imp_bit_text_align_last, is_important);
                break;
            case cssd_text_decoration:
                style->Apply((css_text_decoration_t)*p++, &style->text_decoration, imp_bit_text_decoration, is_important);
                break;
            case cssd_text_transform:
                style->Apply((css_text_transform_t)*p++, &style->text_transform, imp_bit_text_transform, is_important);
                break;
            case cssd_hyphenate:
                style->Apply((css_hyphenate_t)*p++, &style->hyphenate, imp_bit_hyphenate, is_important);
                break;
            case cssd_list_style_type:
                style->Apply((css_list_style_type_t)*p++, &style->list_style_type, imp_bit_list_style_type, is_important);
                break;
            case cssd_list_style_position:
                style->Apply((css_list_style_position_t)*p++, &style->list_style_position, imp_bit_list_style_position, is_important);
                break;
            case cssd_page_break_before:
                style->Apply((css_page_break_t)*p++, &style->page_break_before, imp_bit_page_break_before, is_important);
                break;
            case cssd_page_break_after:
                style->Apply((css_page_break_t)*p++, &style->page_break_after, imp_bit_page_break_after, is_important);
                break;
            case cssd_page_break_inside:
                style->Apply((css_page_break_t)*p++, &style->page_break_inside, imp_bit_page_break_inside, is_important);
                break;
            case cssd_vertical_align:
                style->Apply(read_length(p), &style->vertical_align, imp_bit_vertical_align, is_important);
                break;
            case cssd_font_family:
                style->Apply((css_font_family_t)*p++, &style->font_family, imp_bit_font_family, is_important);
                break;
            case cssd_font_names: {
                lString8 names;
                names.reserve(64);
                int len = *p++;
                for (int i = 0; i < len; i++)
                    names << (lChar8)(*p++);
                names.pack();
                style->Apply(names, &style->font_name, imp_bit_font_name, is_important);
            } break;
            case cssd_font_style:
                style->Apply((css_font_style_t)*p++, &style->font_style, imp_bit_font_style, is_important);
                break;
            case cssd_font_weight:
                style->Apply((css_font_weight_t)*p++, &style->font_weight, imp_bit_font_weight, is_important);
                break;
            case cssd_font_size:
                style->Apply(read_length(p), &style->font_size, imp_bit_font_size, is_important);
                break;
            case cssd_font_features:
                // We want to 'OR' the bitmap from any declaration that is to be applied to this node
                // (while still ensuring !important).
                style->ApplyAsBitmapOr(read_length(p), &style->font_features, imp_bit_font_features, is_important);
                break;
            case cssd_text_indent:
                style->Apply(read_length(p), &style->text_indent, imp_bit_text_indent, is_important);
                break;
            case cssd_line_height:
                style->Apply(read_length(p), &style->line_height, imp_bit_line_height, is_important);
                break;
            case cssd_letter_spacing:
                style->Apply(read_length(p), &style->letter_spacing, imp_bit_letter_spacing, is_important);
                break;
            case cssd_color:
                style->Apply(read_length(p), &style->color, imp_bit_color, is_important);
                break;
            case cssd_background_color:
                style->Apply(read_length(p), &style->background_color, imp_bit_background_color, is_important);
                break;
            case cssd_width:
                style->Apply(read_length(p), &style->width, imp_bit_width, is_important);
                break;
            case cssd_height:
                style->Apply(read_length(p), &style->height, imp_bit_height, is_important);
                break;
            case cssd_min_width:
                style->Apply(read_length(p), &style->min_width, imp_bit_min_width, is_important);
                break;
            case cssd_min_height:
                style->Apply(read_length(p), &style->min_height, imp_bit_min_height, is_important);
                break;
            case cssd_max_width:
                style->Apply(read_length(p), &style->max_width, imp_bit_max_width, is_important);
                break;
            case cssd_max_height:
                style->Apply(read_length(p), &style->max_height, imp_bit_max_height, is_important);
                break;
            case cssd_margin_left:
                style->Apply(read_length(p), &style->margin[0], imp_bit_margin_left, is_important);
                break;
            case cssd_margin_right:
                style->Apply(read_length(p), &style->margin[1], imp_bit_margin_right, is_important);
                break;
            case cssd_margin_top:
                style->Apply(read_length(p), &style->margin[2], imp_bit_margin_top, is_important);
                break;
            case cssd_margin_bottom:
                style->Apply(read_length(p), &style->margin[3], imp_bit_margin_bottom, is_important);
                break;
            case cssd_margin:
                style->Apply(read_length(p), &style->margin[2], imp_bit_margin_top, is_important);
                style->Apply(read_length(p), &style->margin[1], imp_bit_margin_right, is_important);
                style->Apply(read_length(p), &style->margin[3], imp_bit_margin_bottom, is_important);
                style->Apply(read_length(p), &style->margin[0], imp_bit_margin_left, is_important);
                break;
            case cssd_padding_left:
                style->Apply(read_length(p), &style->padding[0], imp_bit_padding_left, is_important);
                break;
            case cssd_padding_right:
                style->Apply(read_length(p), &style->padding[1], imp_bit_padding_right, is_important);
                break;
            case cssd_padding_top:
                style->Apply(read_length(p), &style->padding[2], imp_bit_padding_top, is_important);
                break;
            case cssd_padding_bottom:
                style->Apply(read_length(p), &style->padding[3], imp_bit_padding_bottom, is_important);
                break;
            case cssd_padding:
                style->Apply(read_length(p), &style->padding[2], imp_bit_padding_top, is_important);
                style->Apply(read_length(p), &style->padding[1], imp_bit_padding_right, is_important);
                style->Apply(read_length(p), &style->padding[3], imp_bit_padding_bottom, is_important);
                style->Apply(read_length(p), &style->padding[0], imp_bit_padding_left, is_important);
                break;
            case cssd_border_top_color:
                style->Apply(read_length(p), &style->border_color[0], imp_bit_border_color_top, is_important);
                break;
            case cssd_border_right_color:
                style->Apply(read_length(p), &style->border_color[1], imp_bit_border_color_right, is_important);
                break;
            case cssd_border_bottom_color:
                style->Apply(read_length(p), &style->border_color[2], imp_bit_border_color_bottom, is_important);
                break;
            case cssd_border_left_color:
                style->Apply(read_length(p), &style->border_color[3], imp_bit_border_color_left, is_important);
                break;
            case cssd_border_top_width:
                style->Apply(read_length(p), &style->border_width[0], imp_bit_border_width_top, is_important);
                break;
            case cssd_border_right_width:
                style->Apply(read_length(p), &style->border_width[1], imp_bit_border_width_right, is_important);
                break;
            case cssd_border_bottom_width:
                style->Apply(read_length(p), &style->border_width[2], imp_bit_border_width_bottom, is_important);
                break;
            case cssd_border_left_width:
                style->Apply(read_length(p), &style->border_width[3], imp_bit_border_width_left, is_important);
                break;
            case cssd_border_top_style:
                style->Apply((css_border_style_type_t)*p++, &style->border_style_top, imp_bit_border_style_top, is_important);
                break;
            case cssd_border_right_style:
                style->Apply((css_border_style_type_t)*p++, &style->border_style_right, imp_bit_border_style_right, is_important);
                break;
            case cssd_border_bottom_style:
                style->Apply((css_border_style_type_t)*p++, &style->border_style_bottom, imp_bit_border_style_bottom, is_important);
                break;
            case cssd_border_left_style:
                style->Apply((css_border_style_type_t)*p++, &style->border_style_left, imp_bit_border_style_left, is_important);
                break;
            case cssd_border_color:
                style->Apply(read_length(p), &style->border_color[0], imp_bit_border_color_top, is_important);
                style->Apply(read_length(p), &style->border_color[1], imp_bit_border_color_right, is_important);
                style->Apply(read_length(p), &style->border_color[2], imp_bit_border_color_bottom, is_important);
                style->Apply(read_length(p), &style->border_color[3], imp_bit_border_color_left, is_important);
                break;
            case cssd_border_width:
                style->Apply(read_length(p), &style->border_width[0], imp_bit_border_width_top, is_important);
                style->Apply(read_length(p), &style->border_width[1], imp_bit_border_width_right, is_important);
                style->Apply(read_length(p), &style->border_width[2], imp_bit_border_width_bottom, is_important);
                style->Apply(read_length(p), &style->border_width[3], imp_bit_border_width_left, is_important);
                break;
            case cssd_border_style:
                style->Apply((css_border_style_type_t)*p++, &style->border_style_top, imp_bit_border_style_top, is_important);
                style->Apply((css_border_style_type_t)*p++, &style->border_style_right, imp_bit_border_style_right, is_important);
                style->Apply((css_border_style_type_t)*p++, &style->border_style_bottom, imp_bit_border_style_bottom, is_important);
                style->Apply((css_border_style_type_t)*p++, &style->border_style_left, imp_bit_border_style_left, is_important);
                break;
            case cssd_background_image: {
                lString8 imagefile;
                imagefile.reserve(64);
                int l = *p++;
                for (int i = 0; i < l; i++)
                    imagefile << (lChar8)(*p++);
                imagefile.pack();
                style->Apply(imagefile, &style->background_image, imp_bit_background_image, is_important);
            } break;
            case cssd_background_repeat:
                style->Apply((css_background_repeat_value_t)*p++, &style->background_repeat, imp_bit_background_repeat, is_important);
                break;
            case cssd_background_position:
                style->Apply((css_background_position_value_t)*p++, &style->background_position, imp_bit_background_position, is_important);
                break;
            case cssd_background_size:
                style->Apply(read_length(p), &style->background_size[0], imp_bit_background_size_h, is_important);
                style->Apply(read_length(p), &style->background_size[1], imp_bit_background_size_v, is_important);
                break;
            case cssd_border_spacing:
                style->Apply(read_length(p), &style->border_spacing[0], imp_bit_border_spacing_h, is_important);
                style->Apply(read_length(p), &style->border_spacing[1], imp_bit_border_spacing_v, is_important);
                break;
            case cssd_border_collapse:
                style->Apply((css_border_collapse_value_t)*p++, &style->border_collapse, imp_bit_border_collapse, is_important);
                break;
            case cssd_orphans:
                style->Apply((css_orphans_widows_value_t)*p++, &style->orphans, imp_bit_orphans, is_important);
                break;
            case cssd_widows:
                style->Apply((css_orphans_widows_value_t)*p++, &style->widows, imp_bit_widows, is_important);
                break;
            case cssd_float:
                style->Apply((css_float_t)*p++, &style->float_, imp_bit_float, is_important);
                break;
            case cssd_clear:
                style->Apply((css_clear_t)*p++, &style->clear, imp_bit_clear, is_important);
                break;
            case cssd_direction:
                style->Apply((css_direction_t)*p++, &style->direction, imp_bit_direction, is_important);
                break;
            case cssd_visibility:
                style->Apply((css_visibility_t)*p++, &style->visibility, imp_bit_visibility, is_important);
                break;
            case cssd_line_break:
                style->Apply((css_line_break_t)*p++, &style->line_break, imp_bit_line_break, is_important);
                break;
            case cssd_word_break:
                style->Apply((css_word_break_t)*p++, &style->word_break, imp_bit_word_break, is_important);
                break;
            case cssd_cr_hint: {
                // We want to 'OR' the bitmap from any declaration that is to be applied to this node
                // (while still ensuring !important) - unless this declaration had "-cr-hint: none"
                // in which case we should reset previously set bits
                css_length_t cr_hint = read_length(p);
                if (cr_hint.value & CSS_CR_HINT_NONE_NO_INHERIT) {
                    style->Apply(cr_hint, &style->cr_hint, imp_bit_cr_hint, is_important);
                } else {
                    style->ApplyAsBitmapOr(cr_hint, &style->cr_hint, imp_bit_cr_hint, is_important);
                }
            } break;
            case cssd_content: {
                int l = *p++;
                lString32 content;
                if (l > 0) {
                    content.reserve(l);
                    for (int i = 0; i < l; i++)
                        content << (lChar32)(*p++);
                }
                style->Apply(content, &style->content, imp_bit_content, is_important);
            } break;
            case cssd_stop:
                return;
        }
    }
}

lUInt32 LVCssDeclaration::getHash() {
    if (!_data)
        return 0;
    int* p = _data;
    lUInt32 hash = 0;
    for (; *p != cssd_stop; p++)
        hash = hash * 31 + *p;
    return hash;
}

static bool parse_ident(const char*& str, char* ident) {
    // Note: skipping any space before or after should be ensured by caller if needed
    *ident = 0;
    if (!css_is_alpha(*str))
        return false;
    int i;
    for (i = 0; css_is_alnum(str[i]); i++)
        ident[i] = str[i];
    ident[i] = 0;
    str += i;
    return true;
}

// We are storing specificity/weight in a lUInt32.
// We also want to include in it the order in which we have
// seen/parsed the selectors, so we store in the lower bits
// of this lUInt32 some sequence number to ensure selectors
// with the same specificity are applied in the order we've
// seen them when parsing.
// So, apply the real CSS specificity in higher bits, allowing
// for the following number of such rules in a single selector
// (we're not checking for overflow thus...)
#define WEIGHT_SPECIFICITY_ID      1 << 29 // allow for 8 #id (b in comment below)
#define WEIGHT_SPECIFICITY_ATTRCLS 1 << 24 // allow for 32 .class and [attr...] (c)
#define WEIGHT_SPECIFICITY_ELEMENT 1 << 19 // allow for 32 element names div > p span (d)
#define WEIGHT_SELECTOR_ORDER      1       // allow for counting 524288 selectors

lUInt32 LVCssSelectorRule::getWeight() {
    /* Each LVCssSelectorRule will add its own weight to
       its LVCssSelector container specifity.

    Following https://www.w3.org/TR/CSS2/cascade.html#specificity

    A selector's specificity is calculated as follows:

    - count 1 if the declaration is from is a 'style' attribute rather
    than a rule with a selector, 0 otherwise (= a) (In HTML, values
    of an element's "style" attribute are style sheet rules. These
    rules have no selectors, so a=1, b=0, c=0, and d=0.)
    - count the number of ID attributes in the selector (= b) => 1 << 16
    - count the number of other attributes and pseudo-classes in the
    selector (= c) => 1 << 8
    - count the number of element names and pseudo-elements in the
    selector (= d) => 1

    The specificity is based only on the form of the selector. In
    particular, a selector of the form "[id=p33]" is counted as an
    attribute selector (a=0, b=0, c=1, d=0), even if the id attribute is
    defined as an "ID" in the source document's DTD.
    */

    // declaration from a style="" attribute (a) are always applied last,
    // and don't have a selector here.
    // LVCssSelector._specificity will be added 1 by LVCssSelector when it
    // has itself an elementName  // E
    //
    switch (_type) {
        case cssrt_id: // E#id
            return WEIGHT_SPECIFICITY_ID;
            break;
        case cssrt_attrset:           // E[foo]
        case cssrt_attreq:            // E[foo="value"]
        case cssrt_attreq_i:          // E[foo="value" i]
        case cssrt_attrhas:           // E[foo~="value"]
        case cssrt_attrhas_i:         // E[foo~="value" i]
        case cssrt_attrstarts_word:   // E[foo|="value"]
        case cssrt_attrstarts_word_i: // E[foo|="value" i]
        case cssrt_attrstarts:        // E[foo^="value"]
        case cssrt_attrstarts_i:      // E[foo^="value" i]
        case cssrt_attrends:          // E[foo$="value"]
        case cssrt_attrends_i:        // E[foo$="value" i]
        case cssrt_attrcontains:      // E[foo*="value"]
        case cssrt_attrcontains_i:    // E[foo*="value" i]
        case cssrt_class:             // E.class
        case cssrt_pseudoclass:       // E:pseudo-class
            return WEIGHT_SPECIFICITY_ATTRCLS;
            break;
        case cssrt_parent:      // E > F
        case cssrt_ancessor:    // E F
        case cssrt_predecessor: // E + F
        case cssrt_predsibling: // E ~ F
            // These don't contribute to specificity. If they
            // come with an element name, WEIGHT_SPECIFICITY_ELEMENT
            // has already been added in LVCssSelector::parse().
            return 0;
            break;
        case cssrt_universal: // *
            return 0;
    }
    return 0;
}

bool LVCssSelectorRule::check(const ldomNode*& node) {
    if (!node || node->isNull() || node->isRoot())
        return false;
    // For most checks, while navigating nodes, we must ignore sibling text nodes.
    // We also ignore crengine internal boxing elements (inserted for rendering
    // purpose) by using the getUnboxedParent/Sibling(true) methods (providing
    // 'true' make them skip text nodes).
    // Note that if we are returnging 'true', the provided 'node' must stay
    // or be updated to the node on which next selectors (on the left in the
    // chain) must be checked against. When returning 'false', we can let
    // node be in any state, even messy.

    // We allow internal/boxing elements element names in selectors,
    // so if one is specified, we should not skip it with getUnboxed*().
    // We expect to find only a single kind of them per selector though.
    lUInt16 exceptBoxingNodeId = 0;
    if (_id >= EL_BOXING_START && _id <= EL_BOXING_END) { // _id from rule
        exceptBoxingNodeId = _id;
    } else {
        // Also check current node: if we stopped on it from a previous
        // rule, it's because the previous rule was checking for this
        // boxing element name
        lUInt16 curNodeId = node->getNodeId();
        if (curNodeId >= EL_BOXING_START && curNodeId <= EL_BOXING_END)
            exceptBoxingNodeId = curNodeId;
    }
    switch (_type) {
        case cssrt_parent: // E > F (child combinator)
        {
            node = node->getUnboxedParent(exceptBoxingNodeId);
            if (!node || node->isNull())
                return false;
            // If _id=0, we are the parent and we match
            if (!_id || node->getNodeId() == _id)
                return true;
            return false;
        } break;
        case cssrt_ancessor: // E F (descendant combinator)
        {
            for (;;) {
                node = node->getUnboxedParent(exceptBoxingNodeId);
                if (!node || node->isNull())
                    return false;
                // cssrt_ancessor is a non-deterministic rule: next rules
                // could fail when checked against this parent that matches
                // current rule, but could succeed when checked against
                // another parent that matches.
                // So, we need to check the full next rules chain on each
                // of our parent that matches current rule.
                // As we check the whole selector rules chain here,
                // LVCssSelector::check() won't have to: so it will trust
                // our return value.
                // Note: this is quite expensive compared to other combinators.
                if (!_id || node->getNodeId() == _id) {
                    // No element name to match against, or this element name
                    // matches: check next rules starting from there.
                    const ldomNode* n = node;
                    if (checkNextRules(n))
                        // We match all next rules (possibly including other
                        // cssrt_ancessor)
                        return true;
                    // Next rules didn't match: continue with next parent
                }
            }
        } break;
        case cssrt_predecessor: // E + F (adjacent sibling combinator)
        {
            node = node->getUnboxedPrevSibling(true, exceptBoxingNodeId); // skip text nodes
            if (!node || node->isNull())
                return false;
            if (!_id || node->getNodeId() == _id) {
                // No element name to match against, or this element name matches
                return true;
            }
            return false;
        } break;
        case cssrt_predsibling: // E ~ F (preceding sibling / general sibling combinator)
        {
            for (;;) {
                node = node->getUnboxedPrevSibling(true, exceptBoxingNodeId); // skip text nodes
                if (!node || node->isNull())
                    return false;
                if (!_id || node->getNodeId() == _id) {
                    // No element name to match against, or this element name
                    // matches: check next rules starting from there.
                    // Same as what is done in cssrt_ancessor above: we may have
                    // to check next rules on all preceeding matching siblings.
                    const ldomNode* n = node;
                    if (checkNextRules(n))
                        // We match all next rules (possibly including other
                        // cssrt_ancessor or cssrt_predsibling)
                        return true;
                    // Next rules didn't match: continue with next prev sibling
                }
            }
        } break;
        case cssrt_attrset: // E[foo]
        {
            if (!node->hasAttributes())
                return false;
            return node->hasAttribute(_attrid);
        } break;
        case cssrt_attreq:   // E[foo="value"]
        case cssrt_attreq_i: // E[foo="value" i]
        {
            if (!node->hasAttribute(_attrid))
                return false;
            lString32 val = node->getAttributeValue(_attrid);
            if (_type == cssrt_attreq_i)
                val.lowercase();
            return val == _value;
        } break;
        case cssrt_attrhas:   // E[foo~="value"]
        case cssrt_attrhas_i: // E[foo~="value" i]
            // one of space separated values
            {
                if (!node->hasAttribute(_attrid))
                    return false;
                lString32 val = node->getAttributeValue(_attrid);
                if (_type == cssrt_attrhas_i)
                    val.lowercase();
                int p = val.pos(lString32(_value.c_str()));
                if (p < 0)
                    return false;
                if ((p > 0 && val[p - 1] != ' ') || (p + _value.length() < val.length() && val[p + _value.length()] != ' '))
                    return false;
                return true;
            }
            break;
        case cssrt_attrstarts_word:   // E[foo|="value"]
        case cssrt_attrstarts_word_i: // E[foo|="value" i]
        {
            if (!node->hasAttribute(_attrid))
                return false;
            // value can be exactly value or can begin with value
            // immediately followed by a hyphen
            lString32 val = node->getAttributeValue(_attrid);
            int val_len = val.length();
            int value_len = _value.length();
            if (value_len > val_len)
                return false;
            if (_type == cssrt_attrstarts_i)
                val.lowercase();
            if (value_len == val_len) {
                return val == _value;
            }
            if (val[value_len] != '-')
                return false;
            val = val.substr(0, value_len);
            return val == _value;
        } break;
        case cssrt_attrstarts:   // E[foo^="value"]
        case cssrt_attrstarts_i: // E[foo^="value" i]
        {
            if (!node->hasAttribute(_attrid))
                return false;
            lString32 val = node->getAttributeValue(_attrid);
            int val_len = val.length();
            int value_len = _value.length();
            if (value_len > val_len)
                return false;
            val = val.substr(0, value_len);
            if (_type == cssrt_attrstarts_i)
                val.lowercase();
            return val == _value;
        } break;
        case cssrt_attrends:   // E[foo$="value"]
        case cssrt_attrends_i: // E[foo$="value" i]
        {
            if (!node->hasAttribute(_attrid))
                return false;
            lString32 val = node->getAttributeValue(_attrid);
            int val_len = val.length();
            int value_len = _value.length();
            if (value_len > val_len)
                return false;
            val = val.substr(val_len - value_len, value_len);
            if (_type == cssrt_attrends_i)
                val.lowercase();
            return val == _value;
        } break;
        case cssrt_attrcontains:   // E[foo*="value"]
        case cssrt_attrcontains_i: // E[foo*="value" i]
        {
            if (!node->hasAttribute(_attrid))
                return false;
            lString32 val = node->getAttributeValue(_attrid);
            if (_value.length() > val.length())
                return false;
            if (_type == cssrt_attrcontains_i)
                val.lowercase();
            return val.pos(_value, 0) >= 0;
        } break;
        case cssrt_id: // E#id
        {
            lString32 val = node->getAttributeValue(attr_id);
            if (val.empty())
                return false;
            /*lString32 ldomDocumentFragmentWriter::convertId( lString32 id ) adds codeBasePrefix to
             *original id name, I can not get codeBasePrefix from here so I add a space to identify the
             *real id name.*/
            int pos = val.pos(" ");
            if (pos != -1) {
                val = val.substr(pos + 1, val.length() - pos - 1);
            }
            if (_value.length() > val.length())
                return false;
            return val == _value;
        } break;
        case cssrt_class: // E.class
        {
            lString32 val = node->getAttributeValue(attr_class);
            if (val.empty())
                return false;
            // val.lowercase(); // className should be case sensitive
            // if ( val.length() != _value.length() )
            //     return false;
            //CRLog::trace("attr_class: %s %s", LCSTR(val), LCSTR(_value) );
            /*As I have eliminated leading and ending spaces in the attribute value, any space in
             *val means there are more than one classes */
            if (val.pos(" ") != -1) {
                lString32 value_w_space_after = _value + " ";
                if (val.pos(value_w_space_after) == 0)
                    return true; // at start
                lString32 value_w_space_before = " " + _value;
                int pos = val.pos(value_w_space_before);
                if (pos != -1 && pos + value_w_space_before.length() == val.length())
                    return true; // at end
                lString32 value_w_spaces_before_after = " " + _value + " ";
                if (val.pos(value_w_spaces_before_after) != -1)
                    return true; // in between
                return false;
            }
            return val == _value;
        } break;
        case cssrt_universal:   // *
            return true;        // should it be: return !node->isBoxingNode(); ?
        case cssrt_pseudoclass: // E:pseudo-class
        {
            int nodeId;
            switch (_attrid) {
                case csspc_root: {
                    // We never have any CSS when meeting the crengine root node.
                    // Only when using :root in our cr3gui/data/*.css we get a chance
                    // to meet the root node's first child node, which is not always <html>.
                    // The elements hierarchy may be:
                    //   <html> <body> with plain HTML files.
                    //   <body> <DocFragment> <body> with EPUB documents.
                    // The embedded stylesheets, being stored as attribute/child of <body>
                    // or <DocFragment> are not yet there when metting the <html> or the
                    // first <body> node.
                    // So, we can only try to match the <body> that is a child of
                    // <html> or <DocFragment>, and apply this style to it.
                    // If we were to use :root in our cr3gui/data/*.css, we would meet
                    // the first <body> or the <html> node, but to avoid applyng the
                    // style twice (to the 2 <body>s), we want to NOT match the first
                    // node.
                    ldomNode* parent = node->getUnboxedParent(exceptBoxingNodeId);
                    if (!parent || parent->isRoot())
                        return false; // we do not want to return true;
                    lUInt16 parentNodeId = parent->getNodeId();
                    return parentNodeId == el_DocFragment || parentNodeId == el_html;
                    // Note: to override, with style tweaks, styles set with :root,
                    // it should be enough to use body { ... !important }.
                    // The '!important' is needed because :root has a higher
                    // specificity that a simple body {}.
                } break;
                case csspc_empty:
                    return node->getChildCount() == 0;
                    break;
                case csspc_dir: {
                    // We're looking at parents, but we don't want to update 'node'
                    const ldomNode* elem = node;
                    while (elem) {
                        if (!elem->hasAttribute(attr_dir)) {
                            // No need to use getUnboxedParent(), boxes don't have this attribute
                            elem = elem->getParentNode();
                            continue;
                        }
                        lString32 dir = elem->getAttributeValue(attr_dir);
                        dir = dir.lowercase(); // (no need for trim(), it's done by the XMLParser)
                        if (dir.compare(_value) == 0)
                            return true;
                        // We could ignore invalide values, but for now, just stop looking.
                        return false;
                    }
                    return false;
                } break;
                case csspc_first_child:
                case csspc_first_of_type: {
                    int n; // 1 = false, 2 = true (should not be 0 for caching)
                    if (!get_cached_node_checked_property(node, _attrid, n)) {
                        n = 2; // true
                        if (_attrid == csspc_first_of_type)
                            nodeId = node->getNodeId();
                        const ldomNode* elem = node;
                        for (;;) {
                            elem = elem->getUnboxedPrevSibling(true, exceptBoxingNodeId); // skip text nodes
                            if (!elem)
                                break;
                            // We have a previous sibling
                            if (_attrid == csspc_first_child || elem->getNodeId() == nodeId) {
                                n = 1; // false, we're not the first
                                break;
                            }
                        }
                        cache_node_checked_property(node, _attrid, n);
                    }
                    return n == 2;
                } break;
                case csspc_last_child:
                case csspc_last_of_type: {
                    int n; // 1 = false, 2 = true (should not be 0 for caching)
                    if (!get_cached_node_checked_property(node, _attrid, n)) {
                        n = 2; // true
                        if (_attrid == csspc_last_of_type)
                            nodeId = node->getNodeId();
                        const ldomNode* elem = node;
                        for (;;) {
                            elem = elem->getUnboxedNextSibling(true, exceptBoxingNodeId); // skip text nodes
                            if (!elem)
                                break;
                            // We have a next sibling
                            if (_attrid == csspc_last_child || elem->getNodeId() == nodeId) {
                                n = 1; // false, we're not the last
                                break;
                            }
                        }
                        cache_node_checked_property(node, _attrid, n);
                    }
                    return n == 2;
                } break;
                case csspc_nth_child:
                case csspc_nth_of_type: {
                    int n;
                    if (!get_cached_node_checked_property(node, _attrid, n)) {
                        if (_attrid == csspc_nth_of_type)
                            nodeId = node->getNodeId();
                        const ldomNode* elem = node;
                        n = 1;
                        for (;;) {
                            elem = elem->getUnboxedPrevSibling(true, exceptBoxingNodeId); // skip text nodes
                            if (!elem)
                                break;
                            if (_attrid == csspc_nth_child || elem->getNodeId() == nodeId)
                                n++;
                        }
                        cache_node_checked_property(node, _attrid, n);
                    }
                    return match_nth_value(_value, n);
                } break;
                case csspc_nth_last_child:
                case csspc_nth_last_of_type: {
                    int n;
                    if (!get_cached_node_checked_property(node, _attrid, n)) {
                        if (_attrid == csspc_nth_last_of_type)
                            nodeId = node->getNodeId();
                        const ldomNode* elem = node;
                        n = 1;
                        for (;;) {
                            elem = elem->getUnboxedNextSibling(true, exceptBoxingNodeId); // skip text nodes
                            if (!elem)
                                break;
                            if (_attrid == csspc_nth_last_child || elem->getNodeId() == nodeId)
                                n++;
                        }
                        cache_node_checked_property(node, _attrid, n);
                    }
                    return match_nth_value(_value, n);
                } break;
                case csspc_only_child:
                case csspc_only_of_type: {
                    int n; // 1 = false, 2 = true (should not be 0 for caching)
                    if (!get_cached_node_checked_property(node, _attrid, n)) {
                        n = 2; // true
                        if (_attrid == csspc_only_of_type)
                            nodeId = node->getNodeId();
                        const ldomNode* elem = node->getUnboxedParent(exceptBoxingNodeId)->getUnboxedFirstChild(true, exceptBoxingNodeId);
                        while (elem) {
                            if (elem != node) {
                                if (_attrid == csspc_only_child || elem->getNodeId() == nodeId) {
                                    n = 1; // false, we're not alone
                                    break;
                                }
                            }
                            elem = elem->getUnboxedNextSibling(true, exceptBoxingNodeId);
                        }
                        cache_node_checked_property(node, _attrid, n);
                    }
                    return n == 2;
                } break;
            }
        }
            return false;
    }
    return true;
}

bool LVCssSelectorRule::checkNextRules(const ldomNode* node) {
    // Similar to LVCssSelector::check() just below, but
    // invoked from a rule
    LVCssSelectorRule* rule = getNext();
    if (!rule)
        return true;
    const ldomNode* n = node;
    do {
        if (!rule->check(n))
            return false;
        if (rule->isFullChecking())
            return true;
        rule = rule->getNext();
    } while (rule != NULL);
    return true;
}

bool LVCssSelector::check(const ldomNode* node) const {
    lUInt16 nodeId = node->getNodeId();
    if (nodeId == el_pseudoElem) {
        if (!_pseudo_elem) { // not a ::before/after rule
            // Our added pseudoElem element should not match any other rules
            // (if we added it as a child of a P element, it should not match P > *)
            return false;
        } else {
            // We might be the pseudoElem that was created by this selector.
            // Start checking the rules starting from the real parent
            // (except if this selector target a boxing element: we should
            // stop unboxing at that boxing element).
            if (_id >= EL_BOXING_START && _id <= EL_BOXING_END)
                node = node->getUnboxedParent(_id);
            else
                node = node->getUnboxedParent();
            nodeId = node->getNodeId();
        }
    } else if (_id == 0 && node->isBoxingNode()) {
        // Don't apply "... *" or '.classname' selectors to boxing nodes
        // (but let those with our internal element names ("... autoBoxing") be applied)
        return false;
    }
    // check main Id
    if (_id != 0 && nodeId != _id)
        return false;
    if (!_rules)
        return true;
    // check additional rules
    const ldomNode* n = node;
    LVCssSelectorRule* rule = _rules;
    do {
        if (!rule->check(n))
            return false;
        // cssrt_ancessor or cssrt_predsibling rules will have checked next
        // rules on each parent or sibling. If it didn't return false, it
        // found one on which next rules match: no need to check them again
        if (rule->isFullChecking())
            return true;
        rule = rule->getNext();
    } while (rule != NULL);
    return true;
}

bool parse_attr_value(const char*& str, char* buf, bool& parse_trailing_i, char stop_char = ']') {
    int pos = 0;
    skip_spaces(str);
    if (*str == '\"') {
        str++;
        for (; str[pos] && str[pos] != '\"'; pos++) {
            if (pos >= 64)
                return false;
        }
        if (str[pos] != '\"')
            return false;
        for (int i = 0; i < pos; i++)
            buf[i] = str[i];
        buf[pos] = 0;
        str += pos + 1;
        skip_spaces(str);
        // The trailing ' i' must be outside the quotes
        if (parse_trailing_i) {
            parse_trailing_i = false;
            if (*str == 'i' || *str == 'I') {
                parse_trailing_i = true;
                str++;
                skip_spaces(str);
            }
        }
        if (*str != stop_char)
            return false;
        str++;
        return true;
    } else {
        for (; str[pos] && str[pos] != ' ' && str[pos] != '\t' && str[pos] != stop_char; pos++) {
            if (pos >= 64)
                return false;
        }
        int end_pos = pos;
        if (parse_trailing_i) {
            parse_trailing_i = false;
            if (end_pos == 0) // Empty value, or some leading space: this is invalid
                return false;
            if (str[pos] && str[pos] == ' ' && str[pos + 1] && (str[pos + 1] == 'i' || str[pos + 1] == 'I')) {
                parse_trailing_i = true;
                pos += 2;
            }
        }
        if (str[pos] != stop_char)
            return false;
        for (int i = 0; i < end_pos; i++)
            buf[i] = str[i];
        buf[end_pos] = 0;
        str += pos;
        str++;
        return true;
    }
}

bool parse_attr_value(const char*& str, char* buf, char stop_char = ']') {
    bool parse_trailing_i = false;
    return parse_attr_value(str, buf, parse_trailing_i, stop_char);
}

LVCssSelectorRule* parse_attr(const char*& str, lxmlDocBase* doc) {
    // We should not skip_spaces() here: it's invalid just after one of '.#:'
    // and we should keep the one after the parsed value as its presence or not
    // has a different meaning (no space: multiple attributes or classnames
    // selector - space: descendant combinator)
    char attrname[512];
    char attrvalue[512];
    LVCssSelectorRuleType st = cssrt_universal;
    if (*str == '.') {
        // E.class
        str++;
        if (!parse_ident(str, attrvalue))
            return NULL;
        LVCssSelectorRule* rule = new LVCssSelectorRule(cssrt_class);
        lString32 s(attrvalue);
        // s.lowercase(); // className should be case sensitive
        rule->setAttr(attr_class, s);
        return rule;
    } else if (*str == '#') {
        // E#id
        str++;
        if (!parse_ident(str, attrvalue))
            return NULL;
        LVCssSelectorRule* rule = new LVCssSelectorRule(cssrt_id);
        lString32 s(attrvalue);
        rule->setAttr(attr_id, s);
        return rule;
    } else if (*str == ':') {
        // E:pseudo-class (eg: E:first-child)
        str++;
        if (*str == ':') {
            // pseudo element (double ::, eg: E::first-line) are not supported,
            // except ::before/after which are handled in LVCssSelector::parse()
            str--;
            return NULL;
        }
        int n = parse_name(str, css_pseudo_classes, -1);
        if (n == -1) { // not one of out supported pseudo classes
            str--;     // LVCssSelector::parse() will also check for :before/after with a single ':'
            return NULL;
        }
        attrvalue[0] = 0;
        if (*str == '(') { // parse () content
            str++;
            if (!parse_attr_value(str, attrvalue, ')'))
                return NULL;
            // We don't parse the value here, it may have specific meaning
            // per pseudo-class type
            // But for the ones we handle, we only compare strings to a fixed set of target
            // values, so trim() and lowercase() below to avoid doing it on each check.
        }
        LVCssSelectorRule* rule = new LVCssSelectorRule(cssrt_pseudoclass);
        lString32 s(attrvalue);
        s.trim().lowercase();
        if (n == csspc_nth_child || n == csspc_nth_of_type || n == csspc_nth_last_child || n == csspc_nth_last_of_type) {
            // Parse "even", "odd", "5", "5n", "5n+2", "-n" into a few
            // numbers packed into a lString32, for quicker checking.
            s = parse_nth_value(s);
        }
        rule->setAttr(n, s);
        // printf("made pseudo class rule %d with %s\n", n, UnicodeToLocal(s).c_str());
        if (n >= csspc_last_child) {
            // Pseudoclasses after csspc_last_child can't be accurately checked
            // in the initial loading phase: a re-render will be needed.
            doc->setNodeStylesInvalidIfLoading();
            // There might still be some issues if CSS would set some display: property
            // as, when re-rendering, a cache might be present and prevent modifying
            // the DOM for some needed autoBoxing - or the invalid styles set now
            // while loading would have created some autoBoxing that we won't be
            // able to remove...
        }
        return rule;
    } else if (*str != '[') // We're looking for an attribute selector after here
        return NULL;
    str++;
    // We may find and skip spaces inside [...]
    skip_spaces(str);
    if (!parse_ident(str, attrname))
        return NULL;
    skip_spaces(str);
    attrvalue[0] = 0;
    bool parse_trailing_i = false;
    if (*str == ']') {
        st = cssrt_attrset;
        str++;
    } else if (*str == '=') {
        str++;
        parse_trailing_i = true; // reset to false if value does not end with " i"
        if (!parse_attr_value(str, attrvalue, parse_trailing_i))
            return NULL;
        if (parse_trailing_i)
            st = cssrt_attreq_i;
        else
            st = cssrt_attreq;
    } else if (*str == '~' && str[1] == '=') {
        str += 2;
        parse_trailing_i = true;
        if (!parse_attr_value(str, attrvalue, parse_trailing_i))
            return NULL;
        if (parse_trailing_i)
            st = cssrt_attrhas_i;
        else
            st = cssrt_attrhas;
    } else if (*str == '|' && str[1] == '=') {
        str += 2;
        parse_trailing_i = true;
        if (!parse_attr_value(str, attrvalue, parse_trailing_i))
            return NULL;
        if (parse_trailing_i)
            st = cssrt_attrstarts_word_i;
        else
            st = cssrt_attrstarts_word;
    } else if (*str == '^' && str[1] == '=') {
        str += 2;
        parse_trailing_i = true;
        if (!parse_attr_value(str, attrvalue, parse_trailing_i))
            return NULL;
        if (parse_trailing_i)
            st = cssrt_attrstarts_i;
        else
            st = cssrt_attrstarts;
    } else if (*str == '$' && str[1] == '=') {
        str += 2;
        parse_trailing_i = true;
        if (!parse_attr_value(str, attrvalue, parse_trailing_i))
            return NULL;
        if (parse_trailing_i)
            st = cssrt_attrends_i;
        else
            st = cssrt_attrends;
    } else if (*str == '*' && str[1] == '=') {
        str += 2;
        parse_trailing_i = true;
        if (!parse_attr_value(str, attrvalue, parse_trailing_i))
            return NULL;
        if (parse_trailing_i)
            st = cssrt_attrcontains_i;
        else
            st = cssrt_attrcontains;
    } else {
        return NULL;
    }
    LVCssSelectorRule* rule = new LVCssSelectorRule(st);
    lString32 s(attrvalue);
    if (parse_trailing_i) { // cssrt_attr*_i met
        s.lowercase();
    }
    lUInt16 id = doc->getAttrNameIndex(lString32(attrname).c_str());
    rule->setAttr(id, s);
    return rule;
}

void LVCssSelector::insertRuleStart(LVCssSelectorRule* rule) {
    rule->setNext(_rules);
    _rules = rule;
}

void LVCssSelector::insertRuleAfterStart(LVCssSelectorRule* rule) {
    if (!_rules) {
        _rules = rule;
        return;
    }
    rule->setNext(_rules->getNext());
    _rules->setNext(rule);
}

bool LVCssSelector::parse(const char*& str, lxmlDocBase* doc) {
    if (!str || !*str)
        return false;
    for (;;) {
        skip_spaces(str);
        // We need to skip spaces in the generic parsing, but we need to
        // NOT check for attributes (.class, [attr=val]...) if we did skip
        // some spaces (because "DIV .classname" has a different meaning
        // (ancestor/descendant combinator) than "DIV.classname" (element
        // with classname)).
        bool check_attribute_rules = true;
        if (*str == '*') // universal selector
        {
            str++;
            if (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r')
                check_attribute_rules = false;
            skip_spaces(str);
            _id = 0;
        } else if (*str == '.') // classname follows
        {
            _id = 0;
        } else if (*str == ':') // pseudoclass follows
        {
            _id = 0;
        } else if (*str == '[') // attribute selector follows
        {
            _id = 0;
        } else if (*str == '#') // node Id follows
        {
            _id = 0;                   // (elementName internal id)
        } else if (css_is_alpha(*str)) // element name follows
        {
            // ident
            char ident[64];
            if (!parse_ident(str, ident))
                return false;
            // All element names have been lowercased by HTMLParser (except
            // a few ones that are added explicitely by crengine): we need
            // to lowercase them here too to expect a match.
            lString32 element(ident);
            if (element.length() < 7) {
                // Avoid following string comparisons if element name string
                // is shorter than the shortest of them (rubyBox)
                element = element.lowercase();
            } else if (element != "DocFragment" &&
                       element != "autoBoxing" && element != "tabularBox" &&
                       element != "rubyBox" && element != "mathBox" &&
                       element != "floatBox" && element != "inlineBox" &&
                       element != "pseudoElem" && element != "FictionBook") {
                element = element.lowercase();
            }
            _id = doc->getElementNameIndex(element.c_str());
            // Note: non standard element names (not listed in fb2def.h) in
            // selectors (eg: blah {font-style: italic}) may have different values
            // returned by getElementNameIndex() across book loadings, and cause:
            // "cached rendering is invalid (style hash mismatch): doing full rendering"
            _specificity += WEIGHT_SPECIFICITY_ELEMENT; // we have an element: update specificity
            if (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r')
                check_attribute_rules = false;
            skip_spaces(str);
        } else {
            return false;
        }
        if (*str == ',' || *str == '{')
            return true;
        // one or more attribute rules
        bool attr_rule = false;
        if (check_attribute_rules) {
            while (*str == '[' || *str == '.' || *str == '#' || *str == ':') {
                LVCssSelectorRule* rule = parse_attr(str, doc);
                if (!rule) {
                    // Might be one of our supported pseudo elements, which should
                    // start with "::" but might start with a single ":".
                    // These pseudo element do not add a LVCssSelectorRule.
                    if (*str == ':') {
                        str++;
                        if (*str == ':') // skip double ::
                            str++;
                        int n = parse_name(str, css_pseudo_elements, -1);
                        if (n != -1) {
                            _pseudo_elem = n + 1; // starts at 1
                            _specificity += WEIGHT_SPECIFICITY_ELEMENT;
                            // Done with this selector: we expect ::before and ::after
                            // to come always last, and are not followed by other rules.
                            // ("x::before::before" seems not ensured by Firefox - if we
                            // stop between them, the 2nd "::before" will make the parsing
                            // of the declaration invalid, and so this rule.)
                            return true;
                        }
                    }
                    return false;
                }
                insertRuleStart(rule); //insertRuleAfterStart
                //insertRuleAfterStart( rule ); //insertRuleAfterStart
                _specificity += rule->getWeight();

                /*
                if ( _id!=0 ) {
                    LVCssSelectorRule * rule = new LVCssSelectorRule(cssrt_parent);
                    rule->setId(_id);
                    insertRuleStart( rule );
                    _id=0;
                }
                */

                // We should not skip spaces here: combining multiple classnames or
                // attributes is to be done only when there is no space in between
                // them. Otherwise, it's a descendant combinator (cssrt_ancessor).

                attr_rule = true;
                //continue;
            }
            // Skip any space now after all combining attributes or classnames have been parsed
            skip_spaces(str);
        }
        // element relation
        if (*str == '>') {
            str++;
            LVCssSelectorRule* rule = new LVCssSelectorRule(cssrt_parent);
            rule->setId(_id);
            insertRuleStart(rule);
            _specificity += rule->getWeight();
            _id = 0;
            continue;
        } else if (*str == '+') {
            str++;
            LVCssSelectorRule* rule = new LVCssSelectorRule(cssrt_predecessor);
            rule->setId(_id);
            insertRuleStart(rule);
            _specificity += rule->getWeight();
            _id = 0;
            continue;
        } else if (*str == '~') {
            str++;
            LVCssSelectorRule* rule = new LVCssSelectorRule(cssrt_predsibling);
            rule->setId(_id);
            insertRuleStart(rule);
            _specificity += rule->getWeight();
            _id = 0;
            continue;
        } else if (css_is_alpha(*str) || (*str == '.') || (*str == '#') || (*str == '*')) {
            LVCssSelectorRule* rule = new LVCssSelectorRule(cssrt_ancessor);
            rule->setId(_id);
            insertRuleStart(rule);
            _specificity += rule->getWeight();
            _id = 0;
            continue;
        }
        if (!attr_rule)
            return false;
        else if (*str == ',' || *str == '{')
            return true;
    }
}

static bool skip_until_end_of_rule(const char*& str) {
    while (*str && *str != '}')
        str++;
    if (*str == '}')
        str++;
    return *str != 0;
}

void LVCssSelector::applyToPseudoElement(const ldomNode* node, css_style_rec_t* style) const {
    // This might be called both on the node that match the selector (we should
    // not apply to the style of this node), and on the actual pseudo element
    // once it has been created as a child (to which we should apply).
    css_style_rec_t* target_style = NULL;
    if (node->getNodeId() == el_pseudoElem) {
        if ((_pseudo_elem == csspe_before && node->hasAttribute(attr_Before)) || (_pseudo_elem == csspe_after && node->hasAttribute(attr_After))) {
            target_style = style;
        }
    } else {
        // For the matching node, we create two style slots to which we apply
        // the declaration. This is just to have all styles applied and see
        // at the end if the pseudo element is display:none or not, and if
        // it should be skipped or created.
        // These css_style_rec_t are just temp slots to gather what's applied,
        // they are not the ones that will be associated to the pseudo element.
        if (_pseudo_elem == csspe_before) {
            if (!style->pseudo_elem_before_style) {
                style->pseudo_elem_before_style = new css_style_rec_t;
            }
            target_style = style->pseudo_elem_before_style;
        } else if (_pseudo_elem == csspe_after) {
            if (!style->pseudo_elem_after_style) {
                style->pseudo_elem_after_style = new css_style_rec_t;
            }
            target_style = style->pseudo_elem_after_style;
        }
    }

    if (target_style) {
        if (!(target_style->flags & STYLE_REC_FLAG_MATCHED)) {
            // pseudoElem starts with "display: none" (in case they were created and
            // inserted in the DOM by a CSS selector that can later disappear).
            // Switch them to "display: inline" when we meet such a selector.
            // (The coming up _decl->apply() may not update ->display, or it may set
            // it explicitely to css_d_none, that we don't want reset to inline.)
            target_style->display = css_d_inline;
            target_style->flags |= STYLE_REC_FLAG_MATCHED;
        }
        // And apply this selector styling.
        _decl->apply(target_style);
    }
    return;
}

LVCssSelectorRule::LVCssSelectorRule(LVCssSelectorRule& v)
        : _type(v._type)
        , _id(v._id)
        , _attrid(v._attrid)
        , _next(NULL)
        , _value(v._value) {
    if (v._next)
        _next = new LVCssSelectorRule(*v._next);
}

LVCssSelector::LVCssSelector(LVCssSelector& v)
        : _id(v._id)
        , _decl(v._decl)
        , _specificity(v._specificity)
        , _pseudo_elem(v._pseudo_elem)
        , _next(NULL)
        , _rules(NULL) {
    if (v._next)
        _next = new LVCssSelector(*v._next);
    if (v._rules)
        _rules = new LVCssSelectorRule(*v._rules);
}

void LVStyleSheet::set(LVPtrVector<LVCssSelector>& v) {
    _selectors.clear();
    if (!v.size())
        return;
    _selectors.reserve(v.size());
    for (int i = 0; i < v.size(); i++) {
        LVCssSelector* selector = v[i];
        if (selector)
            _selectors.add(new LVCssSelector(*selector));
        else
            _selectors.add(NULL);
    }
}

LVStyleSheet::LVStyleSheet(LVStyleSheet& sheet)
        : _doc(sheet._doc) {
    set(sheet._selectors);
    _selector_count = sheet._selector_count;
    _charset = sheet._charset;
}

void LVStyleSheet::apply(const ldomNode* node, css_style_rec_t* style) {
    if (!_selectors.length())
        return; // no rules!

    lUInt16 id = node->getNodeId();
    if (id == el_pseudoElem) { // get the id chain from the parent element
        // Note that a "div:before {float:left}" will result in: <div><floatBox><pseudoElem>
        // There is just one kind of boxing element that is explicitely
        // added when parsing MathML (and can't be implicitely added) that
        // could generate pseudo elements: <mathBox>. So, don't skip them
        id = node->getUnboxedParent(el_mathBox)->getNodeId();
    }

    // _selectors[0] holds the ordered chain of selectors starting (from
    // the right of the selector) with a rule with no element name attached
    // (eg. "div p .quote1", class name .quote1 should be checked against
    // all elements' classnames before continuing checking for ancestors).
    // _selectors[element_name_id] holds the ordered chain of selector starting
    // with that element name (eg. ".body div.chapter > p" should be
    // first checked agains all <p>).
    // To see which selectors apply to a <p>, we must iterate thru both chains,
    // checking and applying them in the order of specificity/parsed position.
    LVCssSelector* selector_0 = _selectors[0];
    LVCssSelector* selector_id = id > 0 && id < _selectors.length() ? _selectors[id] : NULL;

    for (;;) {
        if (selector_0 != NULL) {
            if (selector_id == NULL || selector_0->getSpecificity() < selector_id->getSpecificity()) {
                // step by sel_0
                selector_0->apply(node, style);
                selector_0 = selector_0->getNext();
            } else {
                // step by sel_id
                selector_id->apply(node, style);
                selector_id = selector_id->getNext();
            }
        } else if (selector_id != NULL) {
            // step by sel_id
            selector_id->apply(node, style);
            selector_id = selector_id->getNext();
        } else {
            break; // end of chains
        }
    }
}

lUInt32 LVCssSelectorRule::getHash() {
    lUInt32 hash = 0;
    hash = ((((lUInt32)_type * 31 + (lUInt32)_id) * 31) + (lUInt32)_attrid * 31) + ::getHash(_value);
    return hash;
}

lUInt32 LVCssSelector::getHash() {
    lUInt32 hash = 0;
    lUInt32 nextHash = 0;

    if (_next)
        nextHash = _next->getHash();
    for (LVCssSelectorRule* p = _rules; p; p = p->getNext()) {
        lUInt32 ruleHash = p->getHash();
        hash = hash * 31 + ruleHash;
    }
    hash = hash * 31 + nextHash;
    hash = hash * 31 + _specificity;
    hash = hash * 31 + _pseudo_elem;
    if (!_decl.isNull())
        hash = hash * 31 + _decl->getHash();
    //CRLog::trace("selector hash: %8x", hash);
    return hash;
}

/// calculate hash
lUInt32 LVStyleSheet::getHash() {
    lUInt32 hash = 0;
    for (int i = 0; i < _selectors.length(); i++) {
        if (_selectors[i])
            hash = hash * 31 + _selectors[i]->getHash() + i * 15324;
    }
    //CRLog::trace("LVStyleSheet::getHash() selector count: %d  hash: %x", _selectors.length(), hash);
    return hash;
}

bool LVStyleSheet::parse(const char* str, bool higher_importance, lString32 codeBase) {
    if (!_doc) {
        // We can't parse anything if no _doc to get element name ids from
        return false;
    }
    LVCssSelector* selector = NULL;
    LVCssSelector* prev_selector;
    int err_count = 0;
    int rule_count = 0;
    lUInt32 domVersionRequested = _doc->getDOMVersionRequested();
    for (; *str;) {
        // new rule
        prev_selector = NULL;
        bool err = false;
        for (; *str;) {
            // parse charset, and... ignored it
            // just to avoid generating a parse error
            parseCharsetRule(str);
            // parse selector(s)
            // Have selector count number make the initial value
            // of _specificity, so order of selectors is preserved
            // when applying selectors with the same CSS specificity.
            selector = new LVCssSelector(_selector_count);
            _selector_count += 1; // = +WEIGHT_SELECTOR_ORDER
            selector->setNext(prev_selector);
            if (!selector->parse(str, _doc)) {
                err = true;
                break;
            } else {
                if (*str == ',') {
                    str++;
                    prev_selector = selector;
                    continue; // next selector
                }
            }
            // parse declaration
            LVCssDeclRef decl(new LVCssDeclaration);
            if (!decl->parse(str, domVersionRequested, higher_importance, _doc, codeBase)) {
                err = true;
                err_count++;
            } else {
                // set decl to selectors
                for (LVCssSelector* p = selector; p; p = p->getNext())
                    p->setDeclaration(decl);
                rule_count++;
            }
            break;
        }
        if (err) {
            // error:
            // delete chain of selectors
            delete selector;
            // ignore current rule
            skip_until_end_of_rule(str);
        } else {
            // Ok:
            // place rules to sheet
            for (LVCssSelector* p = selector; p;) {
                LVCssSelector* item = p;
                p = p->getNext();
                lUInt16 id = item->getElementNameId();
                if (_selectors.length() <= id)
                    _selectors.set(id, NULL);
                // insert with specificity sorting
                if (_selectors[id] == NULL || _selectors[id]->getSpecificity() > item->getSpecificity()) {
                    // insert as first item
                    item->setNext(_selectors[id]);
                    _selectors[id] = item;
                } else {
                    // insert as internal item
                    for (LVCssSelector* p = _selectors[id]; p; p = p->getNext()) {
                        if (p->getNext() == NULL || p->getNext()->getSpecificity() > item->getSpecificity()) {
                            item->setNext(p->getNext());
                            p->setNext(item);
                            break;
                        }
                    }
                }
            }
        }
    }
    return _selectors.length() > 0;
}

bool LVStyleSheet::parseCharsetRule(const char*& str) {
    // Parse rule '@charset' according https://developer.mozilla.org/en-US/docs/Web/CSS/@charset
    if (!str || !*str)
        return false;
    skip_spaces(str);
    if (*str == '@') {
        str++;
        char word[64];
        if (parse_ident(str, word)) {
            lString8 keyword(word);
            if (keyword == "charset") {
                // skip required space(s)
                if (*str == ' ' || *str == '\t') {
                    skip_spaces(str);
                    if (parse_attr_value(str, word, ';')) {
                        if (_charset.empty()) {
                            _charset = word;
                            return true;
                        }
                    }
                }
            }
        }
    }
    return false;
}

/// extract @import filename from beginning of CSS
bool LVProcessStyleSheetImport(const char*& str, lString8& import_file) {
    const char* p = str;
    import_file.clear();
    skip_spaces(p);
    if (*p != '@')
        return false;
    p++;
    if (strncmp(p, "import", 6) != 0)
        return false;
    p += 6;
    skip_spaces(p);
    bool in_url = false;
    char quote_ch = 0;
    if (!strncmp(p, "url", 3)) {
        p += 3;
        skip_spaces(p);
        if (*p != '(')
            return false;
        p++;
        skip_spaces(p);
        in_url = true;
    }
    if (*p == '\'' || *p == '\"')
        quote_ch = *p++;
    while (*p) {
        if (quote_ch && *p == quote_ch) {
            p++;
            break;
        }
        if (!quote_ch) {
            if (in_url && *p == ')') {
                break;
            }
            if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')
                break;
        }
        import_file << *p++;
    }
    skip_spaces(p);
    if (in_url) {
        if (*p != ')')
            return false;
        p++;
    }
    // Remove trailing ';' at end of "@import url(..);"
    skip_spaces(p);
    if (*p == ';')
        p++;
    if (import_file.empty())
        return false;
    str = p;
    return true;
}

/// load stylesheet from file, with processing of import
bool LVLoadStylesheetFile(lString32 pathName, lString8& css) {
    LVStreamRef file = LVOpenFileStream(pathName.c_str(), LVOM_READ);
    if (file.isNull())
        return false;
    lString8 txt = UnicodeToUtf8(LVReadTextFile(file));
    lString8 txt2;
    const char* s = txt.c_str();
    lString8 import_file;
    if (LVProcessStyleSheetImport(s, import_file)) {
        lString32 importFilename = LVMakeRelativeFilename(pathName, Utf8ToUnicode(import_file));
        //lString8 ifn = UnicodeToLocal(importFilename);
        //const char * ifns = ifn.c_str();
        if (!importFilename.empty()) {
            LVStreamRef file2 = LVOpenFileStream(importFilename.c_str(), LVOM_READ);
            if (!file2.isNull())
                txt2 = UnicodeToUtf8(LVReadTextFile(file2));
        }
    }
    if (!txt2.empty())
        txt2 << "\r\n";
    css = txt2 + s;
    return !css.empty();
}
