gtk2/gtk/gtkcssparser.c
Benjamin Otte fb47a8d714 css: Remove deprecated number-as-pixels compatibility
Previously, for compatibility with GTK 3.0, we allowed specifying
numbers without units and interpreted them as pixels, even when the CSS
specification didn't.

Remove that now that we can break API.
2017-01-18 04:13:56 +01:00

1061 lines
24 KiB
C

/* GTK - The GIMP Toolkit
* Copyright (C) 2011 Benjamin Otte <otte@gnome.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "gtkcssparserprivate.h"
#include "gtkcssdimensionvalueprivate.h"
#include <errno.h>
#include <string.h>
/* 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;
};
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;
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);
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);
}
void
_gtk_css_parser_error_full (GtkCssParser *parser,
GtkCssProviderError code,
const char *format,
...)
{
GError *error;
va_list args;
va_start (args, format);
error = g_error_new_valist (GTK_CSS_PROVIDER_ERROR,
code, 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)
{
g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), FALSE);
g_return_val_if_fail (string != NULL, FALSE);
if (g_ascii_strncasecmp (parser->data, string, strlen (string)) != 0)
return FALSE;
parser->data += strlen (string);
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;
}
char *
_gtk_css_parser_try_name (GtkCssParser *parser,
gboolean skip_whitespace)
{
GString *name;
g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), NULL);
name = g_string_new (NULL);
while (_gtk_css_parser_read_char (parser, name, NMCHAR))
;
if (skip_whitespace)
_gtk_css_parser_skip_whitespace (parser);
return g_string_free (name, FALSE);
}
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;
ident = g_string_new (NULL);
if (*parser->data == '-')
{
g_string_append_c (ident, '-');
parser->data++;
}
if (!_gtk_css_parser_read_char (parser, ident, NMSTART))
{
parser->data = start;
g_string_free (ident, TRUE);
return NULL;
}
while (_gtk_css_parser_read_char (parser, ident, NMCHAR))
;
if (skip_whitespace)
_gtk_css_parser_skip_whitespace (parser);
return g_string_free (ident, FALSE);
}
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++;
str = g_string_new (NULL);
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 g_string_free (str, FALSE);
}
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_free (str, TRUE);
return NULL;
default:
_gtk_css_parser_error (parser,
"Invalid character in string. Must be escaped.");
g_string_free (str, TRUE);
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_uint (GtkCssParser *parser,
guint *value)
{
guint64 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_strtoull (parser->data, &end, 10);
if (errno)
return FALSE;
if (result > G_MAXUINT)
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);
}
/* XXX: we should introduce GtkCssLenght that deals with
* different kind of units */
gboolean
_gtk_css_parser_try_length (GtkCssParser *parser,
int *value)
{
if (!_gtk_css_parser_try_int (parser, value))
return FALSE;
/* FIXME: _try_uint skips spaces while the
* spec forbids them
*/
_gtk_css_parser_try (parser, "px", TRUE);
return TRUE;
}
gboolean
_gtk_css_parser_try_enum (GtkCssParser *parser,
GType enum_type,
int *value)
{
GEnumClass *enum_class;
gboolean result;
const char *start;
char *str;
g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), FALSE);
g_return_val_if_fail (value != NULL, FALSE);
result = FALSE;
enum_class = g_type_class_ref (enum_type);
start = parser->data;
str = _gtk_css_parser_try_ident (parser, TRUE);
if (str == NULL)
return FALSE;
if (enum_class->n_values)
{
GEnumValue *enum_value;
for (enum_value = enum_class->values; enum_value->value_name; enum_value++)
{
if (enum_value->value_nick &&
g_ascii_strcasecmp (str, enum_value->value_nick) == 0)
{
*value = enum_value->value;
result = TRUE;
break;
}
}
}
g_free (str);
g_type_class_unref (enum_class);
if (!result)
parser->data = start;
return result;
}
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]) &&
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;
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 = 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_skip_whitespace (parser);
if (_gtk_css_parser_try (parser, "(", TRUE))
{
_gtk_css_parser_error_full (parser,
GTK_CSS_PROVIDER_ERROR_DEPRECATED,
"Whitespace between 'url' and '(' is deprecated");
}
else
{
_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);
}
char *
_gtk_css_parser_read_value (GtkCssParser *parser)
{
const char *start;
char *result;
g_return_val_if_fail (GTK_IS_CSS_PARSER (parser), NULL);
start = parser->data;
/* This needs to be done better */
gtk_css_parser_resync_internal (parser, TRUE, FALSE, '}');
result = g_strndup (start, parser->data - start);
if (result)
{
g_strchomp (result);
if (result[0] == 0)
{
g_free (result);
result = NULL;
}
}
if (result == NULL)
_gtk_css_parser_error (parser, "Expected a property value");
return result;
}
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, '"');
}