/* GTK - The GIMP Toolkit * Copyright (C) 2011 Benjamin Otte * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . */ #include "config.h" #include "gtkcssparserprivate.h" #include "gtkcssdimensionvalueprivate.h" #include #include /* just for the errors, yay! */ #include "gtkcssprovider.h" #define NEWLINE_CHARS "\r\n" #define WHITESPACE_CHARS "\f \t" #define NMSTART "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" #define NMCHAR NMSTART "01234567890-_" #define URLCHAR NMCHAR "!#$%&*~" #define GTK_IS_CSS_PARSER(parser) ((parser) != NULL) struct _GtkCssParser { const char *data; GFile *file; GtkCssParserErrorFunc error_func; gpointer user_data; const char *line_start; guint line; /* Use this for parsing identifiers, names and strings. */ GString *ident_str; }; GtkCssParser * _gtk_css_parser_new (const char *data, GFile *file, GtkCssParserErrorFunc error_func, gpointer user_data) { GtkCssParser *parser; g_return_val_if_fail (data != NULL, NULL); g_return_val_if_fail (file == NULL || G_IS_FILE (file), NULL); parser = g_slice_new0 (GtkCssParser); parser->data = data; if (file) parser->file = g_object_ref (file); parser->error_func = error_func; parser->user_data = user_data; parser->line_start = data; parser->line = 0; parser->ident_str = NULL; return parser; } void _gtk_css_parser_free (GtkCssParser *parser) { g_return_if_fail (GTK_IS_CSS_PARSER (parser)); if (parser->file) g_object_unref (parser->file); if (parser->ident_str) g_string_free (parser->ident_str, TRUE); g_slice_free (GtkCssParser, parser); } gboolean _gtk_css_parser_is_eof (GtkCssParser *parser) { g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), TRUE); return *parser->data == 0; } gboolean _gtk_css_parser_begins_with (GtkCssParser *parser, char c) { g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), TRUE); return *parser->data == c; } gboolean _gtk_css_parser_has_prefix (GtkCssParser *parser, const char *prefix) { g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), FALSE); return g_ascii_strncasecmp (parser->data, prefix, strlen (prefix)) == 0; } guint _gtk_css_parser_get_line (GtkCssParser *parser) { g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), 1); return parser->line; } guint _gtk_css_parser_get_position (GtkCssParser *parser) { g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), 1); return parser->data - parser->line_start; } static GFile * gtk_css_parser_get_base_file (GtkCssParser *parser) { GFile *base; if (parser->file) { base = g_file_get_parent (parser->file); } else { char *dir = g_get_current_dir (); base = g_file_new_for_path (dir); g_free (dir); } return base; } GFile * _gtk_css_parser_get_file_for_path (GtkCssParser *parser, const char *path) { GFile *base, *file; g_return_val_if_fail (parser != NULL, NULL); g_return_val_if_fail (path != NULL, NULL); base = gtk_css_parser_get_base_file (parser); file = g_file_resolve_relative_path (base, path); g_object_unref (base); return file; } GFile * _gtk_css_parser_get_file (GtkCssParser *parser) { g_return_val_if_fail (parser != NULL, NULL); return parser->file; } void _gtk_css_parser_take_error (GtkCssParser *parser, GError *error) { parser->error_func (parser, error, parser->user_data); g_error_free (error); } void _gtk_css_parser_error (GtkCssParser *parser, const char *format, ...) { GError *error; va_list args; va_start (args, format); error = g_error_new_valist (GTK_CSS_PROVIDER_ERROR, GTK_CSS_PROVIDER_ERROR_SYNTAX, format, args); va_end (args); _gtk_css_parser_take_error (parser, error); } static gboolean gtk_css_parser_new_line (GtkCssParser *parser) { gboolean result = FALSE; if (*parser->data == '\r') { result = TRUE; parser->data++; } if (*parser->data == '\n') { result = TRUE; parser->data++; } if (result) { parser->line++; parser->line_start = parser->data; } return result; } static gboolean gtk_css_parser_skip_comment (GtkCssParser *parser) { if (parser->data[0] != '/' || parser->data[1] != '*') return FALSE; parser->data += 2; while (*parser->data) { gsize len = strcspn (parser->data, NEWLINE_CHARS "/"); parser->data += len; if (gtk_css_parser_new_line (parser)) continue; parser->data++; if (len > 0 && parser->data[-2] == '*') return TRUE; if (parser->data[0] == '*') _gtk_css_parser_error (parser, "'/*' in comment block"); } /* FIXME: position */ _gtk_css_parser_error (parser, "Unterminated comment"); return TRUE; } void _gtk_css_parser_skip_whitespace (GtkCssParser *parser) { size_t len; while (*parser->data) { if (gtk_css_parser_new_line (parser)) continue; len = strspn (parser->data, WHITESPACE_CHARS); if (len) { parser->data += len; continue; } if (!gtk_css_parser_skip_comment (parser)) break; } } gboolean _gtk_css_parser_try (GtkCssParser *parser, const char *string, gboolean skip_whitespace) { gsize string_len; g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), FALSE); g_return_val_if_fail (string != NULL, FALSE); string_len = strlen (string); if (g_ascii_strncasecmp (parser->data, string, string_len) != 0) return FALSE; parser->data += string_len; if (skip_whitespace) _gtk_css_parser_skip_whitespace (parser); return TRUE; } static guint get_xdigit (char c) { if (c >= 'a') return c - 'a' + 10; else if (c >= 'A') return c - 'A' + 10; else return c - '0'; } static void _gtk_css_parser_unescape (GtkCssParser *parser, GString *str) { guint i; gunichar result = 0; g_assert (*parser->data == '\\'); parser->data++; for (i = 0; i < 6; i++) { if (!g_ascii_isxdigit (parser->data[i])) break; result = (result << 4) + get_xdigit (parser->data[i]); } if (i != 0) { g_string_append_unichar (str, result); parser->data += i; /* NB: gtk_css_parser_new_line() forward data pointer itself */ if (!gtk_css_parser_new_line (parser) && *parser->data && strchr (WHITESPACE_CHARS, *parser->data)) parser->data++; return; } if (gtk_css_parser_new_line (parser)) return; g_string_append_c (str, *parser->data); parser->data++; return; } static gboolean _gtk_css_parser_read_char (GtkCssParser *parser, GString * str, const char * allowed) { if (*parser->data == 0) return FALSE; if (strchr (allowed, *parser->data)) { g_string_append_c (str, *parser->data); parser->data++; return TRUE; } if (*parser->data >= 127) { gsize len = g_utf8_skip[(guint) *(guchar *) parser->data]; g_string_append_len (str, parser->data, len); parser->data += len; return TRUE; } if (*parser->data == '\\') { _gtk_css_parser_unescape (parser, str); return TRUE; } return FALSE; } static char * _gtk_css_parser_get_ident (GtkCssParser *parser) { char *result; gsize len; len = parser->ident_str->len; result = g_new (char, len + 1); memcpy (result, parser->ident_str->str, len + 1); g_string_set_size (parser->ident_str, 0); return result; } char * _gtk_css_parser_try_name (GtkCssParser *parser, gboolean skip_whitespace) { GString *name; g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), NULL); if (parser->ident_str == NULL) parser->ident_str = g_string_new (NULL); name = parser->ident_str; while (_gtk_css_parser_read_char (parser, name, NMCHAR)) ; if (skip_whitespace) _gtk_css_parser_skip_whitespace (parser); return _gtk_css_parser_get_ident (parser); } char * _gtk_css_parser_try_ident (GtkCssParser *parser, gboolean skip_whitespace) { const char *start; GString *ident; g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), NULL); start = parser->data; if (parser->ident_str == NULL) parser->ident_str = g_string_new (NULL); ident = parser->ident_str; if (*parser->data == '-') { g_string_append_c (ident, '-'); parser->data++; } if (!_gtk_css_parser_read_char (parser, ident, NMSTART)) { parser->data = start; g_string_set_size (ident, 0); return NULL; } while (_gtk_css_parser_read_char (parser, ident, NMCHAR)) ; if (skip_whitespace) _gtk_css_parser_skip_whitespace (parser); return _gtk_css_parser_get_ident (parser); } gboolean _gtk_css_parser_is_string (GtkCssParser *parser) { g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), FALSE); return *parser->data == '"' || *parser->data == '\''; } char * _gtk_css_parser_read_string (GtkCssParser *parser) { GString *str; char quote; g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), NULL); quote = *parser->data; if (quote != '"' && quote != '\'') { _gtk_css_parser_error (parser, "Expected a string."); return NULL; } parser->data++; if (parser->ident_str == NULL) parser->ident_str = g_string_new (NULL); str = parser->ident_str; g_assert (str->len == 0); while (TRUE) { gsize len = strcspn (parser->data, "\\'\"\n\r\f"); g_string_append_len (str, parser->data, len); parser->data += len; switch (*parser->data) { case '\\': _gtk_css_parser_unescape (parser, str); break; case '"': case '\'': if (*parser->data == quote) { parser->data++; _gtk_css_parser_skip_whitespace (parser); return _gtk_css_parser_get_ident (parser); } g_string_append_c (str, *parser->data); parser->data++; break; case '\0': /* FIXME: position */ _gtk_css_parser_error (parser, "Missing end quote in string."); g_string_set_size (str, 0); return NULL; default: _gtk_css_parser_error (parser, "Invalid character in string. Must be escaped."); g_string_set_size (str, 0); return NULL; } } g_assert_not_reached (); return NULL; } gboolean _gtk_css_parser_try_int (GtkCssParser *parser, int *value) { gint64 result; char *end; g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), FALSE); g_return_val_if_fail (value != NULL, FALSE); /* strtoll parses a plus, but we are not allowed to */ if (*parser->data == '+') return FALSE; errno = 0; result = g_ascii_strtoll (parser->data, &end, 10); if (errno) return FALSE; if (result > G_MAXINT || result < G_MININT) return FALSE; if (parser->data == end) return FALSE; parser->data = end; *value = result; _gtk_css_parser_skip_whitespace (parser); return TRUE; } gboolean _gtk_css_parser_try_double (GtkCssParser *parser, gdouble *value) { gdouble result; char *end; g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), FALSE); g_return_val_if_fail (value != NULL, FALSE); errno = 0; result = g_ascii_strtod (parser->data, &end); if (errno) return FALSE; if (parser->data == end) return FALSE; parser->data = end; *value = result; _gtk_css_parser_skip_whitespace (parser); return TRUE; } gboolean _gtk_css_parser_has_number (GtkCssParser *parser) { char c; if (parser->data[0] == '-' || parser->data[0] == '+') c = parser->data[1]; else c = parser->data[0]; /* ahem */ return g_ascii_isdigit (c) || c == '.'; } GtkCssValue * gtk_css_dimension_value_parse (GtkCssParser *parser, GtkCssNumberParseFlags flags) { static const struct { const char *name; GtkCssUnit unit; GtkCssNumberParseFlags required_flags; } units[] = { { "px", GTK_CSS_PX, GTK_CSS_PARSE_LENGTH }, { "pt", GTK_CSS_PT, GTK_CSS_PARSE_LENGTH }, { "em", GTK_CSS_EM, GTK_CSS_PARSE_LENGTH }, { "ex", GTK_CSS_EX, GTK_CSS_PARSE_LENGTH }, { "rem", GTK_CSS_REM, GTK_CSS_PARSE_LENGTH }, { "pc", GTK_CSS_PC, GTK_CSS_PARSE_LENGTH }, { "in", GTK_CSS_IN, GTK_CSS_PARSE_LENGTH }, { "cm", GTK_CSS_CM, GTK_CSS_PARSE_LENGTH }, { "mm", GTK_CSS_MM, GTK_CSS_PARSE_LENGTH }, { "rad", GTK_CSS_RAD, GTK_CSS_PARSE_ANGLE }, { "deg", GTK_CSS_DEG, GTK_CSS_PARSE_ANGLE }, { "grad", GTK_CSS_GRAD, GTK_CSS_PARSE_ANGLE }, { "turn", GTK_CSS_TURN, GTK_CSS_PARSE_ANGLE }, { "s", GTK_CSS_S, GTK_CSS_PARSE_TIME }, { "ms", GTK_CSS_MS, GTK_CSS_PARSE_TIME } }; char *end, *unit_name; double value; GtkCssUnit unit; g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), NULL); errno = 0; value = g_ascii_strtod (parser->data, &end); if (errno) { _gtk_css_parser_error (parser, "not a number: %s", g_strerror (errno)); return NULL; } if (parser->data == end) { _gtk_css_parser_error (parser, "not a number"); return NULL; } parser->data = end; if (flags & GTK_CSS_POSITIVE_ONLY && value < 0) { _gtk_css_parser_error (parser, "negative values are not allowed."); return NULL; } unit_name = _gtk_css_parser_try_ident (parser, FALSE); if (unit_name) { guint i; for (i = 0; i < G_N_ELEMENTS (units); i++) { if (flags & units[i].required_flags && g_ascii_strcasecmp (unit_name, units[i].name) == 0) break; } if (i >= G_N_ELEMENTS (units)) { _gtk_css_parser_error (parser, "'%s' is not a valid unit.", unit_name); g_free (unit_name); return NULL; } unit = units[i].unit; g_free (unit_name); } else { if ((flags & GTK_CSS_PARSE_PERCENT) && _gtk_css_parser_try (parser, "%", FALSE)) { unit = GTK_CSS_PERCENT; } else if (value == 0.0) { if (flags & GTK_CSS_PARSE_NUMBER) unit = GTK_CSS_NUMBER; else if (flags & GTK_CSS_PARSE_LENGTH) unit = GTK_CSS_PX; else if (flags & GTK_CSS_PARSE_ANGLE) unit = GTK_CSS_DEG; else if (flags & GTK_CSS_PARSE_TIME) unit = GTK_CSS_S; else unit = GTK_CSS_PERCENT; } else if (flags & GTK_CSS_PARSE_NUMBER) { unit = GTK_CSS_NUMBER; } else { _gtk_css_parser_error (parser, "Unit is missing."); return NULL; } } _gtk_css_parser_skip_whitespace (parser); return gtk_css_dimension_value_new (value, unit); } gboolean _gtk_css_parser_try_hash_color (GtkCssParser *parser, GdkRGBA *rgba) { if (parser->data[0] == '#' && g_ascii_isxdigit (parser->data[1]) && g_ascii_isxdigit (parser->data[2]) && g_ascii_isxdigit (parser->data[3])) { if (g_ascii_isxdigit (parser->data[4])) { if (g_ascii_isxdigit (parser->data[5]) && g_ascii_isxdigit (parser->data[6])) { rgba->red = ((get_xdigit (parser->data[1]) << 4) + get_xdigit (parser->data[2])) / 255.0; rgba->green = ((get_xdigit (parser->data[3]) << 4) + get_xdigit (parser->data[4])) / 255.0; rgba->blue = ((get_xdigit (parser->data[5]) << 4) + get_xdigit (parser->data[6])) / 255.0; if (g_ascii_isxdigit (parser->data[7]) && g_ascii_isxdigit (parser->data[8])) { rgba->alpha = ((get_xdigit (parser->data[7]) << 4) + get_xdigit (parser->data[8])) / 255.0; parser->data += 9; } else { rgba->alpha = 1.0; parser->data += 7; } } else { rgba->red = get_xdigit (parser->data[1]) / 15.0; rgba->green = get_xdigit (parser->data[2]) / 15.0; rgba->blue = get_xdigit (parser->data[3]) / 15.0; rgba->alpha = get_xdigit (parser->data[4]) / 15.0; parser->data += 5; } } else { rgba->red = get_xdigit (parser->data[1]) / 15.0; rgba->green = get_xdigit (parser->data[2]) / 15.0; rgba->blue = get_xdigit (parser->data[3]) / 15.0; rgba->alpha = 1.0; parser->data += 4; } _gtk_css_parser_skip_whitespace (parser); return TRUE; } return FALSE; } GFile * _gtk_css_parser_read_url (GtkCssParser *parser) { gchar *path; char *scheme; GFile *file; if (_gtk_css_parser_try (parser, "url", FALSE)) { if (!_gtk_css_parser_try (parser, "(", TRUE)) { _gtk_css_parser_error (parser, "Expected '(' after 'url'"); return NULL; } path = _gtk_css_parser_read_string (parser); if (path == NULL) return NULL; if (!_gtk_css_parser_try (parser, ")", TRUE)) { _gtk_css_parser_error (parser, "No closing ')' found for 'url'"); g_free (path); return NULL; } scheme = g_uri_parse_scheme (path); if (scheme != NULL) { file = g_file_new_for_uri (path); g_free (path); g_free (scheme); return file; } } else { path = _gtk_css_parser_try_name (parser, TRUE); if (path == NULL) { _gtk_css_parser_error (parser, "Not a valid url"); return NULL; } } file = _gtk_css_parser_get_file_for_path (parser, path); g_free (path); return file; } static void gtk_css_parser_resync_internal (GtkCssParser *parser, gboolean sync_at_semicolon, gboolean read_sync_token, char terminator) { gsize len; do { len = strcspn (parser->data, "\\\"'/()[]{};" NEWLINE_CHARS); parser->data += len; if (gtk_css_parser_new_line (parser)) continue; if (_gtk_css_parser_is_string (parser)) { /* Hrm, this emits errors, and i suspect it shouldn't... */ char *free_me = _gtk_css_parser_read_string (parser); g_free (free_me); continue; } if (gtk_css_parser_skip_comment (parser)) continue; switch (*parser->data) { case '\\': { GString *ignore = g_string_new (NULL); _gtk_css_parser_unescape (parser, ignore); g_string_free (ignore, TRUE); } break; case ';': if (sync_at_semicolon && !read_sync_token) return; parser->data++; if (sync_at_semicolon) { _gtk_css_parser_skip_whitespace (parser); return; } break; case '(': parser->data++; _gtk_css_parser_resync (parser, FALSE, ')'); if (*parser->data) parser->data++; break; case '[': parser->data++; _gtk_css_parser_resync (parser, FALSE, ']'); if (*parser->data) parser->data++; break; case '{': parser->data++; _gtk_css_parser_resync (parser, FALSE, '}'); if (*parser->data) parser->data++; if (sync_at_semicolon || !terminator) { _gtk_css_parser_skip_whitespace (parser); return; } break; case '}': case ')': case ']': if (terminator == *parser->data) { _gtk_css_parser_skip_whitespace (parser); return; } parser->data++; continue; case '\0': break; case '/': default: parser->data++; break; } } while (*parser->data); } void _gtk_css_parser_resync (GtkCssParser *parser, gboolean sync_at_semicolon, char terminator) { g_return_if_fail (GTK_IS_CSS_PARSER (parser)); gtk_css_parser_resync_internal (parser, sync_at_semicolon, TRUE, terminator); } void _gtk_css_print_string (GString *str, const char *string) { gsize len; g_return_if_fail (str != NULL); g_return_if_fail (string != NULL); g_string_append_c (str, '"'); do { len = strcspn (string, "\\\"\n\r\f"); g_string_append_len (str, string, len); string += len; switch (*string) { case '\0': goto out; case '\n': g_string_append (str, "\\A "); break; case '\r': g_string_append (str, "\\D "); break; case '\f': g_string_append (str, "\\C "); break; case '\"': g_string_append (str, "\\\""); break; case '\\': g_string_append (str, "\\\\"); break; default: g_assert_not_reached (); break; } string++; } while (*string); out: g_string_append_c (str, '"'); }