/*
 * Copyright 2009 John-Mark Bell <jmb@netsurf-browser.org>
 *
 * This file is part of NetSurf, http://www.netsurf-browser.org/
 *
 * NetSurf 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; version 2 of the License.
 *
 * NetSurf 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, see <http://www.gnu.org/licenses/>.
 */

#include <assert.h>
#include <stdbool.h>
#include <string.h>
#include <strings.h>

#include "content/content_protected.h"
#include "content/urldb.h"
#include "css/internal.h"
#include "css/select.h"
#include "css/utils.h"
#include "desktop/gui.h"
#include "desktop/options.h"
#include "utils/corestrings.h"
#include "utils/log.h"
#include "utils/url.h"
#include "utils/utils.h"

static css_error node_name(void *pw, void *node, css_qname *qname);
static css_error node_classes(void *pw, void *node,
		lwc_string ***classes, uint32_t *n_classes);
static css_error node_id(void *pw, void *node, lwc_string **id);
static css_error named_ancestor_node(void *pw, void *node,
		const css_qname *qname, void **ancestor);
static css_error named_parent_node(void *pw, void *node,
		const css_qname *qname, void **parent);
static css_error named_sibling_node(void *pw, void *node,
		const css_qname *qname, void **sibling);
static css_error named_generic_sibling_node(void *pw, void *node,
		const css_qname *qname, void **sibling);
static css_error parent_node(void *pw, void *node, void **parent);
static css_error sibling_node(void *pw, void *node, void **sibling);
static css_error node_has_name(void *pw, void *node,
		const css_qname *qname, bool *match);
static css_error node_has_class(void *pw, void *node,
		lwc_string *name, bool *match);
static css_error node_has_id(void *pw, void *node,
		lwc_string *name, bool *match);
static css_error node_has_attribute(void *pw, void *node,
		const css_qname *qname, bool *match);
static css_error node_has_attribute_equal(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match);
static css_error node_has_attribute_dashmatch(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match);
static css_error node_has_attribute_includes(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match);
static css_error node_has_attribute_prefix(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match);
static css_error node_has_attribute_suffix(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match);
static css_error node_has_attribute_substring(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match);
static css_error node_is_root(void *pw, void *node, bool *match);
static css_error node_count_siblings(void *pw, void *node,
		bool same_name, bool after, int32_t *count);
static css_error node_is_empty(void *pw, void *node, bool *match);
static css_error node_is_link(void *pw, void *node, bool *match);
static css_error node_is_visited(void *pw, void *node, bool *match);
static css_error node_is_hover(void *pw, void *node, bool *match);
static css_error node_is_active(void *pw, void *node, bool *match);
static css_error node_is_focus(void *pw, void *node, bool *match);
static css_error node_is_enabled(void *pw, void *node, bool *match);
static css_error node_is_disabled(void *pw, void *node, bool *match);
static css_error node_is_checked(void *pw, void *node, bool *match);
static css_error node_is_target(void *pw, void *node, bool *match);
static css_error node_is_lang(void *pw, void *node,
		lwc_string *lang, bool *match);
static css_error node_presentational_hint(void *pw, void *node,
		uint32_t property, css_hint *hint);
static css_error ua_default_for_property(void *pw, uint32_t property,
		css_hint *hint);

static int cmp_colour_name(const void *a, const void *b);
static bool parse_named_colour(const char *data, css_color *result);
static bool parse_dimension(const char *data, bool strict,
		css_fixed *length, css_unit *unit);
static bool parse_number(const char *data, bool non_negative, bool real,
		css_fixed *value, size_t *consumed);
static bool parse_font_size(const char *size, uint8_t *val, 
		css_fixed *len, css_unit *unit);

static css_computed_style *nscss_get_initial_style(nscss_select_ctx *ctx,
		css_allocator_fn, void *pw);

static bool isWhitespace(char c);
static bool isHex(char c);
static uint8_t charToHex(char c);

/**
 * Selection callback table for libcss
 */
static css_select_handler selection_handler = {
	CSS_SELECT_HANDLER_VERSION_1,

	node_name,
	node_classes,
	node_id,
	named_ancestor_node,
	named_parent_node,
	named_sibling_node,
	named_generic_sibling_node,
	parent_node,
	sibling_node,
	node_has_name,
	node_has_class,
	node_has_id,
	node_has_attribute,
	node_has_attribute_equal,
	node_has_attribute_dashmatch,
	node_has_attribute_includes,
	node_has_attribute_prefix,
	node_has_attribute_suffix,
	node_has_attribute_substring,
	node_is_root,
	node_count_siblings,
	node_is_empty,
	node_is_link,
	node_is_visited,
	node_is_hover,
	node_is_active,
	node_is_focus,
	node_is_enabled,
	node_is_disabled,
	node_is_checked,
	node_is_target,
	node_is_lang,
	node_presentational_hint,
	ua_default_for_property,
	nscss_compute_font_size
};

/**
 * Create an inline style
 *
 * \param data          Source data
 * \param len           Length of data in bytes
 * \param charset       Charset of data, or NULL if unknown
 * \param url           URL of document containing data
 * \param allow_quirks  True to permit CSS parsing quirks
 * \param alloc         Memory allocation function
 * \param pw            Private word for allocator
 * \return Pointer to stylesheet, or NULL on failure.
 */
css_stylesheet *nscss_create_inline_style(const uint8_t *data, size_t len,
		const char *charset, const char *url, bool allow_quirks,
		css_allocator_fn alloc, void *pw)
{
	css_stylesheet_params params;
	css_stylesheet *sheet;
	css_error error;

	params.params_version = CSS_STYLESHEET_PARAMS_VERSION_1;
	params.level = CSS_LEVEL_DEFAULT;
	params.charset = charset;
	params.url = url;
	params.title = NULL;
	params.allow_quirks = allow_quirks;
	params.inline_style = true;
	params.resolve = nscss_resolve_url;
	params.resolve_pw = NULL;
	params.import = NULL;
	params.import_pw = NULL;
	params.color = gui_system_colour;
	params.color_pw = NULL;
	params.font = NULL;
	params.font_pw = NULL;

	error = css_stylesheet_create(&params, alloc, pw, &sheet);
	if (error != CSS_OK) {
		LOG(("Failed creating sheet: %d", error));
		return NULL;
	}

	error = css_stylesheet_append_data(sheet, data, len);
	if (error != CSS_OK && error != CSS_NEEDDATA) {
		LOG(("failed appending data: %d", error));
		css_stylesheet_destroy(sheet);
		return NULL;
	}

	error = css_stylesheet_data_done(sheet);
	if (error != CSS_OK) {
		LOG(("failed completing parse: %d", error));
		css_stylesheet_destroy(sheet);
		return NULL;
	}

	return sheet;
}

/**
 * Get a style selection results (partial computed styles) for an element
 *
 * \param ctx             CSS selection context
 * \param n               Element to select for
 * \param media           Permitted media types
 * \param inline_style    Inline style associated with element, or NULL
 * \param alloc           Memory allocation function
 * \param pw              Private word for allocator
 * \return Pointer to selection results (containing partial computed styles),
 *         or NULL on failure
 */
css_select_results *nscss_get_style(nscss_select_ctx *ctx, dom_node *n,
		uint64_t media, const css_stylesheet *inline_style,
		css_allocator_fn alloc, void *pw)
{
	css_select_results *styles;
	css_error error;

	error = css_select_style(ctx->ctx, n, media, inline_style, 
			&selection_handler, ctx, &styles);
	if (error != CSS_OK) {
		return NULL;
	}

	return styles;
}

/**
 * Get an initial style
 *
 * \param ctx    CSS selection context
 * \param alloc  Memory allocation function
 * \param pw     Private word for allocator
 * \return Pointer to partial computed style, or NULL on failure
 */
css_computed_style *nscss_get_initial_style(nscss_select_ctx *ctx,
		css_allocator_fn alloc, void *pw)
{
	css_computed_style *style;
	css_error error;

	error = css_computed_style_create(alloc, pw, &style);
	if (error != CSS_OK)
		return NULL;

	error = css_computed_style_initialise(style, &selection_handler, ctx);
	if (error != CSS_OK) {
		css_computed_style_destroy(style);
		return NULL;
	}

	return style;
}

/**
 * Get a blank style
 *
 * \param ctx     CSS selection context
 * \param parent  Parent style to cascade inherited properties from
 * \param alloc   Memory allocation function
 * \param pw      Private word for allocator
 * \return Pointer to blank style, or NULL on failure
 */
css_computed_style *nscss_get_blank_style(nscss_select_ctx *ctx,
		const css_computed_style *parent,
		css_allocator_fn alloc, void *pw)
{
	css_computed_style *partial;
	css_error error;

	partial = nscss_get_initial_style(ctx, alloc, pw);
	if (partial == NULL)
		return NULL;

	error = css_computed_style_compose(parent, partial,
			nscss_compute_font_size, NULL, partial);
	if (error != CSS_OK) {
		css_computed_style_destroy(partial);
		return NULL;
	}

	return partial;
}

/**
 * Font size computation callback for libcss
 *
 * \param pw      Computation context
 * \param parent  Parent font size (absolute)
 * \param size    Font size to compute
 * \return CSS_OK on success
 *
 * \post \a size will be an absolute font size
 */
css_error nscss_compute_font_size(void *pw, const css_hint *parent,
		css_hint *size)
{
	/**
	 * Table of font-size keyword scale factors
	 *
	 * These are multiplied by the configured default font size
	 * to produce an absolute size for the relevant keyword
	 */
	static const css_fixed factors[] = {
		FLTTOFIX(0.5625), /* xx-small */
		FLTTOFIX(0.6250), /* x-small */
		FLTTOFIX(0.8125), /* small */
		FLTTOFIX(1.0000), /* medium */
		FLTTOFIX(1.1250), /* large */
		FLTTOFIX(1.5000), /* x-large */
		FLTTOFIX(2.0000)  /* xx-large */
	};
	css_hint_length parent_size;

	/* Grab parent size, defaulting to medium if none */
	if (parent == NULL) {
		parent_size.value = FDIV(FMUL(factors[CSS_FONT_SIZE_MEDIUM - 1],
					      INTTOFIX(nsoption_int(font_size))), 
					 INTTOFIX(10));
		parent_size.unit = CSS_UNIT_PT;
	} else {
		assert(parent->status == CSS_FONT_SIZE_DIMENSION);
		assert(parent->data.length.unit != CSS_UNIT_EM);
		assert(parent->data.length.unit != CSS_UNIT_EX);
		assert(parent->data.length.unit != CSS_UNIT_PCT);

		parent_size = parent->data.length;
	}

	assert(size->status != CSS_FONT_SIZE_INHERIT);

	if (size->status < CSS_FONT_SIZE_LARGER) {
		/* Keyword -- simple */
		size->data.length.value = FDIV(FMUL(factors[size->status - 1],
						    INTTOFIX(nsoption_int(font_size))), 
					       F_10);
		size->data.length.unit = CSS_UNIT_PT;
	} else if (size->status == CSS_FONT_SIZE_LARGER) {
		/** \todo Step within table, if appropriate */
		size->data.length.value =
				FMUL(parent_size.value, FLTTOFIX(1.2));
		size->data.length.unit = parent_size.unit;
	} else if (size->status == CSS_FONT_SIZE_SMALLER) {
		/** \todo Step within table, if appropriate */
		size->data.length.value =
				FDIV(parent_size.value, FLTTOFIX(1.2));
		size->data.length.unit = parent_size.unit;
	} else if (size->data.length.unit == CSS_UNIT_EM ||
			size->data.length.unit == CSS_UNIT_EX) {
		size->data.length.value =
			FMUL(size->data.length.value, parent_size.value);

		if (size->data.length.unit == CSS_UNIT_EX) {
			/* 1ex = 0.6em in NetSurf */
			size->data.length.value = FMUL(size->data.length.value,
					FLTTOFIX(0.6));
		}

		size->data.length.unit = parent_size.unit;
	} else if (size->data.length.unit == CSS_UNIT_PCT) {
		size->data.length.value = FDIV(FMUL(size->data.length.value,
				parent_size.value), INTTOFIX(100));
		size->data.length.unit = parent_size.unit;
	}

	size->status = CSS_FONT_SIZE_DIMENSION;

	return CSS_OK;
}

/**
 * Parser for colours specified in attribute values.
 *
 * \param data    Data to parse (NUL-terminated)
 * \param result  Pointer to location to receive resulting css_color
 * \return true on success, false on invalid input
 */
bool nscss_parse_colour(const char *data, css_color *result)
{
	size_t len = strlen(data);
	uint8_t r, g, b;

	/* 2 */
	if (len == 0)
		return false;

	/* 3 */
	if (len == SLEN("transparent") && strcasecmp(data, "transparent") == 0)
		return false;

	/* 4 */
	if (parse_named_colour(data, result))
		return true;

	/** \todo Implement HTML5's utterly insane legacy colour parsing */

	if (data[0] == '#') {
		data++;
		len--;
	}

	if (len == 3 && isHex(data[0]) && isHex(data[1]) && isHex(data[2])) {
		r = charToHex(data[0]);
		g = charToHex(data[1]);
		b = charToHex(data[2]);

		r |= (r << 4);
		g |= (g << 4);
		b |= (b << 4);

		*result = (0xff << 24) | (r << 16) | (g << 8) | b;

		return true;
	} else if (len == 6 && isHex(data[0]) && isHex(data[1]) &&
			isHex(data[2]) && isHex(data[3]) && isHex(data[4]) &&
			isHex(data[5])) {
		r = (charToHex(data[0]) << 4) | charToHex(data[1]);
		g = (charToHex(data[2]) << 4) | charToHex(data[3]);
		b = (charToHex(data[4]) << 4) | charToHex(data[5]);

		*result = (0xff << 24) | (r << 16) | (g << 8) | b;

		return true;
	}

	return false;
}

/******************************************************************************
 * Style selection callbacks                                                  *
 ******************************************************************************/

/**
 * Callback to retrieve a node's name.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param qname  Pointer to location to receive node name
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 */
css_error node_name(void *pw, void *node, css_qname *qname)
{
	dom_node *n = node;
	dom_string *name;
	dom_exception err;

	err = dom_node_get_node_name(n, &name);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	qname->ns = NULL;

	err = dom_string_intern(name, &qname->name);
	if (err != DOM_NO_ERR) {
		dom_string_unref(name);
		return CSS_NOMEM;
	}

	dom_string_unref(name);

	return CSS_OK;
}

/**
 * Callback to retrieve a node's classes.
 *
 * \param pw         HTML document
 * \param node       DOM node
 * \param classes    Pointer to location to receive class name array
 * \param n_classes  Pointer to location to receive length of class name array
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 *
 * \note The returned array will be destroyed by libcss. Therefore, it must
 *       be allocated using the same allocator as used by libcss during style
 *       selection.
 */
css_error node_classes(void *pw, void *node, 
		lwc_string ***classes, uint32_t *n_classes)
{
	dom_node *n = node;
	dom_exception err;

	*classes = NULL;
	*n_classes = 0;

	err = dom_element_get_classes(n, classes, n_classes);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	return CSS_OK;
}

/**
 * Callback to retrieve a node's ID.
 *
 * \param pw    HTML document
 * \param node  DOM node
 * \param id    Pointer to location to receive id value
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 */
css_error node_id(void *pw, void *node, lwc_string **id)
{
	dom_node *n = node;
	dom_string *attr;
	dom_exception err;

	*id = NULL;

	/** \todo Assumes an HTML DOM */
	err = dom_html_element_get_id(n, &attr);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	if (attr != NULL) {
		err = dom_string_intern(attr, id);
		if (err != DOM_NO_ERR) {
			dom_string_unref(attr);
			return CSS_NOMEM;
		}
		dom_string_unref(attr);
	}

	return CSS_OK;
}

/**
 * Callback to find a named ancestor node.
 *
 * \param pw        HTML document
 * \param node      DOM node
 * \param qname     Node name to search for
 * \param ancestor  Pointer to location to receive ancestor
 * \return CSS_OK.
 *
 * \post \a ancestor will contain the result, or NULL if there is no match
 */
css_error named_ancestor_node(void *pw, void *node,
		const css_qname *qname, void **ancestor)
{
	dom_element_named_ancestor_node(node, qname->name,
			(struct dom_element **)ancestor);

	return CSS_OK;
}

/**
 * Callback to find a named parent node
 *
 * \param pw      HTML document
 * \param node    DOM node
 * \param qname   Node name to search for
 * \param parent  Pointer to location to receive parent
 * \return CSS_OK.
 *
 * \post \a parent will contain the result, or NULL if there is no match
 */
css_error named_parent_node(void *pw, void *node,
		const css_qname *qname, void **parent)
{
	dom_element_named_parent_node(node, qname->name,
			(struct dom_element **)parent);

	return CSS_OK;
}

/**
 * Callback to find a named sibling node.
 *
 * \param pw       HTML document
 * \param node     DOM node
 * \param qname    Node name to search for
 * \param sibling  Pointer to location to receive sibling
 * \return CSS_OK.
 *
 * \post \a sibling will contain the result, or NULL if there is no match
 */
css_error named_sibling_node(void *pw, void *node,
		const css_qname *qname, void **sibling)
{
	dom_node *n = node;
	dom_node *prev;
	dom_exception err;

	*sibling = NULL;

	/* Find sibling element */
	err = dom_node_get_previous_sibling(n, &n);
	if (err != DOM_NO_ERR)
		return CSS_OK;

	while (n != NULL) {
		dom_node_type type;

		err = dom_node_get_node_type(n, &type);
		if (err != DOM_NO_ERR) {
			dom_node_unref(n);
			return CSS_OK;
		}

		if (type == DOM_ELEMENT_NODE)
			break;

		err = dom_node_get_previous_sibling(n, &prev);
		if (err != DOM_NO_ERR) {
			dom_node_unref(n);
			return CSS_OK;
		}

		dom_node_unref(n);
		n = prev;
	}

	if (n != NULL) {
		dom_string *name;

		err = dom_node_get_node_name(n, &name);
		if (err != DOM_NO_ERR) {
			dom_node_unref(n);
			return CSS_OK;
		}

		dom_node_unref(n);

		if (dom_string_caseless_lwc_isequal(name, qname->name)) {
			*sibling = n;
		}

		dom_string_unref(name);
	}

	return CSS_OK;
}

/**
 * Callback to find a named generic sibling node.
 *
 * \param pw       HTML document
 * \param node     DOM node
 * \param qname    Node name to search for
 * \param sibling  Pointer to location to receive ancestor
 * \return CSS_OK.
 *
 * \post \a sibling will contain the result, or NULL if there is no match
 */
css_error named_generic_sibling_node(void *pw, void *node,
		const css_qname *qname, void **sibling)
{
	dom_node *n = node;
	dom_node *prev;
	dom_exception err;

	*sibling = NULL;

	err = dom_node_get_previous_sibling(n, &n);
	if (err != DOM_NO_ERR)
		return CSS_OK;

	while (n != NULL) {
		dom_node_type type;
		dom_string *name;

		err = dom_node_get_node_type(n, &type);
		if (err != DOM_NO_ERR) {
			dom_node_unref(n);
			return CSS_OK;
		}

		if (type == DOM_ELEMENT_NODE) {
			err = dom_node_get_node_name(n, &name);
			if (err != DOM_NO_ERR) {
				dom_node_unref(n);
				return CSS_OK;
			}

			if (dom_string_caseless_lwc_isequal(name, 
					qname->name)) {
				dom_string_unref(name);
				dom_node_unref(n);
				*sibling = n;
				break;
			}
			dom_string_unref(name);
		}

		err = dom_node_get_previous_sibling(n, &prev);
		if (err != DOM_NO_ERR) {
			dom_node_unref(n);
			return CSS_OK;
		}

		dom_node_unref(n);
		n = prev;
	}

	return CSS_OK;
}

/**
 * Callback to retrieve the parent of a node.
 *
 * \param pw      HTML document
 * \param node    DOM node
 * \param parent  Pointer to location to receive parent
 * \return CSS_OK.
 *
 * \post \a parent will contain the result, or NULL if there is no match
 */
css_error parent_node(void *pw, void *node, void **parent)
{
	dom_element_parent_node(node, (struct dom_element **)parent);

	return CSS_OK;
}

/**
 * Callback to retrieve the preceding sibling of a node.
 *
 * \param pw       HTML document
 * \param node     DOM node
 * \param sibling  Pointer to location to receive sibling
 * \return CSS_OK.
 *
 * \post \a sibling will contain the result, or NULL if there is no match
 */
css_error sibling_node(void *pw, void *node, void **sibling)
{
	dom_node *n = node;
	dom_node *prev;
	dom_exception err;

	*sibling = NULL;

	/* Find sibling element */
	err = dom_node_get_previous_sibling(n, &n);
	if (err != DOM_NO_ERR)
		return CSS_OK;

	while (n != NULL) {
		dom_node_type type;

		err = dom_node_get_node_type(n, &type);
		if (err != DOM_NO_ERR) {
			dom_node_unref(n);
			return CSS_OK;
		}

		if (type == DOM_ELEMENT_NODE)
			break;

		err = dom_node_get_previous_sibling(n, &prev);
		if (err != DOM_NO_ERR) {
			dom_node_unref(n);
			return CSS_OK;
		}

		dom_node_unref(n);
		n = prev;
	}

	if (n != NULL) {
		/** \todo Sort out reference counting */
		dom_node_unref(n);

		*sibling = n;
	}

	return CSS_OK;
}

/**
 * Callback to determine if a node has the given name.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param qname  Name to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_name(void *pw, void *node,
		const css_qname *qname, bool *match)
{
	nscss_select_ctx *ctx = pw;
	dom_node *n = node;

	lwc_string_isequal(qname->name, ctx->universal, match);
	if (*match == false) {
		dom_string *name;
		dom_exception err;

		err = dom_node_get_node_name(n, &name);
		if (err != DOM_NO_ERR)
			return CSS_OK;

		/* Element names are case insensitive in HTML */
		*match = dom_string_caseless_lwc_isequal(name, qname->name);

		dom_string_unref(name);
	}

	return CSS_OK;
}

/**
 * Callback to determine if a node has the given class.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param name   Name to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_class(void *pw, void *node,
		lwc_string *name, bool *match)
{
	dom_node *n = node;
	dom_exception err;

	/** \todo: Ensure that libdom performs case-insensitive 
	 * matching in quirks mode */
	err = dom_element_has_class(n, name, match);

	assert(err == DOM_NO_ERR);

	return CSS_OK;
}

/**
 * Callback to determine if a node has the given id.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param name   Name to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_id(void *pw, void *node,
		lwc_string *name, bool *match)
{
	dom_node *n = node;
	dom_string *attr;
	dom_exception err;

	*match = false;

	/** \todo Assumes an HTML DOM */
	err = dom_html_element_get_id(n, &attr);
	if (err != DOM_NO_ERR)
		return CSS_OK;

	if (attr != NULL) {
		*match = dom_string_lwc_isequal(attr, name);

		dom_string_unref(attr);
	}

	return CSS_OK;
}

/**
 * Callback to determine if a node has an attribute with the given name.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param qname  Name to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_attribute(void *pw, void *node,
		const css_qname *qname, bool *match)
{
	dom_node *n = node;
	dom_string *name;
	dom_exception err;

	err = dom_string_create_interned(
			(const uint8_t *) lwc_string_data(qname->name), 
			lwc_string_length(qname->name), &name);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	err = dom_element_has_attribute(n, name, match);
	if (err != DOM_NO_ERR) {
		dom_string_unref(name);
		return CSS_OK;
	}

	dom_string_unref(name);

	return CSS_OK;
}

/**
 * Callback to determine if a node has an attribute with given name and value.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param qname  Name to match
 * \param value  Value to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_attribute_equal(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match)
{
	dom_node *n = node;
	dom_string *name;
	dom_string *atr_val;
	dom_exception err;

	size_t vlen = lwc_string_length(value);

	if (vlen == 0) {
		*match = false;
		return CSS_OK;
	}

	err = dom_string_create_interned(
		(const uint8_t *) lwc_string_data(qname->name), 
		lwc_string_length(qname->name), &name);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	err = dom_element_get_attribute(n, name, &atr_val);
	if ((err != DOM_NO_ERR) || (atr_val == NULL)) {
		dom_string_unref(name);
		*match = false;
		return CSS_OK;
	}

	dom_string_unref(name);

	*match = dom_string_caseless_lwc_isequal(atr_val, value);

	dom_string_unref(atr_val);

	return CSS_OK;
}

/**
 * Callback to determine if a node has an attribute with the given name whose
 * value dashmatches that given.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param qname  Name to match
 * \param value  Value to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_attribute_dashmatch(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match)
{
	dom_node *n = node;
	dom_string *name;
	dom_string *atr_val;
	dom_exception err;

	size_t vlen = lwc_string_length(value);

	if (vlen == 0) {
		*match = false;
		return CSS_OK;
	}

	err = dom_string_create_interned(
		(const uint8_t *) lwc_string_data(qname->name), 
		lwc_string_length(qname->name), &name);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	err = dom_element_get_attribute(n, name, &atr_val);
	if ((err != DOM_NO_ERR) || (atr_val == NULL)) {
		dom_string_unref(name);
		*match = false;
		return CSS_OK;
	}

	dom_string_unref(name);

	/* check for exact match */
	*match = dom_string_caseless_lwc_isequal(atr_val, value);

	/* check for dashmatch */
	if (*match == false) {
		const char *vdata = lwc_string_data(value);
		const char *data = (const char *) dom_string_data(atr_val);
		size_t len = dom_string_byte_length(atr_val);

		if (len > vlen && data[vlen] == '-' &&
		    strncasecmp(data, vdata, vlen) == 0) {
				*match = true;
		}
	}

	dom_string_unref(atr_val);

	return CSS_OK;
}

/**
 * Callback to determine if a node has an attribute with the given name whose
 * value includes that given.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param qname  Name to match
 * \param value  Value to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_attribute_includes(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match)
{
	dom_node *n = node;
	dom_string *name;
	dom_string *atr_val;
	dom_exception err;
	size_t vlen = lwc_string_length(value);
	const char *p;
	const char *start;
	const char *end;

	if (vlen == 0) {
		*match = false;
		return CSS_OK;
	}

	err = dom_string_create_interned(
		(const uint8_t *) lwc_string_data(qname->name), 
		lwc_string_length(qname->name), &name);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	err = dom_element_get_attribute(n, name, &atr_val);
	if ((err != DOM_NO_ERR) || (atr_val == NULL)) {
		dom_string_unref(name);
		*match = false;
		return CSS_OK;
	}

	dom_string_unref(name);

	/* check for match */
	start = (const char *) dom_string_data(atr_val);
	end = start + dom_string_byte_length(atr_val);

	for (p = start; p <= end; p++) {
		if (*p == ' ' || *p == '\0') {
			if ((size_t) (p - start) == vlen &&
			    strncasecmp(start,
					lwc_string_data(value),
					vlen) == 0) {
				*match = true;
				break;
			}

			start = p + 1;
		}
	}

	dom_string_unref(atr_val);

	return CSS_OK;
}

/**
 * Callback to determine if a node has an attribute with the given name whose
 * value has the prefix given.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param qname  Name to match
 * \param value  Value to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_attribute_prefix(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match)
{
	dom_node *n = node;
	dom_string *name;
	dom_string *atr_val;
	dom_exception err;

	size_t vlen = lwc_string_length(value);

	if (vlen == 0) {
		*match = false;
		return CSS_OK;
	}

	err = dom_string_create_interned(
		(const uint8_t *) lwc_string_data(qname->name), 
		lwc_string_length(qname->name), &name);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	err = dom_element_get_attribute(n, name, &atr_val);
	if ((err != DOM_NO_ERR) || (atr_val == NULL)) {
		dom_string_unref(name);
		*match = false;
		return CSS_OK;
	}

	dom_string_unref(name);

	/* check for exact match */
	*match = dom_string_caseless_lwc_isequal(atr_val, value);

	/* check for prefix match */
	if (*match == false) {
		const char *data = (const char *) dom_string_data(atr_val);
		size_t len = dom_string_byte_length(atr_val);

		if ((len >= vlen) && 
		    (strncasecmp(data, lwc_string_data(value), vlen) == 0)) {
			*match = true;
		}
	}

	dom_string_unref(atr_val);

	return CSS_OK;
}

/**
 * Callback to determine if a node has an attribute with the given name whose
 * value has the suffix given.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param qname  Name to match
 * \param value  Value to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_attribute_suffix(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match)
{
	dom_node *n = node;
	dom_string *name;
	dom_string *atr_val;
	dom_exception err;

	size_t vlen = lwc_string_length(value);

	if (vlen == 0) {
		*match = false;
		return CSS_OK;
	}

	err = dom_string_create_interned(
		(const uint8_t *) lwc_string_data(qname->name), 
		lwc_string_length(qname->name), &name);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	err = dom_element_get_attribute(n, name, &atr_val);
	if ((err != DOM_NO_ERR) || (atr_val == NULL)) {
		dom_string_unref(name);
		*match = false;
		return CSS_OK;
	}

	dom_string_unref(name);

	/* check for exact match */
	*match = dom_string_caseless_lwc_isequal(atr_val, value);

	/* check for prefix match */
	if (*match == false) {
		const char *data = (const char *) dom_string_data(atr_val);
		size_t len = dom_string_byte_length(atr_val);
		
		const char *start = (char *) data + len - vlen;

		if ((len >= vlen) && 
		    (strncasecmp(start, lwc_string_data(value), vlen) == 0)) {
			*match = true;
		}


	}

	dom_string_unref(atr_val);

	return CSS_OK;
}

/**
 * Callback to determine if a node has an attribute with the given name whose
 * value contains the substring given.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param qname  Name to match
 * \param value  Value to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK on success,
 *         CSS_NOMEM on memory exhaustion.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_has_attribute_substring(void *pw, void *node,
		const css_qname *qname, lwc_string *value,
		bool *match)
{
	dom_node *n = node;
	dom_string *name;
	dom_string *atr_val;
	dom_exception err;

	size_t vlen = lwc_string_length(value);

	if (vlen == 0) {
		*match = false;
		return CSS_OK;
	}

	err = dom_string_create_interned(
		(const uint8_t *) lwc_string_data(qname->name), 
		lwc_string_length(qname->name), &name);
	if (err != DOM_NO_ERR)
		return CSS_NOMEM;

	err = dom_element_get_attribute(n, name, &atr_val);
	if ((err != DOM_NO_ERR) || (atr_val == NULL)) {
		dom_string_unref(name);
		*match = false;
		return CSS_OK;
	}

	dom_string_unref(name);

	/* check for exact match */
	*match = dom_string_caseless_lwc_isequal(atr_val, value);

	/* check for prefix match */
	if (*match == false) {
		const char *vdata = lwc_string_data(value);
		const char *start = (const char *) dom_string_data(atr_val);
		size_t len = dom_string_byte_length(atr_val);
		const char *last_start = start + len - vlen;

		if (len >= vlen) {
			while (start <= last_start) {
				if (strncasecmp(start, vdata, 
						vlen) == 0) {
					*match = true;
					break;
				}

				start++;
			}
		}
	}

	dom_string_unref(atr_val);

	return CSS_OK;
}

/**
 * Callback to determine if a node is the root node of the document.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_is_root(void *pw, void *node, bool *match)
{
	dom_node *n = node;
	dom_node *parent;
	dom_node_type type;
	dom_exception err;

	err = dom_node_get_parent_node(n, &parent);
	if (err != DOM_NO_ERR) {
		return CSS_NOMEM;
	}

	if (parent != NULL) {
		err = dom_node_get_node_type(parent, &type);

		dom_node_unref(parent);

		if (err != DOM_NO_ERR)
			return CSS_NOMEM;

		if (type != DOM_DOCUMENT_NODE) {
			*match = false;
			return CSS_OK;
		} 
	}
	
	*match = true;

	return CSS_OK;
}

static int 
node_count_siblings_check(dom_node *node, 
			  bool check_name, 
			  dom_string *name)
{
	dom_node_type type;
	int ret = 0;
	dom_exception exc;

	if (node == NULL) 
		return 0;

	exc = dom_node_get_node_type(node, &type);
	if ((exc != DOM_NO_ERR) || (type != DOM_ELEMENT_NODE)) {
		return 0;
	}
	
	if (check_name) {
		dom_string *node_name = NULL;
		exc = dom_node_get_node_name(node, &node_name);

		if ((exc == DOM_NO_ERR) && (node_name != NULL)) {

			if (dom_string_caseless_isequal(name, 
							node_name)) {
				ret = 1;
			}
			dom_string_unref(node_name);
		}
	} else {
		ret = 1;
	}
	
	return ret;
}
			
/**
 * Callback to count a node's siblings.
 *
 * \param pw         HTML document
 * \param node       DOM node
 * \param same_name  Only count siblings with the same name, or all
 * \param after      Count anteceding instead of preceding siblings
 * \param count      Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a count will contain the number of siblings
 */
css_error node_count_siblings(void *pw, void *n, bool same_name,
		bool after, int32_t *count)
{
	int32_t cnt = 0;
	dom_exception exc;
	dom_string *node_name = NULL;

	if (same_name) {
		dom_node *node = n;
		exc = dom_node_get_node_name(node, &node_name);
		if ((exc != DOM_NO_ERR) || (node_name == NULL)) {
			return CSS_NOMEM;
		}
	}
	
	if (after) {
		dom_node *node = dom_node_ref(n);
		dom_node *next;
		
		do {
			exc = dom_node_get_next_sibling(node, &next);
			if ((exc != DOM_NO_ERR))
				break;
			
			dom_node_unref(node);
			node = next;

			cnt += node_count_siblings_check(node, same_name, node_name);
		} while (node != NULL);
	} else {
		dom_node *node = dom_node_ref(n);
		dom_node *next;
		
		do {
			exc = dom_node_get_previous_sibling(node, &next);
			if ((exc != DOM_NO_ERR))
				break;
			
			dom_node_unref(node);
			node = next;

			cnt += node_count_siblings_check(node, same_name, node_name);

		} while (node != NULL);
	}

	if (node_name != NULL) {
		dom_string_unref(node_name);	
	}

	*count = cnt;
	return CSS_OK;
}

/**
 * Callback to determine if a node is empty.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node is empty and false otherwise.
 */
css_error node_is_empty(void *pw, void *node, bool *match)
{
	dom_node *n = node, *next;
	dom_exception err;
	
	*match = true;
	
	err = dom_node_get_first_child(n, &n);
	if (err != DOM_NO_ERR) {
		return CSS_BADPARM;
	}
	
	while (n != NULL) {
		dom_node_type ntype;
		err = dom_node_get_node_type(n, &ntype);
		if (err != DOM_NO_ERR) {
			dom_node_unref(n);
			return CSS_BADPARM;
		}
		
		if (ntype == DOM_ELEMENT_NODE ||
		    ntype == DOM_TEXT_NODE) {
			*match = false;
			dom_node_unref(n);
			break;
		}
		
		err = dom_node_get_next_sibling(n, &next);
		if (err != DOM_NO_ERR) {
			dom_node_unref(n);
			return CSS_BADPARM;
		}
		dom_node_unref(n);
		n = next;
	}
	
	return CSS_OK;
}

/**
 * Callback to determine if a node is a linking element.
 *
 * \param pw     HTML document
 * \param n      DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_is_link(void *pw, void *n, bool *match)
{
	dom_node *node = n;
	dom_exception exc;
	dom_string *node_name = NULL;

	exc = dom_node_get_node_name(node, &node_name);
	if ((exc != DOM_NO_ERR) || (node_name == NULL)) {
		return CSS_NOMEM;
	}

	if (dom_string_caseless_lwc_isequal(node_name, corestring_lwc_a)) {
		bool has_href;
		exc = dom_element_has_attribute(node, corestring_dom_href,
				&has_href); 
		if ((exc == DOM_NO_ERR) && (has_href)) {
			*match = true;
		} else {
			*match = false;
		}
	} else {
		*match = false;
	}
	dom_string_unref(node_name);

	return CSS_OK;
}

/**
 * Callback to determine if a node is a linking element whose target has been
 * visited.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_is_visited(void *pw, void *node, bool *match)
{
	*match = false;

	/** \todo Implement visted check in a more performant way */

#ifdef SUPPORT_VISITED
	nscss_select_ctx *ctx = pw;
	xmlNode *n = node;

	if (strcasecmp((const char *) n->name, "a") == 0) {
		nsurl *url;
		nserror error;
		const struct url_data *data;
		xmlChar *href = xmlGetProp(n, (const xmlChar *) "href");

		if (href == NULL)
			return CSS_OK;

		/* Make href absolute */
		/* TODO: this duplicates what we do for box->href */
		error = nsurl_join(ctx->base_url, (const char *)href, &url);

		xmlFree(href);
		if (error != NSERROR_OK) {
			return CSS_NOMEM;
		}

		data = urldb_get_url_data(nsurl_access(url));

		/* Visited if in the db and has
		 * non-zero visit count */
		if (data != NULL && data->visits > 0)
			*match = true;

		nsurl_unref(url);
	}
#endif

	return CSS_OK;
}

/**
 * Callback to determine if a node is currently being hovered over.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_is_hover(void *pw, void *node, bool *match)
{
	/** \todo Support hovering */

	*match = false;

	return CSS_OK;
}

/**
 * Callback to determine if a node is currently activated.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_is_active(void *pw, void *node, bool *match)
{
	/** \todo Support active nodes */

	*match = false;

	return CSS_OK;
}

/**
 * Callback to determine if a node has the input focus.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_is_focus(void *pw, void *node, bool *match)
{
	/** \todo Support focussed nodes */

	*match = false;

	return CSS_OK;
}

/**
 * Callback to determine if a node is enabled.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match with contain true if the node is enabled and false otherwise.
 */
css_error node_is_enabled(void *pw, void *node, bool *match)
{
	/** \todo Support enabled nodes */

	*match = false;

	return CSS_OK;
}

/**
 * Callback to determine if a node is disabled.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match with contain true if the node is disabled and false otherwise.
 */
css_error node_is_disabled(void *pw, void *node, bool *match)
{
	/** \todo Support disabled nodes */

	*match = false;

	return CSS_OK;
}

/**
 * Callback to determine if a node is checked.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match with contain true if the node is checked and false otherwise.
 */
css_error node_is_checked(void *pw, void *node, bool *match)
{
	/** \todo Support checked nodes */

	*match = false;

	return CSS_OK;
}

/**
 * Callback to determine if a node is the target of the document URL.
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match with contain true if the node matches and false otherwise.
 */
css_error node_is_target(void *pw, void *node, bool *match)
{
	/** \todo Support target */

	*match = false;

	return CSS_OK;
}

/**
 * Callback to determine if a node has the given language
 *
 * \param pw     HTML document
 * \param node   DOM node
 * \param lang   Language specifier to match
 * \param match  Pointer to location to receive result
 * \return CSS_OK.
 *
 * \post \a match will contain true if the node matches and false otherwise.
 */
css_error node_is_lang(void *pw, void *node,
		lwc_string *lang, bool *match)
{
	/** \todo Support languages */

	*match = false;

	return CSS_OK;
}

static css_error 
node_presentational_hint_vertical_align(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_string *name;
	dom_string *valign = NULL;
	dom_exception err;

	err = dom_node_get_node_name(node, &name);
	if (err != DOM_NO_ERR)
		return CSS_PROPERTY_NOT_SET;

	if (dom_string_caseless_lwc_isequal(name, corestring_lwc_col) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_thead) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_tbody) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_tfoot) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_tr) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_td) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_th)) {
		err = dom_element_get_attribute(node, 
				corestring_dom_valign, &valign);
		if (err != DOM_NO_ERR || valign == NULL) {
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		if (dom_string_caseless_lwc_isequal(valign,
				corestring_lwc_top)) {
			hint->status = CSS_VERTICAL_ALIGN_TOP;
		} else if (dom_string_caseless_lwc_isequal(valign,
					corestring_lwc_middle)) {
			hint->status = CSS_VERTICAL_ALIGN_MIDDLE;
		} else if (dom_string_caseless_lwc_isequal(valign,
					corestring_lwc_bottom)) {
			hint->status = CSS_VERTICAL_ALIGN_BOTTOM;
		} else if (dom_string_caseless_lwc_isequal(valign,
					corestring_lwc_baseline)) {
			hint->status = CSS_VERTICAL_ALIGN_BASELINE;
		} else {
			dom_string_unref(valign);
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		dom_string_unref(valign);
		dom_string_unref(name);

		return CSS_OK;
	} else if (dom_string_caseless_lwc_isequal(name,
				corestring_lwc_applet) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_embed) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_iframe) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_img) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_object)) {
		/** \todo input[type=image][align=*] - $11.3.3 */
		err = dom_element_get_attribute(node, 
				corestring_dom_align, &valign);
		if (err != DOM_NO_ERR || valign == NULL) {
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		if (dom_string_caseless_lwc_isequal(valign,
				corestring_lwc_top)) {
			hint->status = CSS_VERTICAL_ALIGN_TOP;
		} else if (dom_string_caseless_lwc_isequal(valign,
					corestring_lwc_bottom) ||
			   dom_string_caseless_lwc_isequal(valign,
					corestring_lwc_baseline)) {
			hint->status = CSS_VERTICAL_ALIGN_BASELINE;
		} else if (dom_string_caseless_lwc_isequal(valign,
					corestring_lwc_texttop)) {
			hint->status = CSS_VERTICAL_ALIGN_TEXT_TOP;
		} else if (dom_string_caseless_lwc_isequal(valign,
					corestring_lwc_absmiddle) ||
			   dom_string_caseless_lwc_isequal(valign,
					corestring_lwc_abscenter)) {
			hint->status = CSS_VERTICAL_ALIGN_MIDDLE;
		} else {
			dom_string_unref(valign);
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		dom_string_unref(valign);
		dom_string_unref(name);

		return CSS_OK;
	}

	dom_string_unref(name);
	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_text_align(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_string *name;
	dom_string *align = NULL;
	dom_exception err;

	err = dom_node_get_node_name(node, &name);
	if (err != DOM_NO_ERR)
		return CSS_PROPERTY_NOT_SET;

	if (dom_string_caseless_lwc_isequal(name, corestring_lwc_p) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_h1) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_h2) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_h3) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_h4) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_h5) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_h6)) {
		err = dom_element_get_attribute(node,
				corestring_dom_align, &align);
		if (err != DOM_NO_ERR || align == NULL) {
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		if (dom_string_caseless_lwc_isequal(align,
				corestring_lwc_left)) {
			hint->status = CSS_TEXT_ALIGN_LEFT;
		} else if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_center)) {
			hint->status = CSS_TEXT_ALIGN_CENTER;
		} else if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_right)) {
			hint->status = CSS_TEXT_ALIGN_RIGHT;
		} else if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_justify)) {
			hint->status = CSS_TEXT_ALIGN_JUSTIFY;
		} else {
			dom_string_unref(align);
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		dom_string_unref(align);
		dom_string_unref(name);

		return CSS_OK;
	} else if (dom_string_caseless_lwc_isequal(name,
				corestring_lwc_center)) {
		hint->status = CSS_TEXT_ALIGN_LIBCSS_CENTER;

		dom_string_unref(name);

		return CSS_OK;
	} else if (dom_string_caseless_lwc_isequal(name,
				corestring_lwc_caption)) {
		err = dom_element_get_attribute(node,
				corestring_dom_align, &align);
		if (err != DOM_NO_ERR) {
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		if (align == NULL || dom_string_caseless_lwc_isequal(align,
				corestring_lwc_center)) {
			hint->status = CSS_TEXT_ALIGN_LIBCSS_CENTER;
		} else if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_left)) {
			hint->status = CSS_TEXT_ALIGN_LIBCSS_LEFT;
		} else if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_right)) {
			hint->status = CSS_TEXT_ALIGN_LIBCSS_RIGHT;
		} else if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_justify)) {
			hint->status = CSS_TEXT_ALIGN_JUSTIFY;
		} else {
			dom_string_unref(align);
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		if (align != NULL)
			dom_string_unref(align);
		dom_string_unref(name);

		return CSS_OK;
	} else if (dom_string_caseless_lwc_isequal(name,
				corestring_lwc_div) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_thead) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_tbody) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_tfoot) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_tr) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_td) ||
		   dom_string_caseless_lwc_isequal(name,
		   		corestring_lwc_th)) {
		err = dom_element_get_attribute(node,
				corestring_dom_align, &align);
		if (err != DOM_NO_ERR || align == NULL) {
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_center)) {
			hint->status = CSS_TEXT_ALIGN_LIBCSS_CENTER;
		} else if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_left)) {
			hint->status = CSS_TEXT_ALIGN_LIBCSS_LEFT;
		} else if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_right)) {
			hint->status = CSS_TEXT_ALIGN_LIBCSS_RIGHT;
		} else if (dom_string_caseless_lwc_isequal(align,
					corestring_lwc_justify)) {
			hint->status = CSS_TEXT_ALIGN_JUSTIFY;
		} else {
			dom_string_unref(align);
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}

		dom_string_unref(align);
		dom_string_unref(name);

		return CSS_OK;
	} else if (dom_string_caseless_lwc_isequal(name,
			corestring_lwc_table)) {
		/* Tables usually reset alignment */
		hint->status = CSS_TEXT_ALIGN_INHERIT_IF_NON_MAGIC;

		dom_string_unref(name);

		return CSS_OK;
	} else {
		dom_string_unref(name);

		return CSS_PROPERTY_NOT_SET;
	}

}

static css_error 
node_presentational_hint_padding_trbl(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_string *name;
	dom_exception exc;
	dom_string *cellpadding = NULL;
	
	exc = dom_node_get_node_name(node, &name);
	if (exc != DOM_NO_ERR)
		return CSS_BADPARM;
	
	if (dom_string_caseless_lwc_isequal(name, corestring_lwc_td) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_th)) {
		css_qname qs;
		dom_node *tablenode = NULL;
		qs.ns = NULL;
		qs.name = lwc_string_ref(corestring_lwc_table);
		if (named_ancestor_node(ctx, node, &qs, 
					(void *)&tablenode) != CSS_OK) {
			/* Didn't find, or had error */
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}
		
		lwc_string_unref(qs.name);
		
		if (tablenode != NULL) {
			exc = dom_element_get_attribute(tablenode,
					corestring_dom_cellpadding,
					&cellpadding);
			if (exc != DOM_NO_ERR) {
				dom_string_unref(name);
				return CSS_BADPARM;
			}
		}
		/* No need to unref tablenode, named_ancestor_node does not
		 * return a reffed node to the CSS
		 */
	}
	
	dom_string_unref(name);
	
	if (cellpadding == NULL)
		return CSS_PROPERTY_NOT_SET;

	if (parse_dimension(dom_string_data(cellpadding), false,
			    &hint->data.length.value,
			    &hint->data.length.unit)) {
		hint->status = CSS_PADDING_SET;
	} else {
		dom_string_unref(cellpadding);
		return CSS_PROPERTY_NOT_SET;
	}
	
	return CSS_OK;
}

static css_error 
node_presentational_hint_margin_rl(nscss_select_ctx *ctx, 
				   dom_node *node, 
				   css_hint *hint,
				   uint32_t property)
{
	dom_string *n;
	dom_exception exc;
	
	exc = dom_node_get_node_name(node, &n);
	if (exc != DOM_NO_ERR)
		return CSS_BADPARM;
	
	if (dom_string_caseless_lwc_isequal(n, corestring_lwc_img) ||
	    dom_string_caseless_lwc_isequal(n, corestring_lwc_applet)) {
		dom_string_unref(n);
		exc = dom_element_get_attribute(node,
				corestring_dom_hspace, &n);
		if (exc != DOM_NO_ERR) {
			return CSS_BADPARM;
		}
		
		if (n == NULL)
			return CSS_PROPERTY_NOT_SET;
		
		if (parse_dimension(dom_string_data(n), false,
				    &hint->data.length.value,
				    &hint->data.length.unit)) {
			hint->status = CSS_MARGIN_SET;
		} else {
			dom_string_unref(n);
			return CSS_PROPERTY_NOT_SET;
		}
		dom_string_unref(n);
		return CSS_OK;
	} else if (dom_string_caseless_lwc_isequal(n, corestring_lwc_table) ||
		   dom_string_caseless_lwc_isequal(n, corestring_lwc_align)) {
		dom_string_unref(n);
		exc = dom_element_get_attribute(node,
				corestring_dom_align, &n);
		if (exc != DOM_NO_ERR) {
			return CSS_BADPARM;
		}
		
		if (n == NULL)
			return CSS_PROPERTY_NOT_SET;
		
		if (dom_string_caseless_lwc_isequal(n,
				corestring_lwc_center) ||
		    dom_string_caseless_lwc_isequal(n,
		    		corestring_lwc_abscenter) ||
		    dom_string_caseless_lwc_isequal(n,
		    		corestring_lwc_middle) ||
		    dom_string_caseless_lwc_isequal(n,
		    		corestring_lwc_absmiddle)) {
			hint->status = CSS_MARGIN_AUTO;
		} else {
			dom_string_unref(n);
			return CSS_PROPERTY_NOT_SET;
		}
		
		dom_string_unref(n);
		return CSS_OK;
	} else if (dom_string_caseless_lwc_isequal(n, corestring_lwc_hr)) {
		dom_string_unref(n);
		exc = dom_element_get_attribute(node,
				corestring_dom_align, &n);
		if (exc != DOM_NO_ERR)
			return CSS_BADPARM;
		
		if (n == NULL)
			return CSS_PROPERTY_NOT_SET;
		
		if (dom_string_caseless_lwc_isequal(n,
				corestring_lwc_left)) {
			if (property == CSS_PROP_MARGIN_LEFT) {
				hint->data.length.value = 0;
				hint->data.length.unit = CSS_UNIT_PX;
				hint->status = CSS_MARGIN_SET;
			} else {
				hint->status = CSS_MARGIN_AUTO;
			}
		} else if (dom_string_caseless_lwc_isequal(n,
				corestring_lwc_center)) {
			hint->status = CSS_MARGIN_AUTO;
		} else if (dom_string_caseless_lwc_isequal(n,
				corestring_lwc_right)) {
			if (property == CSS_PROP_MARGIN_RIGHT) {
				hint->data.length.value = 0;
				hint->data.length.unit = CSS_UNIT_PX;
				hint->status = CSS_MARGIN_SET;
			} else {
				hint->status = CSS_MARGIN_AUTO;
			}
		} else {
			dom_string_unref(n);
			return CSS_PROPERTY_NOT_SET;
		}
		dom_string_unref(n);
		
		return CSS_OK;
	}
	
	dom_string_unref(n);
	
	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_margin_tb(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_string *name, *vspace = NULL;
	dom_exception exc;
	
	exc = dom_node_get_node_name(node, &name);
	if (exc != DOM_NO_ERR)
		return CSS_BADPARM;
	
	if (dom_string_caseless_lwc_isequal(name, corestring_lwc_img) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_applet)) {
		exc = dom_element_get_attribute(node, corestring_dom_vspace,
				&vspace);
		if (exc != DOM_NO_ERR) {
			dom_string_unref(name);
			return CSS_BADPARM;
		}
	}
	
	dom_string_unref(name);
	
	if (vspace == NULL)
		return CSS_PROPERTY_NOT_SET;
	
	if (parse_dimension(dom_string_data(vspace), false,
			    &hint->data.length.value,
			    &hint->data.length.unit)) {
		hint->status = CSS_MARGIN_SET;
	} else {
		dom_string_unref(vspace);
		return CSS_PROPERTY_NOT_SET;
	}

	dom_string_unref(vspace);

	return CSS_OK;
}

static css_error 
node_presentational_hint_border_trbl_width(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_string *name;
	dom_exception exc;
	dom_string *width = NULL;
	bool is_table_cell = false;
	
	exc = dom_node_get_node_name(node, &name);
	if (exc != DOM_NO_ERR)
		return CSS_BADPARM;
	
	if (dom_string_caseless_lwc_isequal(name, corestring_lwc_td) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_th)) {
		css_qname qs;
		dom_node *tablenode = NULL;
		qs.ns = NULL;
		qs.name = lwc_string_ref(corestring_lwc_table);
		if (named_ancestor_node(ctx, node, &qs, 
					(void *)&tablenode) != CSS_OK) {
			/* Didn't find, or had error */
			lwc_string_unref(qs.name);
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}
		
		lwc_string_unref(qs.name);
		if (tablenode != NULL) {
			exc = dom_element_get_attribute(tablenode,
					corestring_dom_border, &width);
			if (exc != DOM_NO_ERR) {
				dom_string_unref(name);
				return CSS_BADPARM;
			}
		}
		/* No need to unref tablenode, named_ancestor_node does not
		 * return a reffed node to the CSS
		 */
		is_table_cell = true;
	} else if (dom_string_caseless_lwc_isequal(name,
				corestring_lwc_table)) {
		exc = dom_element_get_attribute(node, corestring_dom_border,
				&width);
		if (exc != DOM_NO_ERR) {
			dom_string_unref(name);
			return CSS_BADPARM;
		}
	}
	
	dom_string_unref(name);
	
	if (width == NULL)
		return CSS_PROPERTY_NOT_SET;

	if (parse_dimension(dom_string_data(width), false,
			    &hint->data.length.value,
			    &hint->data.length.unit)) {
		if (is_table_cell &&
		    INTTOFIX(0) !=
		    hint->data.length.value) {
			hint->data.length.value = INTTOFIX(1);
			hint->data.length.unit = CSS_UNIT_PX;
		}
		hint->status = CSS_BORDER_WIDTH_WIDTH;
	} else {
		dom_string_unref(width);
		return CSS_PROPERTY_NOT_SET;
	}

	dom_string_unref(width);

	return CSS_OK;
}

static css_error 
node_presentational_hint_border_trbl_style(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_string *name;
	dom_exception exc;

	exc = dom_node_get_node_name(node, &name);
	if (exc != DOM_NO_ERR)
		return CSS_BADPARM;

	if (dom_string_caseless_lwc_isequal(name, corestring_lwc_td) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_th)) {
		css_qname qs;
		dom_node *tablenode = NULL;
		qs.ns = NULL;
		qs.name = lwc_string_ref(corestring_lwc_table);

		if (named_ancestor_node(ctx, node, &qs, 
					(void *)&tablenode) != CSS_OK) {
			/* Didn't find, or had error */
			lwc_string_unref(qs.name);
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}
		
		lwc_string_unref(qs.name);

		if (tablenode != NULL) {
			bool has_border = false;

			exc = dom_element_has_attribute(tablenode,
							corestring_dom_border,
							&has_border);
			if (exc != DOM_NO_ERR) {
				dom_string_unref(name);
				return CSS_BADPARM;
			}

			if (has_border) {
				hint->status = CSS_BORDER_STYLE_INSET;
				dom_string_unref(name);
				return CSS_OK;
			}
		}
		/* No need to unref tablenode, named_ancestor_node does not
		 * return a reffed node to the CSS
		 */
	} else if (dom_string_caseless_lwc_isequal(name,
			corestring_lwc_table)) {
		bool has_border = false;

		exc = dom_element_has_attribute(node,
						corestring_dom_border,
						&has_border);
		if (exc != DOM_NO_ERR) {
			dom_string_unref(name);
			return CSS_BADPARM;
		}

		if (has_border) {
			hint->status = CSS_BORDER_STYLE_OUTSET;
			dom_string_unref(name);
			return CSS_OK;
		}
	}

	dom_string_unref(name);

	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_border_trbl_color(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_string *name;
	dom_string *bordercolor = NULL;
	dom_exception err;

	err = dom_node_get_node_name(node, &name);
	if (err != DOM_NO_ERR)
		return CSS_PROPERTY_NOT_SET;

	if (dom_string_caseless_lwc_isequal(name, corestring_lwc_td) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_th)) {
		css_qname qs;
		dom_node *tablenode = NULL;
		qs.ns = NULL;
		qs.name = lwc_string_ref(corestring_lwc_table);

		if (named_ancestor_node(ctx, node, &qs, 
					(void *)&tablenode) != CSS_OK) {
			/* Didn't find, or had error */
			lwc_string_unref(qs.name);
			dom_string_unref(name);
			return CSS_PROPERTY_NOT_SET;
		}
		
		lwc_string_unref(qs.name);

		if (tablenode != NULL) {
			err = dom_element_get_attribute(node, 
					corestring_dom_bordercolor, 
					&bordercolor);
		}
		/* No need to unref tablenode, named_ancestor_node does not
		 * return a reffed node to the CSS
		 */

	} else if (dom_string_caseless_lwc_isequal(name,
			corestring_lwc_table)) {
		err = dom_element_get_attribute(node, 
				corestring_dom_bordercolor, 
				&bordercolor);
	}

	dom_string_unref(name);

	if ((err != DOM_NO_ERR) || (bordercolor == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	if (nscss_parse_colour((const char *)dom_string_data(bordercolor), 
			       &hint->data.color)) {
		hint->status = CSS_BORDER_COLOR_COLOR;
		dom_string_unref(bordercolor);
		return CSS_OK;
	}

	dom_string_unref(bordercolor);
	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_border_spacing(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_exception err;
	dom_string *node_name = NULL;
	dom_string *cellspacing = NULL;

	err = dom_node_get_node_name(node, &node_name);
	if ((err != DOM_NO_ERR) || (node_name == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	if (!dom_string_caseless_lwc_isequal(node_name,
			corestring_lwc_table)) {
		dom_string_unref(node_name);
		return CSS_PROPERTY_NOT_SET;
	}

	dom_string_unref(node_name);

	err = dom_element_get_attribute(node,
			corestring_dom_cellspacing, &cellspacing);
	if ((err != DOM_NO_ERR) || (cellspacing == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}


	if (parse_dimension((const char *)dom_string_data(cellspacing), 
			    false,
			    &hint->data.position.h.value,
			    &hint->data.position.h.unit)) {

		hint->data.position.v = hint->data.position.h;
		hint->status = CSS_BORDER_SPACING_SET;

		dom_string_unref(cellspacing);
		return CSS_OK;
	}

	dom_string_unref(cellspacing);
	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_width(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_string *name;
	dom_string *width = NULL;
	dom_exception err;
	bool textarea = false;
	bool input = false;

	err = dom_node_get_node_name(node, &name);
	if (err != DOM_NO_ERR)
		return CSS_PROPERTY_NOT_SET;

	if (dom_string_caseless_lwc_isequal(name, corestring_lwc_hr) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_iframe) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_img) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_object) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_table) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_td) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_th) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_applet)) {
		err = dom_element_get_attribute(node, 
				corestring_dom_width, &width);
	} else if (dom_string_caseless_lwc_isequal(name,
				corestring_lwc_textarea)) {
		textarea = true;
		err = dom_element_get_attribute(node, 
				corestring_dom_cols, &width);
	} else if (dom_string_caseless_lwc_isequal(name,
			corestring_lwc_input)) {
		input = true;
		err = dom_element_get_attribute(node, 
				corestring_dom_size, &width);
	}

	dom_string_unref(name);

	if ((err != DOM_NO_ERR) || (width == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	if (parse_dimension((const char *)dom_string_data(width), 
			    false,
			    &hint->data.length.value,
			    &hint->data.length.unit)) {
		hint->status = CSS_WIDTH_SET;
		dom_string_unref(width);

		if (textarea) {
			hint->data.length.unit = CSS_UNIT_EX;
		}

		if (input) {
			err = dom_element_get_attribute(node,
					corestring_dom_type, &width);
			if ((err != DOM_NO_ERR) || (width == NULL)) {
				return CSS_PROPERTY_NOT_SET;
			}

			if (dom_string_lwc_isequal(width,
					corestring_lwc_text) ||
			    dom_string_lwc_isequal(width,
			    		corestring_lwc_password)) {
				hint->data.length.unit = CSS_UNIT_EX;

			}
			dom_string_unref(width);


		}

		return CSS_OK;
	}

	dom_string_unref(width);
	return CSS_PROPERTY_NOT_SET;

}

static css_error 
node_presentational_hint_height(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_string *name;
	dom_string *height = NULL;
	dom_exception err;
	bool textarea = false;

	err = dom_node_get_node_name(node, &name);
	if (err != DOM_NO_ERR)
		return CSS_PROPERTY_NOT_SET;

	if (dom_string_caseless_lwc_isequal(name, corestring_lwc_iframe) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_td) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_th) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_tr) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_img) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_object) ||
	    dom_string_caseless_lwc_isequal(name, corestring_lwc_applet)) {
		err = dom_element_get_attribute(node, 
				corestring_dom_height, &height);
	} else if (dom_string_caseless_lwc_isequal(name,
			corestring_lwc_textarea)) {
		textarea = true;
		err = dom_element_get_attribute(node, 
				corestring_dom_rows, &height);
	}

	dom_string_unref(name);

	if ((err != DOM_NO_ERR) || (height == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	if (parse_dimension((const char *)dom_string_data(height), 
			    false,
			    &hint->data.length.value,
			    &hint->data.length.unit)) {
		hint->status = CSS_HEIGHT_SET;

		if (textarea) {
			hint->data.length.unit = CSS_UNIT_EM;
		}

		dom_string_unref(height);
		return CSS_OK;
	}

	dom_string_unref(height);
	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_font_size(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_exception err;
	dom_string *node_name = NULL;
	dom_string *size;

	err = dom_node_get_node_name(node, &node_name);
	if ((err != DOM_NO_ERR) || (node_name == NULL)) {
		return CSS_NOMEM;
	}

	if (!dom_string_caseless_lwc_isequal(node_name,
			corestring_lwc_font)) {
		dom_string_unref(node_name);
		return CSS_PROPERTY_NOT_SET;
	}

	dom_string_unref(node_name);

	err = dom_element_get_attribute(node, corestring_dom_align, &size);
	if ((err != DOM_NO_ERR) || (size == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	if (parse_font_size((const char *)dom_string_data(size), 
			    &hint->status,
			    &hint->data.length.value,
			    &hint->data.length.unit)) {
		dom_string_unref(size);
		return CSS_OK;
	}

	dom_string_unref(size);
	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_float(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_exception err;
	dom_string *node_name = NULL;
	dom_string *align;

	err = dom_node_get_node_name(node, &node_name);
	if ((err != DOM_NO_ERR) || (node_name == NULL)) {
		return CSS_NOMEM;
	}

	/** \todo input[type=image][align=*] - $11.3.3 */
	if (!dom_string_caseless_lwc_isequal(node_name,
			corestring_lwc_applet) &&
	    !dom_string_caseless_lwc_isequal(node_name,
	    		corestring_lwc_embed) &&
	    !dom_string_caseless_lwc_isequal(node_name,
	    		corestring_lwc_iframe) &&
	    !dom_string_caseless_lwc_isequal(node_name,
	    		corestring_lwc_img) &&
	    !dom_string_caseless_lwc_isequal(node_name,
	    		corestring_lwc_object)) {
		dom_string_unref(node_name);
		return CSS_PROPERTY_NOT_SET;
	}

	dom_string_unref(node_name);

	err = dom_element_get_attribute(node, corestring_dom_align, &align);
	if ((err != DOM_NO_ERR) || (align == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	if (dom_string_lwc_isequal(align, corestring_lwc_left)) {
		hint->status = CSS_FLOAT_LEFT;
		dom_string_unref(align);
		return CSS_OK;
	} else if (dom_string_lwc_isequal(align, corestring_lwc_right)) {
		hint->status = CSS_FLOAT_RIGHT;
		dom_string_unref(align);
		return CSS_OK;
	}

	dom_string_unref(align);

	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_color(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	css_error error;
	dom_exception err;
	dom_string *node_name = NULL;
	dom_string *color;

	err = dom_node_get_node_name(node, &node_name);
	if ((err != DOM_NO_ERR) || (node_name == NULL)) {
		return CSS_NOMEM;
	}

	if (dom_string_caseless_lwc_isequal(node_name, corestring_lwc_a)) {
		/* find body node */
		css_qname qs;
		dom_node *bodynode = NULL;
		bool is_visited;

		qs.ns = NULL;
		qs.name = lwc_string_ref(corestring_lwc_body);
		if (named_ancestor_node(ctx, node, &qs, 
					(void *)&bodynode) != CSS_OK) {
			/* Didn't find, or had error */
			lwc_string_unref(qs.name);
			dom_string_unref(node_name);
			return CSS_PROPERTY_NOT_SET;
		}
		
		lwc_string_unref(qs.name);

		/* deal with missing body ancestor */
		if (bodynode == NULL) {
			dom_string_unref(node_name);
			return CSS_BADPARM;
		}

		error = node_is_visited(ctx, node, &is_visited);
		if (error != CSS_OK)
			return error;

		if (is_visited) {
			err = dom_element_get_attribute(node,
					corestring_dom_vlink, &color);
			if ((err != DOM_NO_ERR) || (color == NULL)) {
				dom_string_unref(node_name);
				return CSS_PROPERTY_NOT_SET;
			}
		} else {
			err = dom_element_get_attribute(node,
					corestring_dom_link, &color);
			if ((err != DOM_NO_ERR) || (color == NULL)) {
				dom_string_unref(node_name);
				return CSS_PROPERTY_NOT_SET;
			}
		}
	} else if (dom_string_caseless_lwc_isequal(node_name,
			corestring_lwc_body)) {
		err = dom_element_get_attribute(node,
				corestring_dom_text, &color);
		if ((err != DOM_NO_ERR) || (color == NULL)) {
			dom_string_unref(node_name);
			return CSS_PROPERTY_NOT_SET;
		}
	} else {
		err = dom_element_get_attribute(node,
				corestring_dom_color, &color);
		if ((err != DOM_NO_ERR) || (color == NULL)) {
			dom_string_unref(node_name);
			return CSS_PROPERTY_NOT_SET;
		}
	}

	if (!nscss_parse_colour((const char *)dom_string_data(color), 
				&hint->data.color)) {
		dom_string_unref(node_name);
		return CSS_PROPERTY_NOT_SET;
	}

	hint->status = CSS_COLOR_COLOR;

	dom_string_unref(node_name);

	return CSS_OK;
}

static css_error 
node_presentational_hint_caption_side(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_exception err;
	dom_string *node_name = NULL;
	dom_string *align = NULL;

	err = dom_node_get_node_name(node, &node_name);
	if ((err != DOM_NO_ERR) || (node_name == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	if (!dom_string_caseless_lwc_isequal(node_name,
			corestring_lwc_caption)) {
		dom_string_unref(node_name);
		return CSS_PROPERTY_NOT_SET;
	}

	dom_string_unref(node_name);

	err = dom_element_get_attribute(node, corestring_dom_align, &align);
	if ((err != DOM_NO_ERR) || (align == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	if (dom_string_caseless_lwc_isequal(align, corestring_lwc_bottom)) {
		hint->status = CSS_CAPTION_SIDE_BOTTOM;
		dom_string_unref(align);
		return CSS_OK;
	}

	dom_string_unref(align);

	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_background_color(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_exception err;
	dom_string *bgcolor;

	err = dom_element_get_attribute(node,
			corestring_dom_bgcolor, &bgcolor);
	if ((err != DOM_NO_ERR) || (bgcolor == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	if (nscss_parse_colour((const char *)dom_string_data(bgcolor),
			       &hint->data.color)) {
		hint->status = CSS_BACKGROUND_COLOR_COLOR;
		dom_string_unref(bgcolor);
		return CSS_OK;
	}

	dom_string_unref(bgcolor);

	return CSS_PROPERTY_NOT_SET;
}

static css_error 
node_presentational_hint_background_image(nscss_select_ctx *ctx, 
					  dom_node *node, 
					  css_hint *hint)
{
	dom_exception err;
	dom_string *atr_val;
	nserror error;
	nsurl *url;
	lwc_string *iurl;
	lwc_error lerror;

	err = dom_element_get_attribute(node,
			corestring_dom_background, &atr_val);
	if ((err != DOM_NO_ERR) || (atr_val == NULL)) {
		return CSS_PROPERTY_NOT_SET;
	}

	error = nsurl_join(ctx->base_url,
			(const char *)dom_string_data(atr_val), &url);

	dom_string_unref(atr_val);

	if (error != NSERROR_OK) {
		return CSS_NOMEM;
	} 

	lerror = lwc_intern_string(nsurl_access(url),
			nsurl_length(url), &iurl);

	nsurl_unref(url);

	if (lerror == lwc_error_oom) {
		return CSS_NOMEM;
	} 

	if (lerror == lwc_error_ok) {
		hint->data.string = iurl;
		hint->status = CSS_BACKGROUND_IMAGE_IMAGE;
		return CSS_OK;
	}
	
	return CSS_PROPERTY_NOT_SET;
}

/**
 * Callback to retrieve presentational hints for a node
 *
 * \param pw        HTML document
 * \param node      DOM node
 * \param property  CSS property to retrieve
 * \param hint      Pointer to hint object to populate
 * \return CSS_OK               on success,
 *         CSS_PROPERTY_NOT_SET if there is no hint for the requested property,
 *         CSS_NOMEM            on memory exhaustion.
 */
css_error node_presentational_hint(void *pw, void *node,
		uint32_t property, css_hint *hint)
{

	switch (property) {
	case CSS_PROP_BACKGROUND_IMAGE:
		return node_presentational_hint_background_image(pw, node, hint);

	case CSS_PROP_BACKGROUND_COLOR:
		return node_presentational_hint_background_color(pw, node, hint);
	case CSS_PROP_CAPTION_SIDE:
		return node_presentational_hint_caption_side(pw, node, hint);

	case CSS_PROP_COLOR:
		return node_presentational_hint_color(pw, node, hint);

	case CSS_PROP_FLOAT:
		return node_presentational_hint_float(pw, node, hint);

	case CSS_PROP_FONT_SIZE:
		return node_presentational_hint_font_size(pw, node, hint);

	case CSS_PROP_HEIGHT:
		return node_presentational_hint_height(pw, node, hint);

	case CSS_PROP_WIDTH:
		return node_presentational_hint_width(pw, node, hint);

	case CSS_PROP_BORDER_SPACING:
		return node_presentational_hint_border_spacing(pw, node, hint);

	case CSS_PROP_BORDER_TOP_COLOR :
	case CSS_PROP_BORDER_RIGHT_COLOR :
	case CSS_PROP_BORDER_BOTTOM_COLOR :
	case CSS_PROP_BORDER_LEFT_COLOR :
		return node_presentational_hint_border_trbl_color(pw, node, hint);

	case CSS_PROP_BORDER_TOP_STYLE :
	case CSS_PROP_BORDER_RIGHT_STYLE :
	case CSS_PROP_BORDER_BOTTOM_STYLE :
	case CSS_PROP_BORDER_LEFT_STYLE :
		return node_presentational_hint_border_trbl_style(pw, node, hint);

	case CSS_PROP_BORDER_TOP_WIDTH :
	case CSS_PROP_BORDER_RIGHT_WIDTH :
	case CSS_PROP_BORDER_BOTTOM_WIDTH :
	case CSS_PROP_BORDER_LEFT_WIDTH :
		return node_presentational_hint_border_trbl_width(pw, node, hint);

	case CSS_PROP_MARGIN_TOP :
	case CSS_PROP_MARGIN_BOTTOM :
		return node_presentational_hint_margin_tb(pw, node, hint);

	case CSS_PROP_MARGIN_RIGHT:
	case CSS_PROP_MARGIN_LEFT:
		return node_presentational_hint_margin_rl(pw, node, hint, property);

	case CSS_PROP_PADDING_TOP:
	case CSS_PROP_PADDING_RIGHT :
	case CSS_PROP_PADDING_BOTTOM :
	case CSS_PROP_PADDING_LEFT:
		return node_presentational_hint_padding_trbl(pw, node, hint);

	case CSS_PROP_TEXT_ALIGN:
		return node_presentational_hint_text_align(pw, node, hint);

	case CSS_PROP_VERTICAL_ALIGN:
		return node_presentational_hint_vertical_align(pw, node, hint);
	}

	return CSS_PROPERTY_NOT_SET;
}

/**
 * Callback to retrieve the User-Agent defaults for a CSS property.
 *
 * \param pw        HTML document
 * \param property  Property to retrieve defaults for
 * \param hint      Pointer to hint object to populate
 * \return CSS_OK       on success,
 *         CSS_INVALID  if the property should not have a user-agent default.
 */
css_error ua_default_for_property(void *pw, uint32_t property, css_hint *hint)
{
	if (property == CSS_PROP_COLOR) {
		hint->data.color = 0xff000000;
		hint->status = CSS_COLOR_COLOR;
	} else if (property == CSS_PROP_FONT_FAMILY) {
		hint->data.strings = NULL;
		switch (nsoption_int(font_default)) {
		case PLOT_FONT_FAMILY_SANS_SERIF:
			hint->status = CSS_FONT_FAMILY_SANS_SERIF;
			break;
		case PLOT_FONT_FAMILY_SERIF:
			hint->status = CSS_FONT_FAMILY_SERIF;
			break;
		case PLOT_FONT_FAMILY_MONOSPACE:
			hint->status = CSS_FONT_FAMILY_MONOSPACE;
			break;
		case PLOT_FONT_FAMILY_CURSIVE:
			hint->status = CSS_FONT_FAMILY_CURSIVE;
			break;
		case PLOT_FONT_FAMILY_FANTASY:
			hint->status = CSS_FONT_FAMILY_FANTASY;
			break;
		}
	} else if (property == CSS_PROP_QUOTES) {
		/** \todo Not exactly useful :) */
		hint->data.strings = NULL;
		hint->status = CSS_QUOTES_NONE;
	} else if (property == CSS_PROP_VOICE_FAMILY) {
		/** \todo Fix this when we have voice-family done */
		hint->data.strings = NULL;
		hint->status = 0;
	} else {
		return CSS_INVALID;
	}

	return CSS_OK;
}

/**
 * Mapping of colour name to CSS color
 */
struct colour_map {
	const char *name;
	css_color color;
};

/**
 * Name comparator for named colour matching
 *
 * \param a  Name to match
 * \param b  Colour map entry to consider
 * \return 0   on match,
 *         < 0 if a < b,
 *         > 0 if b > a.
 */
int cmp_colour_name(const void *a, const void *b)
{
	const char *aa = a;
	const struct colour_map *bb = b;

	return strcasecmp(aa, bb->name);
}

/**
 * Parse a named colour
 *
 * \param name    Name to parse
 * \param result  Pointer to location to receive css_color
 * \return true on success, false on invalid input
 */
bool parse_named_colour(const char *name, css_color *result)
{
	static const struct colour_map named_colours[] = {
		{ "aliceblue",		0xfff0f8ff },
		{ "antiquewhite",	0xfffaebd7 },
		{ "aqua",		0xff00ffff },
		{ "aquamarine",		0xff7fffd4 },
		{ "azure",		0xfff0ffff },
		{ "beige",		0xfff5f5dc },
		{ "bisque",		0xffffe4c4 },
		{ "black",		0xff000000 },
		{ "blanchedalmond",	0xffffebcd },
		{ "blue",		0xff0000ff },
		{ "blueviolet",		0xff8a2be2 },
		{ "brown",		0xffa52a2a },
		{ "burlywood",		0xffdeb887 },
		{ "cadetblue",		0xff5f9ea0 },
		{ "chartreuse",		0xff7fff00 },
		{ "chocolate",		0xffd2691e },
		{ "coral",		0xffff7f50 },
		{ "cornflowerblue",	0xff6495ed },
		{ "cornsilk",		0xfffff8dc },
		{ "crimson",		0xffdc143c },
		{ "cyan",		0xff00ffff },
		{ "darkblue",		0xff00008b },
		{ "darkcyan",		0xff008b8b },
		{ "darkgoldenrod",	0xffb8860b },
		{ "darkgray",		0xffa9a9a9 },
		{ "darkgreen",		0xff006400 },
		{ "darkgrey",		0xffa9a9a9 },
		{ "darkkhaki",		0xffbdb76b },
		{ "darkmagenta",	0xff8b008b },
		{ "darkolivegreen",	0xff556b2f },
		{ "darkorange",		0xffff8c00 },
		{ "darkorchid",		0xff9932cc },
		{ "darkred",		0xff8b0000 },
		{ "darksalmon",		0xffe9967a },
		{ "darkseagreen",	0xff8fbc8f },
		{ "darkslateblue",	0xff483d8b },
		{ "darkslategray",	0xff2f4f4f },
		{ "darkslategrey",	0xff2f4f4f },
		{ "darkturquoise",	0xff00ced1 },
		{ "darkviolet",		0xff9400d3 },
		{ "deeppink",		0xffff1493 },
		{ "deepskyblue",	0xff00bfff },
		{ "dimgray",		0xff696969 },
		{ "dimgrey",		0xff696969 },
		{ "dodgerblue",		0xff1e90ff },
		{ "feldspar",		0xffd19275 },
		{ "firebrick",		0xffb22222 },
		{ "floralwhite",	0xfffffaf0 },
		{ "forestgreen",	0xff228b22 },
		{ "fuchsia",		0xffff00ff },
		{ "gainsboro",		0xffdcdcdc },
		{ "ghostwhite",		0xfff8f8ff },
		{ "gold",		0xffffd700 },
		{ "goldenrod",		0xffdaa520 },
		{ "gray",		0xff808080 },
		{ "green",		0xff008000 },
		{ "greenyellow",	0xffadff2f },
		{ "grey",		0xff808080 },
		{ "honeydew",		0xfff0fff0 },
		{ "hotpink",		0xffff69b4 },
		{ "indianred",		0xffcd5c5c },
		{ "indigo",		0xff4b0082 },
		{ "ivory",		0xfffffff0 },
		{ "khaki",		0xfff0e68c },
		{ "lavender",		0xffe6e6fa },
		{ "lavenderblush",	0xfffff0f5 },
		{ "lawngreen",		0xff7cfc00 },
		{ "lemonchiffon",	0xfffffacd },
		{ "lightblue",		0xffadd8e6 },
		{ "lightcoral",		0xfff08080 },
		{ "lightcyan",		0xffe0ffff },
		{ "lightgoldenrodyellow",	0xfffafad2 },
		{ "lightgray",		0xffd3d3d3 },
		{ "lightgreen",		0xff90ee90 },
		{ "lightgrey",		0xffd3d3d3 },
		{ "lightpink",		0xffffb6c1 },
		{ "lightsalmon",	0xffffa07a },
		{ "lightseagreen",	0xff20b2aa },
		{ "lightskyblue",	0xff87cefa },
		{ "lightslateblue",	0xff8470ff },
		{ "lightslategray",	0xff778899 },
		{ "lightslategrey",	0xff778899 },
		{ "lightsteelblue",	0xffb0c4de },
		{ "lightyellow",	0xffffffe0 },
		{ "lime",		0xff00ff00 },
		{ "limegreen",		0xff32cd32 },
		{ "linen",		0xfffaf0e6 },
		{ "magenta",		0xffff00ff },
		{ "maroon",		0xff800000 },
		{ "mediumaquamarine",	0xff66cdaa },
		{ "mediumblue",		0xff0000cd },
		{ "mediumorchid",	0xffba55d3 },
		{ "mediumpurple",	0xff9370db },
		{ "mediumseagreen",	0xff3cb371 },
		{ "mediumslateblue",	0xff7b68ee },
		{ "mediumspringgreen",	0xff00fa9a },
		{ "mediumturquoise",	0xff48d1cc },
		{ "mediumvioletred",	0xffc71585 },
		{ "midnightblue",	0xff191970 },
		{ "mintcream",		0xfff5fffa },
		{ "mistyrose",		0xffffe4e1 },
		{ "moccasin",		0xffffe4b5 },
		{ "navajowhite",	0xffffdead },
		{ "navy",		0xff000080 },
		{ "oldlace",		0xfffdf5e6 },
		{ "olive",		0xff808000 },
		{ "olivedrab",		0xff6b8e23 },
		{ "orange",		0xffffa500 },
		{ "orangered",		0xffff4500 },
		{ "orchid",		0xffda70d6 },
		{ "palegoldenrod",	0xffeee8aa },
		{ "palegreen",		0xff98fb98 },
		{ "paleturquoise",	0xffafeeee },
		{ "palevioletred",	0xffdb7093 },
		{ "papayawhip",		0xffffefd5 },
		{ "peachpuff",		0xffffdab9 },
		{ "peru",		0xffcd853f },
		{ "pink",		0xffffc0cb },
		{ "plum",		0xffdda0dd },
		{ "powderblue",		0xffb0e0e6 },
		{ "purple",		0xff800080 },
		{ "red",		0xffff0000 },
		{ "rosybrown",		0xffbc8f8f },
		{ "royalblue",		0xff4169e1 },
		{ "saddlebrown",	0xff8b4513 },
		{ "salmon",		0xfffa8072 },
		{ "sandybrown",		0xfff4a460 },
		{ "seagreen",		0xff2e8b57 },
		{ "seashell",		0xfffff5ee },
		{ "sienna",		0xffa0522d },
		{ "silver",		0xffc0c0c0 },
		{ "skyblue",		0xff87ceeb },
		{ "slateblue",		0xff6a5acd },
		{ "slategray",		0xff708090 },
		{ "slategrey",		0xff708090 },
		{ "snow",		0xfffffafa },
		{ "springgreen",	0xff00ff7f },
		{ "steelblue",		0xff4682b4 },
		{ "tan",		0xffd2b48c },
		{ "teal",		0xff008080 },
		{ "thistle",		0xffd8bfd8 },
		{ "tomato",		0xffff6347 },
		{ "turquoise",		0xff40e0d0 },
		{ "violet",		0xffee82ee },
		{ "violetred",		0xffd02090 },
		{ "wheat",		0xfff5deb3 },
		{ "white",		0xffffffff },
		{ "whitesmoke",		0xfff5f5f5 },
		{ "yellow",		0xffffff00 },
		{ "yellowgreen",	0xff9acd32 }
	};
	const struct colour_map *entry;

	entry = bsearch(name, named_colours,
			sizeof(named_colours) / sizeof(named_colours[0]),
			sizeof(named_colours[0]),
			cmp_colour_name);

	if (entry != NULL)
		*result = entry->color;

	return entry != NULL;
}

/**
 * Parse a dimension string
 *
 * \param data    Data to parse (NUL-terminated)
 * \param strict  Whether to enforce strict parsing rules
 * \param length  Pointer to location to receive dimension's length
 * \param unit    Pointer to location to receive dimension's unit
 * \return true on success, false on invalid input
 */
bool parse_dimension(const char *data, bool strict, css_fixed *length,
		css_unit *unit)
{
	size_t len;
	size_t read;
	css_fixed value;

	len = strlen(data);

	if (parse_number(data, false, true, &value, &read) == false)
		return false;

	if (strict && value < INTTOFIX(1))
		return false;

	*length = value;

	if (len > read && data[read] == '%')
		*unit = CSS_UNIT_PCT;
	else
		*unit = CSS_UNIT_PX;

	return true;
}

/**
 * Parse a number string
 *
 * \param data  Data to parse (NUL-terminated)
 * \param maybe_negative  Negative numbers permitted
 * \param real            Floating point numbers permitted
 * \param value           Pointer to location to receive numeric value
 * \param consumed        Pointer to location to receive number of input
 *                        bytes consumed
 * \return true on success, false on invalid input
 */
bool parse_number(const char *data, bool maybe_negative, bool real,
		css_fixed *value, size_t *consumed)
{
	size_t len;
	const uint8_t *ptr;
	int32_t intpart = 0;
	int32_t fracpart = 0;
	int32_t pwr = 1;
	int sign = 1;

	*consumed = 0;

	len = strlen(data);
	ptr = (const uint8_t *) data;

	if (len == 0)
		return false;

	/* Skip leading whitespace */
	while (len > 0 && isWhitespace(ptr[0])) {
		len--;
		ptr++;
	}

	if (len == 0)
		return false;

	/* Extract sign, if any */
	if (ptr[0] == '+') {
		len--;
		ptr++;
	} else if (ptr[0] == '-' && maybe_negative) {
		sign = -1;
		len--;
		ptr++;
	}

	if (len == 0)
		return false;

	/* Must have a digit [0,9] */
	if ('0' > ptr[0] || ptr[0] > '9')
		return false;

	/* Now extract intpart, assuming base 10 */
	while (len > 0) {
		/* Stop on first non-digit */
		if (ptr[0] < '0' || '9' < ptr[0])
			break;

		/* Prevent overflow of 'intpart'; proper clamping below */
		if (intpart < (1 << 22)) {
			intpart *= 10;
			intpart += ptr[0] - '0';
		}
		ptr++;
		len--;
	}

	/* And fracpart, again, assuming base 10 */
	if (real && len > 1 && ptr[0] == '.' &&
			('0' <= ptr[1] && ptr[1] <= '9')) {
		ptr++;
		len--;

		while (len > 0) {
			if (ptr[0] < '0' || '9' < ptr[0])
				break;

			if (pwr < 1000000) {
				pwr *= 10;
				fracpart *= 10;
				fracpart += ptr[0] - '0';
			}
			ptr++;
			len--;
		}

		fracpart = ((1 << 10) * fracpart + pwr/2) / pwr;
		if (fracpart >= (1 << 10)) {
			intpart++;
			fracpart &= (1 << 10) - 1;
		}
	}

	if (sign > 0) {
		/* If the result is larger than we can represent,
		 * then clamp to the maximum value we can store. */
		if (intpart >= (1 << 21)) {
			intpart = (1 << 21) - 1;
			fracpart = (1 << 10) - 1;
		}
	} else {
		/* If the negated result is smaller than we can represent
		 * then clamp to the minimum value we can store. */
		if (intpart >= (1 << 21)) {
			intpart = -(1 << 21);
			fracpart = 0;
		} else {
			intpart = -intpart;
			if (fracpart) {
				fracpart = (1 << 10) - fracpart;
				intpart--;
			}
		}
	}

	*value = (intpart << 10) | fracpart;

	*consumed = ptr - (const uint8_t *) data;

	return true;
}

/**
 * Parse a font \@size attribute
 *
 * \param size  Data to parse (NUL-terminated)
 * \param val   Pointer to location to receive enum value
 * \param len   Pointer to location to receive length
 * \param unit  Pointer to location to receive unit
 * \return True on success, false on failure
 */
bool parse_font_size(const char *size, uint8_t *val, 
		css_fixed *len, css_unit *unit)
{
	static const uint8_t size_map[] = {
		CSS_FONT_SIZE_XX_SMALL,
		CSS_FONT_SIZE_SMALL,
		CSS_FONT_SIZE_MEDIUM,
		CSS_FONT_SIZE_LARGE,
		CSS_FONT_SIZE_X_LARGE,
		CSS_FONT_SIZE_XX_LARGE,
		CSS_FONT_SIZE_DIMENSION	/* xxx-large (see below) */
	};

	const char *p = size;
	char mode;
	int value = 0;

	/* Skip whitespace */
	while (*p != '\0' && isWhitespace(*p))
		p++;

	mode = *p;

	/* Skip +/- */
	if (mode == '+' || mode == '-')
		p++;

	/* Need at least one digit */
	if (*p < '0' || *p > '9') {
		return false;
	}

	/* Consume digits, computing value */
	while ('0' <= *p && *p <= '9') {
		value = value * 10 + (*p - '0');
		p++;
	}

	/* Resolve relative sizes */
	if (mode == '+')
		value += 3;
	else if (mode == '-')
		value = 3 - value;

	/* Clamp to range [1,7] */
	if (value < 1)
		value = 1;
	else if (value > 7)
		value = 7;

	if (value == 7) {
		/* Manufacture xxx-large */
	  *len = FDIV(FMUL(INTTOFIX(3), INTTOFIX(nsoption_int(font_size))), 
				F_10);
	} else {
		/* Len is irrelevant */
		*len = 0;
	}

	*unit = CSS_UNIT_PT;
	*val = size_map[value - 1];

	return true;
}

/******************************************************************************
 * Utility functions                                                          *
 ******************************************************************************/

/**
 * Determine if a given character is whitespace
 *
 * \param c  Character to consider
 * \return true if character is whitespace, false otherwise
 */
bool isWhitespace(char c)
{
	return c == ' ' || c == '\t' || c == '\f' || c == '\r' || c == '\n';
}

/**
 * Determine if a given character is a valid hex digit
 *
 * \param c  Character to consider
 * \return true if character is a valid hex digit, false otherwise
 */
bool isHex(char c)
{
	return ('0' <= c && c <= '9') ||
			('A' <= (c & ~0x20) && (c & ~0x20) <= 'F');
}

/**
 * Convert a character representing a hex digit to the corresponding hex value
 *
 * \param c  Character to convert
 * \return Hex value represented by character
 *
 * \note This function assumes an ASCII-compatible character set
 */
uint8_t charToHex(char c)
{
	/* 0-9 */
	c -= '0';

	/* A-F */
	if (c > 9)
		c -= 'A' - '9' - 1;

	/* a-f */
	if (c > 15)
		c -= 'a' - 'A';

	return c;
}

