b6cc9c7ff6
For implementing extensions, we want the ability to add a stylesheet dynamically, and unload it as well. https://bugzilla.gnome.org/show_bug.cgi?id=599661
1127 lines
34 KiB
C
1127 lines
34 KiB
C
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
|
|
|
|
/* This file started as a cut-and-paste of cr-sel-eng.c from libcroco.
|
|
*
|
|
* In moving it to hippo-canvas:
|
|
* - Reformatted and otherwise edited to match our coding style
|
|
* - Switched from handling xmlNode to handling HippoStyle
|
|
* - Simplified by removing things that we don't need or that don't
|
|
* make sense in our context.
|
|
* - The code to get a list of matching properties works quite differently;
|
|
* we order things in priority order, but we don't actually try to
|
|
* coalesce properties with the same name.
|
|
*
|
|
* In moving it to GNOME Shell:
|
|
* - Renamed again to StTheme
|
|
* - Reformatted to match the gnome-shell coding style
|
|
* - Removed notion of "theme engine" from hippo-canvas
|
|
* - pseudo-class matching changed from link enum to strings
|
|
* - Some code simplification
|
|
*/
|
|
|
|
/*
|
|
* This file is part of The Croco Library
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of version 2.1 of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation.
|
|
*
|
|
* 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 Lesser
|
|
* General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
|
|
* USA
|
|
*
|
|
* Copyright (C) 2003-2004 Dodji Seketeli. All Rights Reserved.
|
|
*/
|
|
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include <gio/gio.h>
|
|
|
|
#include "st-theme-node.h"
|
|
#include "st-theme-private.h"
|
|
|
|
static GObject *st_theme_constructor (GType type,
|
|
guint n_construct_properties,
|
|
GObjectConstructParam *construct_properties);
|
|
|
|
static void st_theme_finalize (GObject *object);
|
|
static void st_theme_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec);
|
|
static void st_theme_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec);
|
|
|
|
struct _StTheme
|
|
{
|
|
GObject parent;
|
|
|
|
char *application_stylesheet;
|
|
char *default_stylesheet;
|
|
char *theme_stylesheet;
|
|
GSList *custom_stylesheets;
|
|
|
|
GHashTable *stylesheets_by_filename;
|
|
GHashTable *filenames_by_stylesheet;
|
|
|
|
CRCascade *cascade;
|
|
};
|
|
|
|
struct _StThemeClass
|
|
{
|
|
GObjectClass parent_class;
|
|
};
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_APPLICATION_STYLESHEET,
|
|
PROP_THEME_STYLESHEET,
|
|
PROP_DEFAULT_STYLESHEET
|
|
};
|
|
|
|
G_DEFINE_TYPE (StTheme, st_theme, G_TYPE_OBJECT)
|
|
|
|
/* Quick strcmp. Test only for == 0 or != 0, not < 0 or > 0. */
|
|
#define strqcmp(str,lit,lit_len) \
|
|
(strlen (str) != (lit_len) || memcmp (str, lit, lit_len))
|
|
|
|
static void
|
|
st_theme_init (StTheme *theme)
|
|
{
|
|
theme->stylesheets_by_filename = g_hash_table_new_full (g_str_hash, g_str_equal,
|
|
(GDestroyNotify)g_free, (GDestroyNotify)cr_stylesheet_unref);
|
|
theme->filenames_by_stylesheet = g_hash_table_new (g_direct_hash, g_direct_equal);
|
|
}
|
|
|
|
static void
|
|
st_theme_class_init (StThemeClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
object_class->constructor = st_theme_constructor;
|
|
object_class->finalize = st_theme_finalize;
|
|
object_class->set_property = st_theme_set_property;
|
|
object_class->get_property = st_theme_get_property;
|
|
|
|
/**
|
|
* StTheme:application-stylesheet:
|
|
*
|
|
* The highest priority stylesheet, representing application-specific
|
|
* styling; this is associated with the CSS "author" stylesheet.
|
|
*/
|
|
g_object_class_install_property (object_class,
|
|
PROP_APPLICATION_STYLESHEET,
|
|
g_param_spec_string ("application-stylesheet",
|
|
"Application Stylesheet",
|
|
"Stylesheet with application-specific styling",
|
|
NULL,
|
|
G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
/**
|
|
* StTheme:theme-stylesheet:
|
|
*
|
|
* The second priority stylesheet, representing theme-specific styling;
|
|
* this is associated with the CSS "user" stylesheet.
|
|
*/
|
|
g_object_class_install_property (object_class,
|
|
PROP_THEME_STYLESHEET,
|
|
g_param_spec_string ("theme-stylesheet",
|
|
"Theme Stylesheet",
|
|
"Stylesheet with theme-specific styling",
|
|
NULL,
|
|
G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
/**
|
|
* StTheme:default-stylesheet:
|
|
*
|
|
* The lowest priority stylesheet, representing global default
|
|
* styling; this is associated with the CSS "user agent" stylesheet.
|
|
*/
|
|
g_object_class_install_property (object_class,
|
|
PROP_DEFAULT_STYLESHEET,
|
|
g_param_spec_string ("default-stylesheet",
|
|
"Default Stylesheet",
|
|
"Stylesheet with global default styling",
|
|
NULL,
|
|
G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY));
|
|
|
|
}
|
|
|
|
/* This is a workaround for a bug in libcroco < 0.6.2 where
|
|
* function starting with 'r' (and 'u') are misparsed. We work
|
|
* around this by exploiting the fact that libcroco is incomformant
|
|
* with the CSS-spec and case sensitive and pre-convert all
|
|
* occurrences of rgba to RGBA. Then we make our own parsing
|
|
* code check for RGBA as well.
|
|
*/
|
|
#if LIBCROCO_VERSION_NUMBER < 602
|
|
static gboolean
|
|
is_identifier_character (char c)
|
|
{
|
|
/* Actual CSS rules allow for unicode > 0x00a1 and escaped
|
|
* characters, but we'll assume we won't do that in our stylesheets
|
|
* or at least not next to the string 'rgba'.
|
|
*/
|
|
return g_ascii_isalnum(c) || c == '-' || c == '_';
|
|
}
|
|
|
|
static void
|
|
convert_rgba_RGBA (char *buf)
|
|
{
|
|
char *p;
|
|
|
|
p = strstr (buf, "rgba");
|
|
while (p)
|
|
{
|
|
/* Check if this looks like a complete token; this is to
|
|
* avoiding mangling, say, a selector '.rgba-entry' */
|
|
if (!((p > buf && is_identifier_character (*(p - 1))) ||
|
|
(is_identifier_character (*(p + 4)))))
|
|
memcpy(p, "RGBA", 4);
|
|
p += 4;
|
|
p = strstr (p, "rgba");
|
|
}
|
|
}
|
|
|
|
static CRStyleSheet *
|
|
parse_stylesheet (const char *filename,
|
|
GError **error)
|
|
{
|
|
enum CRStatus status;
|
|
char *contents;
|
|
gsize length;
|
|
CRStyleSheet *stylesheet = NULL;
|
|
|
|
if (filename == NULL)
|
|
return NULL;
|
|
|
|
if (!g_file_get_contents (filename, &contents, &length, error))
|
|
return NULL;
|
|
|
|
convert_rgba_RGBA (contents);
|
|
|
|
status = cr_om_parser_simply_parse_buf ((const guchar *) contents,
|
|
length,
|
|
CR_UTF_8,
|
|
&stylesheet);
|
|
g_free (contents);
|
|
|
|
if (status != CR_OK)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Error parsing stylesheet '%s'; errcode:%d", filename, status);
|
|
return NULL;
|
|
}
|
|
|
|
return stylesheet;
|
|
}
|
|
|
|
CRDeclaration *
|
|
_st_theme_parse_declaration_list (const char *str)
|
|
{
|
|
char *copy = g_strdup (str);
|
|
CRDeclaration *result;
|
|
|
|
convert_rgba_RGBA (copy);
|
|
|
|
result = cr_declaration_parse_list_from_buf ((const guchar *)copy,
|
|
CR_UTF_8);
|
|
g_free (copy);
|
|
|
|
return result;
|
|
}
|
|
#else /* LIBCROCO_VERSION_NUMBER >= 602 */
|
|
static CRStyleSheet *
|
|
parse_stylesheet (const char *filename,
|
|
GError **error)
|
|
{
|
|
enum CRStatus status;
|
|
CRStyleSheet *stylesheet;
|
|
|
|
if (filename == NULL)
|
|
return NULL;
|
|
|
|
status = cr_om_parser_simply_parse_file ((const guchar *) filename,
|
|
CR_UTF_8,
|
|
&stylesheet);
|
|
|
|
if (status != CR_OK)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Error parsing stylesheet '%s'; errcode:%d", filename, status);
|
|
return NULL;
|
|
}
|
|
|
|
return stylesheet;
|
|
}
|
|
|
|
CRDeclaration *
|
|
_st_theme_parse_declaration_list (const char *str)
|
|
{
|
|
return cr_declaration_parse_list_from_buf ((const guchar *)str,
|
|
CR_UTF_8);
|
|
}
|
|
#endif /* LIBCROCO_VERSION_NUMBER < 602 */
|
|
|
|
/* Just g_warning for now until we have something nicer to do */
|
|
static CRStyleSheet *
|
|
parse_stylesheet_nofail (const char *filename)
|
|
{
|
|
GError *error = NULL;
|
|
CRStyleSheet *result;
|
|
|
|
result = parse_stylesheet (filename, &error);
|
|
if (error)
|
|
{
|
|
g_warning ("%s", error->message);
|
|
g_clear_error (&error);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
insert_stylesheet (StTheme *theme,
|
|
const char *filename,
|
|
CRStyleSheet *stylesheet)
|
|
{
|
|
char *filename_copy;
|
|
|
|
if (stylesheet == NULL)
|
|
return;
|
|
|
|
filename_copy = g_strdup(filename);
|
|
cr_stylesheet_ref (stylesheet);
|
|
|
|
g_hash_table_insert (theme->stylesheets_by_filename, filename_copy, stylesheet);
|
|
g_hash_table_insert (theme->filenames_by_stylesheet, stylesheet, filename_copy);
|
|
}
|
|
|
|
gboolean
|
|
st_theme_load_stylesheet (StTheme *theme,
|
|
const char *path,
|
|
GError **error)
|
|
{
|
|
CRStyleSheet *stylesheet;
|
|
|
|
stylesheet = parse_stylesheet (path, error);
|
|
if (!stylesheet)
|
|
return FALSE;
|
|
|
|
insert_stylesheet (theme, path, stylesheet);
|
|
theme->custom_stylesheets = g_slist_prepend (theme->custom_stylesheets, stylesheet);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void
|
|
st_theme_unload_stylesheet (StTheme *theme,
|
|
const char *path)
|
|
{
|
|
CRStyleSheet *stylesheet;
|
|
|
|
stylesheet = g_hash_table_lookup (theme->stylesheets_by_filename, path);
|
|
if (!stylesheet)
|
|
return;
|
|
|
|
if (!g_slist_find (theme->custom_stylesheets, stylesheet))
|
|
return;
|
|
|
|
theme->custom_stylesheets = g_slist_remove (theme->custom_stylesheets, stylesheet);
|
|
g_hash_table_remove (theme->stylesheets_by_filename, path);
|
|
g_hash_table_remove (theme->filenames_by_stylesheet, stylesheet);
|
|
cr_stylesheet_unref (stylesheet);
|
|
}
|
|
|
|
static GObject *
|
|
st_theme_constructor (GType type,
|
|
guint n_construct_properties,
|
|
GObjectConstructParam *construct_properties)
|
|
{
|
|
GObject *object;
|
|
StTheme *theme;
|
|
CRStyleSheet *application_stylesheet;
|
|
CRStyleSheet *theme_stylesheet;
|
|
CRStyleSheet *default_stylesheet;
|
|
|
|
object = (*G_OBJECT_CLASS (st_theme_parent_class)->constructor) (type,
|
|
n_construct_properties,
|
|
construct_properties);
|
|
theme = ST_THEME (object);
|
|
|
|
application_stylesheet = parse_stylesheet_nofail (theme->application_stylesheet);
|
|
theme_stylesheet = parse_stylesheet_nofail (theme->theme_stylesheet);
|
|
default_stylesheet = parse_stylesheet_nofail (theme->default_stylesheet);
|
|
|
|
theme->cascade = cr_cascade_new (application_stylesheet,
|
|
theme_stylesheet,
|
|
default_stylesheet);
|
|
|
|
if (theme->cascade == NULL)
|
|
g_error ("Out of memory when creating cascade object");
|
|
|
|
insert_stylesheet (theme, theme->application_stylesheet, application_stylesheet);
|
|
insert_stylesheet (theme, theme->theme_stylesheet, theme_stylesheet);
|
|
insert_stylesheet (theme, theme->default_stylesheet, default_stylesheet);
|
|
|
|
return object;
|
|
}
|
|
|
|
static void
|
|
st_theme_finalize (GObject * object)
|
|
{
|
|
StTheme *theme = ST_THEME (object);
|
|
|
|
g_slist_foreach (theme->custom_stylesheets, (GFunc) cr_stylesheet_unref, NULL);
|
|
g_slist_free (theme->custom_stylesheets);
|
|
theme->custom_stylesheets = NULL;
|
|
|
|
g_hash_table_destroy (theme->stylesheets_by_filename);
|
|
g_hash_table_destroy (theme->filenames_by_stylesheet);
|
|
|
|
g_free (theme->application_stylesheet);
|
|
g_free (theme->theme_stylesheet);
|
|
g_free (theme->default_stylesheet);
|
|
|
|
if (theme->cascade)
|
|
{
|
|
cr_cascade_unref (theme->cascade);
|
|
theme->cascade = NULL;
|
|
}
|
|
|
|
G_OBJECT_CLASS (st_theme_parent_class)->finalize (object);
|
|
}
|
|
|
|
static void
|
|
st_theme_set_property (GObject *object,
|
|
guint prop_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
StTheme *theme = ST_THEME (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_APPLICATION_STYLESHEET:
|
|
{
|
|
const char *path = g_value_get_string (value);
|
|
|
|
if (path != theme->application_stylesheet)
|
|
{
|
|
g_free (theme->application_stylesheet);
|
|
theme->application_stylesheet = g_strdup (path);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case PROP_THEME_STYLESHEET:
|
|
{
|
|
const char *path = g_value_get_string (value);
|
|
|
|
if (path != theme->theme_stylesheet)
|
|
{
|
|
g_free (theme->theme_stylesheet);
|
|
theme->theme_stylesheet = g_strdup (path);
|
|
}
|
|
|
|
break;
|
|
}
|
|
case PROP_DEFAULT_STYLESHEET:
|
|
{
|
|
const char *path = g_value_get_string (value);
|
|
|
|
if (path != theme->default_stylesheet)
|
|
{
|
|
g_free (theme->default_stylesheet);
|
|
theme->default_stylesheet = g_strdup (path);
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
st_theme_get_property (GObject *object,
|
|
guint prop_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
StTheme *theme = ST_THEME (object);
|
|
|
|
switch (prop_id)
|
|
{
|
|
case PROP_APPLICATION_STYLESHEET:
|
|
g_value_set_string (value, theme->application_stylesheet);
|
|
break;
|
|
case PROP_THEME_STYLESHEET:
|
|
g_value_set_string (value, theme->theme_stylesheet);
|
|
break;
|
|
case PROP_DEFAULT_STYLESHEET:
|
|
g_value_set_string (value, theme->default_stylesheet);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* st_theme_new:
|
|
* @application_stylesheet: The highest priority stylesheet, representing application-specific
|
|
* styling; this is associated with the CSS "author" stylesheet, may be %NULL
|
|
* @theme_stylesheet: The second priority stylesheet, representing theme-specific styling ;
|
|
* this is associated with the CSS "user" stylesheet, may be %NULL
|
|
* @default_stylesheet: The lowest priority stylesheet, representing global default styling;
|
|
* this is associated with the CSS "user agent" stylesheet, may be %NULL
|
|
*
|
|
* Return value: the newly created theme object
|
|
**/
|
|
StTheme *
|
|
st_theme_new (const char *application_stylesheet,
|
|
const char *theme_stylesheet,
|
|
const char *default_stylesheet)
|
|
{
|
|
StTheme *theme = g_object_new (ST_TYPE_THEME,
|
|
"application-stylesheet", application_stylesheet,
|
|
"theme-stylesheet", theme_stylesheet,
|
|
"default-stylesheet", default_stylesheet,
|
|
NULL);
|
|
|
|
return theme;
|
|
}
|
|
|
|
static gboolean
|
|
string_in_list (GString *stryng,
|
|
const char *list)
|
|
{
|
|
const char *cur;
|
|
|
|
for (cur = list; *cur;)
|
|
{
|
|
while (*cur && cr_utils_is_white_space (*cur))
|
|
cur++;
|
|
|
|
if (strncmp (cur, stryng->str, stryng->len) == 0)
|
|
{
|
|
cur += stryng->len;
|
|
if ((!*cur) || cr_utils_is_white_space (*cur))
|
|
return TRUE;
|
|
}
|
|
|
|
/* skip to next whitespace character */
|
|
while (*cur && !cr_utils_is_white_space (*cur))
|
|
cur++;
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
pseudo_class_add_sel_matches_style (StTheme *a_this,
|
|
CRAdditionalSel *a_add_sel,
|
|
StThemeNode *a_node)
|
|
{
|
|
const char *node_pseudo_class;
|
|
|
|
g_return_val_if_fail (a_this
|
|
&& a_add_sel
|
|
&& a_add_sel->content.pseudo
|
|
&& a_add_sel->content.pseudo->name
|
|
&& a_add_sel->content.pseudo->name->stryng
|
|
&& a_add_sel->content.pseudo->name->stryng->str
|
|
&& a_node, FALSE);
|
|
|
|
node_pseudo_class = st_theme_node_get_pseudo_class (a_node);
|
|
|
|
if (node_pseudo_class == NULL)
|
|
return FALSE;
|
|
|
|
return string_in_list (a_add_sel->content.pseudo->name->stryng, node_pseudo_class);
|
|
}
|
|
|
|
/**
|
|
*@param a_add_sel the class additional selector to consider.
|
|
*@param a_node the style node to consider.
|
|
*@return TRUE if the class additional selector matches
|
|
*the style node given in argument, FALSE otherwise.
|
|
*/
|
|
static gboolean
|
|
class_add_sel_matches_style (CRAdditionalSel *a_add_sel,
|
|
StThemeNode *a_node)
|
|
{
|
|
const char *element_class;
|
|
|
|
g_return_val_if_fail (a_add_sel
|
|
&& a_add_sel->type == CLASS_ADD_SELECTOR
|
|
&& a_add_sel->content.class_name
|
|
&& a_add_sel->content.class_name->stryng
|
|
&& a_add_sel->content.class_name->stryng->str
|
|
&& a_node, FALSE);
|
|
|
|
element_class = st_theme_node_get_element_class (a_node);
|
|
if (element_class == NULL)
|
|
return FALSE;
|
|
|
|
return string_in_list (a_add_sel->content.class_name->stryng, element_class);
|
|
}
|
|
|
|
/**
|
|
*@return TRUE if the additional attribute selector matches
|
|
*the current style node given in argument, FALSE otherwise.
|
|
*@param a_add_sel the additional attribute selector to consider.
|
|
*@param a_node the style node to consider.
|
|
*/
|
|
static gboolean
|
|
id_add_sel_matches_style (CRAdditionalSel *a_add_sel,
|
|
StThemeNode *a_node)
|
|
{
|
|
gboolean result = FALSE;
|
|
const char *id;
|
|
|
|
g_return_val_if_fail (a_add_sel
|
|
&& a_add_sel->type == ID_ADD_SELECTOR
|
|
&& a_add_sel->content.id_name
|
|
&& a_add_sel->content.id_name->stryng
|
|
&& a_add_sel->content.id_name->stryng->str
|
|
&& a_node, FALSE);
|
|
g_return_val_if_fail (a_add_sel
|
|
&& a_add_sel->type == ID_ADD_SELECTOR
|
|
&& a_node, FALSE);
|
|
|
|
id = st_theme_node_get_element_id (a_node);
|
|
|
|
if (id != NULL)
|
|
{
|
|
if (!strqcmp (id, a_add_sel->content.id_name->stryng->str,
|
|
a_add_sel->content.id_name->stryng->len))
|
|
{
|
|
result = TRUE;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
*additional_selector_matches_style:
|
|
*Evaluates if a given additional selector matches an style node.
|
|
*@param a_add_sel the additional selector to consider.
|
|
*@param a_node the style node to consider.
|
|
*@return TRUE is a_add_sel matches a_node, FALSE otherwise.
|
|
*/
|
|
static gboolean
|
|
additional_selector_matches_style (StTheme *a_this,
|
|
CRAdditionalSel *a_add_sel,
|
|
StThemeNode *a_node)
|
|
{
|
|
CRAdditionalSel *cur_add_sel = NULL;
|
|
|
|
g_return_val_if_fail (a_add_sel, FALSE);
|
|
|
|
for (cur_add_sel = a_add_sel; cur_add_sel; cur_add_sel = cur_add_sel->next)
|
|
{
|
|
switch (cur_add_sel->type)
|
|
{
|
|
case NO_ADD_SELECTOR:
|
|
return FALSE;
|
|
case CLASS_ADD_SELECTOR:
|
|
if (!class_add_sel_matches_style (cur_add_sel, a_node))
|
|
return FALSE;
|
|
break;
|
|
case ID_ADD_SELECTOR:
|
|
if (!id_add_sel_matches_style (cur_add_sel, a_node))
|
|
return FALSE;
|
|
break;
|
|
case ATTRIBUTE_ADD_SELECTOR:
|
|
g_warning ("Attribute selectors not supported");
|
|
return FALSE;
|
|
case PSEUDO_CLASS_ADD_SELECTOR:
|
|
if (!pseudo_class_add_sel_matches_style (a_this, cur_add_sel, a_node))
|
|
return FALSE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
element_name_matches_type (const char *element_name,
|
|
GType element_type)
|
|
{
|
|
if (element_type == G_TYPE_NONE)
|
|
{
|
|
return strcmp (element_name, "stage") == 0;
|
|
}
|
|
else
|
|
{
|
|
GType match_type = g_type_from_name (element_name);
|
|
if (match_type == G_TYPE_INVALID)
|
|
return FALSE;
|
|
|
|
return g_type_is_a (element_type, match_type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*Evaluate a selector (a simple selectors list) and says
|
|
*if it matches the style node given in parameter.
|
|
*The algorithm used here is the following:
|
|
*Walk the combinator separated list of simple selectors backward, starting
|
|
*from the end of the list. For each simple selector, looks if
|
|
*if matches the current style.
|
|
*
|
|
*@param a_this the selection engine.
|
|
*@param a_sel the simple selection list.
|
|
*@param a_node the style node.
|
|
*@param a_result out parameter. Set to true if the
|
|
*selector matches the style node, FALSE otherwise.
|
|
*@param a_recurse if set to TRUE, the function will walk to
|
|
*the next simple selector (after the evaluation of the current one)
|
|
*and recursively evaluate it. Must be usually set to TRUE unless you
|
|
*know what you are doing.
|
|
*/
|
|
static enum CRStatus
|
|
sel_matches_style_real (StTheme *a_this,
|
|
CRSimpleSel *a_sel,
|
|
StThemeNode *a_node,
|
|
gboolean *a_result,
|
|
gboolean a_eval_sel_list_from_end,
|
|
gboolean a_recurse)
|
|
{
|
|
CRSimpleSel *cur_sel = NULL;
|
|
StThemeNode *cur_node = NULL;
|
|
GType cur_type;
|
|
|
|
*a_result = FALSE;
|
|
|
|
if (a_eval_sel_list_from_end)
|
|
{
|
|
/*go and get the last simple selector of the list */
|
|
for (cur_sel = a_sel; cur_sel && cur_sel->next; cur_sel = cur_sel->next)
|
|
;
|
|
}
|
|
else
|
|
{
|
|
cur_sel = a_sel;
|
|
}
|
|
|
|
cur_node = a_node;
|
|
cur_type = st_theme_node_get_element_type (cur_node);
|
|
|
|
while (cur_sel)
|
|
{
|
|
if (((cur_sel->type_mask & TYPE_SELECTOR)
|
|
&& (cur_sel->name
|
|
&& cur_sel->name->stryng
|
|
&& cur_sel->name->stryng->str)
|
|
&&
|
|
(element_name_matches_type (cur_sel->name->stryng->str, cur_type)))
|
|
|| (cur_sel->type_mask & UNIVERSAL_SELECTOR))
|
|
{
|
|
/*
|
|
*this simple selector
|
|
*matches the current style node
|
|
*Let's see if the preceding
|
|
*simple selectors also match
|
|
*their style node counterpart.
|
|
*/
|
|
if (cur_sel->add_sel)
|
|
{
|
|
if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node))
|
|
goto walk_a_step_in_expr;
|
|
else
|
|
goto done;
|
|
}
|
|
else
|
|
goto walk_a_step_in_expr;
|
|
}
|
|
if (!(cur_sel->type_mask & TYPE_SELECTOR)
|
|
&& !(cur_sel->type_mask & UNIVERSAL_SELECTOR))
|
|
{
|
|
if (!cur_sel->add_sel)
|
|
goto done;
|
|
if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node))
|
|
goto walk_a_step_in_expr;
|
|
else
|
|
goto done;
|
|
}
|
|
else
|
|
{
|
|
goto done;
|
|
}
|
|
|
|
walk_a_step_in_expr:
|
|
if (a_recurse == FALSE)
|
|
{
|
|
*a_result = TRUE;
|
|
goto done;
|
|
}
|
|
|
|
/*
|
|
*here, depending on the combinator of cur_sel
|
|
*choose the axis of the element tree traversal
|
|
*and walk one step in the element tree.
|
|
*/
|
|
if (!cur_sel->prev)
|
|
break;
|
|
|
|
switch (cur_sel->combinator)
|
|
{
|
|
case NO_COMBINATOR:
|
|
break;
|
|
|
|
case COMB_WS: /*descendant selector */
|
|
{
|
|
StThemeNode *n = NULL;
|
|
|
|
/*
|
|
*walk the element tree upward looking for a parent
|
|
*style that matches the preceding selector.
|
|
*/
|
|
for (n = st_theme_node_get_parent (a_node); n; n = st_theme_node_get_parent (n))
|
|
{
|
|
enum CRStatus status;
|
|
gboolean matches = FALSE;
|
|
|
|
status = sel_matches_style_real (a_this, cur_sel->prev, n, &matches, FALSE, TRUE);
|
|
|
|
if (status != CR_OK)
|
|
goto done;
|
|
|
|
if (matches)
|
|
{
|
|
cur_node = n;
|
|
cur_type = st_theme_node_get_element_type (cur_node);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!n)
|
|
{
|
|
/*
|
|
*didn't find any ancestor that matches
|
|
*the previous simple selector.
|
|
*/
|
|
goto done;
|
|
}
|
|
/*
|
|
*in this case, the preceding simple sel
|
|
*will have been interpreted twice, which
|
|
*is a cpu and mem waste ... I need to find
|
|
*another way to do this. Anyway, this is
|
|
*my first attempt to write this function and
|
|
*I am a bit clueless.
|
|
*/
|
|
break;
|
|
}
|
|
|
|
case COMB_PLUS:
|
|
g_warning ("+ combinators are not supported");
|
|
goto done;
|
|
|
|
case COMB_GT:
|
|
cur_node = st_theme_node_get_parent (cur_node);
|
|
if (!cur_node)
|
|
goto done;
|
|
cur_type = st_theme_node_get_element_type (cur_node);
|
|
break;
|
|
|
|
default:
|
|
goto done;
|
|
}
|
|
|
|
cur_sel = cur_sel->prev;
|
|
}
|
|
|
|
/*
|
|
*if we reached this point, it means the selector matches
|
|
*the style node.
|
|
*/
|
|
*a_result = TRUE;
|
|
|
|
done:
|
|
return CR_OK;
|
|
}
|
|
|
|
static void
|
|
add_matched_properties (StTheme *a_this,
|
|
CRStyleSheet *a_nodesheet,
|
|
StThemeNode *a_node,
|
|
GPtrArray *props)
|
|
{
|
|
CRStatement *cur_stmt = NULL;
|
|
CRSelector *sel_list = NULL;
|
|
CRSelector *cur_sel = NULL;
|
|
gboolean matches = FALSE;
|
|
enum CRStatus status = CR_OK;
|
|
|
|
/*
|
|
*walk through the list of statements and,
|
|
*get the selectors list inside the statements that
|
|
*contain some, and try to match our style node in these
|
|
*selectors lists.
|
|
*/
|
|
for (cur_stmt = a_nodesheet->statements; cur_stmt; cur_stmt = cur_stmt->next)
|
|
{
|
|
/*
|
|
*initialyze the selector list in which we will
|
|
*really perform the search.
|
|
*/
|
|
sel_list = NULL;
|
|
|
|
/*
|
|
*get the the damn selector list in
|
|
*which we have to look
|
|
*/
|
|
switch (cur_stmt->type)
|
|
{
|
|
case RULESET_STMT:
|
|
if (cur_stmt->kind.ruleset && cur_stmt->kind.ruleset->sel_list)
|
|
{
|
|
sel_list = cur_stmt->kind.ruleset->sel_list;
|
|
}
|
|
break;
|
|
|
|
case AT_MEDIA_RULE_STMT:
|
|
if (cur_stmt->kind.media_rule
|
|
&& cur_stmt->kind.media_rule->rulesets
|
|
&& cur_stmt->kind.media_rule->rulesets->kind.ruleset
|
|
&& cur_stmt->kind.media_rule->rulesets->kind.ruleset->sel_list)
|
|
{
|
|
sel_list = cur_stmt->kind.media_rule->rulesets->kind.ruleset->sel_list;
|
|
}
|
|
break;
|
|
|
|
case AT_IMPORT_RULE_STMT:
|
|
{
|
|
CRAtImportRule *import_rule = cur_stmt->kind.import_rule;
|
|
|
|
if (import_rule->sheet == NULL)
|
|
{
|
|
char *filename = NULL;
|
|
|
|
if (import_rule->url->stryng && import_rule->url->stryng->str)
|
|
filename = _st_theme_resolve_url (a_this,
|
|
a_nodesheet,
|
|
import_rule->url->stryng->str);
|
|
|
|
if (filename)
|
|
import_rule->sheet = parse_stylesheet (filename, NULL);
|
|
|
|
if (import_rule->sheet)
|
|
{
|
|
insert_stylesheet (a_this, filename, import_rule->sheet);
|
|
/* refcount of stylesheets starts off at zero, so we don't need to unref! */
|
|
}
|
|
else
|
|
{
|
|
/* Set a marker to avoid repeatedly trying to parse a non-existent or
|
|
* broken stylesheet
|
|
*/
|
|
import_rule->sheet = (CRStyleSheet *) - 1;
|
|
}
|
|
|
|
if (filename)
|
|
g_free (filename);
|
|
}
|
|
|
|
if (import_rule->sheet != (CRStyleSheet *) - 1)
|
|
{
|
|
add_matched_properties (a_this, import_rule->sheet,
|
|
a_node, props);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!sel_list)
|
|
continue;
|
|
|
|
/*
|
|
*now, we have a comma separated selector list to look in.
|
|
*let's walk it and try to match the style node
|
|
*on each item of the list.
|
|
*/
|
|
for (cur_sel = sel_list; cur_sel; cur_sel = cur_sel->next)
|
|
{
|
|
if (!cur_sel->simple_sel)
|
|
continue;
|
|
|
|
status = sel_matches_style_real (a_this, cur_sel->simple_sel, a_node, &matches, TRUE, TRUE);
|
|
|
|
if (status == CR_OK && matches)
|
|
{
|
|
CRDeclaration *cur_decl = NULL;
|
|
|
|
/* In order to sort the matching properties, we need to compute the
|
|
* specificity of the selector that actually matched this
|
|
* element. In a non-thread-safe fashion, we store it in the
|
|
* ruleset. (Fixing this would mean cut-and-pasting
|
|
* cr_simple_sel_compute_specificity(), and have no need for
|
|
* thread-safety anyways.)
|
|
*
|
|
* Once we've sorted the properties, the specificity no longer
|
|
* matters and it can be safely overriden.
|
|
*/
|
|
cr_simple_sel_compute_specificity (cur_sel->simple_sel);
|
|
|
|
cur_stmt->specificity = cur_sel->simple_sel->specificity;
|
|
|
|
for (cur_decl = cur_stmt->kind.ruleset->decl_list; cur_decl; cur_decl = cur_decl->next)
|
|
g_ptr_array_add (props, cur_decl);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#define ORIGIN_AUTHOR_IMPORTANT (ORIGIN_AUTHOR + 1)
|
|
#define ORIGIN_USER_IMPORTANT (ORIGIN_AUTHOR + 2)
|
|
|
|
static inline int
|
|
get_origin (const CRDeclaration * decl)
|
|
{
|
|
enum CRStyleOrigin origin = decl->parent_statement->parent_sheet->origin;
|
|
|
|
if (decl->important)
|
|
{
|
|
if (origin == ORIGIN_AUTHOR)
|
|
return ORIGIN_AUTHOR_IMPORTANT;
|
|
else if (origin == ORIGIN_USER)
|
|
return ORIGIN_USER_IMPORTANT;
|
|
}
|
|
|
|
return origin;
|
|
}
|
|
|
|
/* Order of comparison is so that higher priority statements compare after
|
|
* lower priority statements */
|
|
static int
|
|
compare_declarations (gconstpointer a,
|
|
gconstpointer b)
|
|
{
|
|
/* g_ptr_array_sort() is broooken */
|
|
CRDeclaration *decl_a = *(CRDeclaration **) a;
|
|
CRDeclaration *decl_b = *(CRDeclaration **) b;
|
|
|
|
int origin_a = get_origin (decl_a);
|
|
int origin_b = get_origin (decl_b);
|
|
|
|
if (origin_a != origin_b)
|
|
return origin_a - origin_b;
|
|
|
|
if (decl_a->parent_statement->specificity != decl_b->parent_statement->specificity)
|
|
return decl_a->parent_statement->specificity - decl_b->parent_statement->specificity;
|
|
|
|
return 0;
|
|
}
|
|
|
|
GPtrArray *
|
|
_st_theme_get_matched_properties (StTheme *theme,
|
|
StThemeNode *node)
|
|
{
|
|
enum CRStyleOrigin origin = 0;
|
|
CRStyleSheet *sheet = NULL;
|
|
GPtrArray *props = g_ptr_array_new ();
|
|
GSList *iter;
|
|
|
|
g_return_val_if_fail (ST_IS_THEME (theme), NULL);
|
|
g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL);
|
|
|
|
for (origin = ORIGIN_UA; origin < NB_ORIGINS; origin++)
|
|
{
|
|
sheet = cr_cascade_get_sheet (theme->cascade, origin);
|
|
if (!sheet)
|
|
continue;
|
|
|
|
add_matched_properties (theme, sheet, node, props);
|
|
}
|
|
|
|
for (iter = theme->custom_stylesheets; iter; iter = iter->next)
|
|
add_matched_properties (theme, iter->data, node, props);
|
|
|
|
/* We count on a stable sort here so that later declarations come
|
|
* after earlier declarations */
|
|
g_ptr_array_sort (props, compare_declarations);
|
|
|
|
return props;
|
|
}
|
|
|
|
/* Resolve an url from an url() reference in a stylesheet into an absolute
|
|
* local filename, if possible. The resolution here is distinctly lame and
|
|
* will fail on many examples.
|
|
*/
|
|
char *
|
|
_st_theme_resolve_url (StTheme *theme,
|
|
CRStyleSheet *base_stylesheet,
|
|
const char *url)
|
|
{
|
|
const char *base_filename = NULL;
|
|
char *dirname;
|
|
char *filename;
|
|
|
|
/* Handle absolute file:/ URLs */
|
|
if (g_str_has_prefix (url, "file:") ||
|
|
g_str_has_prefix (url, "File:") ||
|
|
g_str_has_prefix (url, "FILE:"))
|
|
{
|
|
GError *error = NULL;
|
|
char *filename;
|
|
|
|
filename = g_filename_from_uri (url, NULL, &error);
|
|
if (filename == NULL)
|
|
{
|
|
g_warning ("%s", error->message);
|
|
g_error_free (error);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Guard against http:/ URLs */
|
|
|
|
if (g_str_has_prefix (url, "http:") ||
|
|
g_str_has_prefix (url, "Http:") ||
|
|
g_str_has_prefix (url, "HTTP:"))
|
|
{
|
|
g_warning ("Http URL '%s' in theme stylesheet is not supported", url);
|
|
return NULL;
|
|
}
|
|
|
|
/* Assume anything else is a relative URL, and "resolve" it
|
|
*/
|
|
if (url[0] == '/')
|
|
return g_strdup (url);
|
|
|
|
base_filename = g_hash_table_lookup (theme->filenames_by_stylesheet, base_stylesheet);
|
|
|
|
if (base_filename == NULL)
|
|
{
|
|
g_warning ("Can't get base to resolve url '%s'", url);
|
|
return NULL;
|
|
}
|
|
|
|
dirname = g_path_get_dirname (base_filename);
|
|
filename = g_build_filename (dirname, url, NULL);
|
|
g_free (dirname);
|
|
|
|
return filename;
|
|
}
|