/* 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)
{
  /* ahem */
  return strchr ("+-0123456789.", parser->data[0]) != NULL;
}

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_NUMBER_AS_PIXELS)
        {
          _gtk_css_parser_error_full (parser,
                                      GTK_CSS_PROVIDER_ERROR_DEPRECATED,
                                      "Not using units is deprecated. Assuming 'px'.");
          unit = GTK_CSS_PX;
        }
      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, '"');
}

