/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
 *
 * gimppropgui-eval.c
 * Copyright (C) 2017 Ell
 *
 * This program 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 3 of the License, or
 * (at your option) any later version.
 *
 * 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Less General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

/* This is a simple interpreter for the GUM language (the GEGL UI Meta
 * language), used in certain property keys of GEGL operations.  What follows
 * is a hand-wavy summary of the syntax and semantics (no BNF for you!)
 *
 * There are currently two types of expressions:
 *
 * Boolean expressions
 * -------------------
 *
 * There are three types of simple boolean expressions:
 *
 *   - Literal:  Either `0` or `1`, evaluating to FALSE and TRUE, respectively.
 *
 *   - Reference:  Has the form `$key` or `$property.key`.  Evaluates to the
 *     value of key `key`, which should itself be a boolean expression.  In
 *     the first form, `key` refers to a key of the same property to which the
 *     currently-evaluated key belongs.  In the second form, `key` refers to a
 *     key of `property`.
 *
 *   - Dependency:  Dependencies begin with the name of a property, on which
 *     the result depends.  Currently supported property types are:
 *
 *       - Boolean:  The expression consists of the property name alone, and
 *         its value is the value of the property.
 *
 *       - Enum:  The property name shall be followed by a brace-enclosed,
 *         comma-separated list of enum values, given as nicks.  The expression
 *         evaluates to TRUE iff the property matches any of the values.
 *
 * Complex boolean expressions can be formed using `!` (negation), `&`
 * (conjunction), `|` (disjunction), and parentheses (grouping), following the
 * usual precedence rules.
 *
 * String expressions
 * ------------------
 *
 * There are three types of simple string expressions:
 *
 *   - Literal:  A string literal, surrounded by single quotes (`'`).  Special
 *     characters (in particular, single quotes) can be escaped using a
 *     backslash (`\`).
 *
 *   - Reference:  Same as a boolean reference, but should refer to a key
 *     containing a string expression.
 *
 *   - Deferred literal:  Names a key, in the same fashion as a reference, but
 *     without the leading `$`.  The value of this key is taken literally as
 *     the value of the expression.  Deferred literals should usually be
 *     favored over inline string literals, because they can be translated
 *     independently of the expression.
 *
 * Currently, the only complex string expression is string selection:  It has
 * the form of a bracket-enclosed, comma-separated list of expressions of the
 * form `<condition> : <value>`, where `<condition>` is a boolean expression,
 * and `<value>` is a string expression.  The result of the expression is the
 * associated value of the first condition that evaluates to TRUE.  If no
 * condition is met, the result is NULL.
 *
 *
 * Whitespace separating subexpressions is insignificant.
 */

#include "config.h"

#include <string.h>

#include <gegl.h>
#include <gegl-paramspecs.h>
#include <gtk/gtk.h>

#include "propgui-types.h"

#include "gimppropgui-eval.h"


typedef enum
{
  GIMP_PROP_EVAL_FAILED  /* generic error condition */
} GimpPropEvalErrorCode;


static gboolean   gimp_prop_eval_boolean_impl     (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar    *key,
                                                   gint            default_value,
                                                   GError        **error,
                                                   gint            depth);
static gboolean   gimp_prop_eval_boolean_or       (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar   **expr,
                                                   gchar         **t,
                                                   GError        **error,
                                                   gint            depth);
static gboolean   gimp_prop_eval_boolean_and      (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar   **expr,
                                                   gchar         **t,
                                                   GError        **error,
                                                   gint            depth);
static gboolean   gimp_prop_eval_boolean_not      (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar   **expr,
                                                   gchar         **t,
                                                   GError        **error,
                                                   gint            depth);
static gboolean   gimp_prop_eval_boolean_group    (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar   **expr,
                                                   gchar         **t,
                                                   GError        **error,
                                                   gint            depth);
static gboolean   gimp_prop_eval_boolean_simple   (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar   **expr,
                                                   gchar         **t,
                                                   GError        **error,
                                                   gint            depth);

static gchar    * gimp_prop_eval_string_impl      (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar    *key,
                                                   const gchar    *default_value,
                                                   GError        **error,
                                                   gint            depth);
static gchar    * gimp_prop_eval_string_selection (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar   **expr,
                                                   gchar         **t,
                                                   GError        **error,
                                                   gint            depth);
static gchar    * gimp_prop_eval_string_simple    (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar   **expr,
                                                   gchar         **t,
                                                   GError        **error,
                                                   gint            depth);

static gboolean   gimp_prop_eval_parse_reference  (GObject        *config,
                                                   GParamSpec     *pspec,
                                                   const gchar   **expr,
                                                   gchar         **t,
                                                   GError        **error,
                                                   GParamSpec    **ref_pspec,
                                                   gchar         **ref_key);

static gboolean   gimp_prop_eval_depth_test       (gint            depth,
                                                   GError        **error);

static gchar    * gimp_prop_eval_read_token       (const gchar   **expr,
                                                   gchar         **t,
                                                   GError        **error);
static gboolean   gimp_prop_eval_is_name          (const gchar    *token);

#define GIMP_PROP_EVAL_ERROR (gimp_prop_eval_error_quark ())

static GQuark     gimp_prop_eval_error_quark      (void);


/*  public functions  */

gboolean
gimp_prop_eval_boolean (GObject     *config,
                        GParamSpec  *pspec,
                        const gchar *key,
                        gboolean     default_value)
{
  GError   *error = NULL;
  gboolean  result;

  result = gimp_prop_eval_boolean_impl (config, pspec,
                                        key, default_value, &error, 0);

  if (error)
    {
      g_warning ("in object of type '%s': %s",
                 G_OBJECT_TYPE_NAME (config),
                 error->message);

      g_error_free (error);

      return default_value;
    }

  return result;
}

gchar *
gimp_prop_eval_string (GObject     *config,
                       GParamSpec  *pspec,
                       const gchar *key,
                       const gchar *default_value)
{
  GError *error = NULL;
  gchar  *result;

  result = gimp_prop_eval_string_impl (config, pspec,
                                       key, default_value, &error, 0);

  if (error)
    {
      g_warning ("in object of type '%s': %s",
                 G_OBJECT_TYPE_NAME (config),
                 error->message);

      g_error_free (error);

      return g_strdup (default_value);
    }

  return result;
}


/*  private functions  */

static gboolean
gimp_prop_eval_boolean_impl (GObject      *config,
                             GParamSpec   *pspec,
                             const gchar  *key,
                             gint          default_value,
                             GError      **error,
                             gint          depth)
{
  const gchar *expr;
  gchar       *t      = NULL;
  gboolean     result = FALSE;

  if (! gimp_prop_eval_depth_test (depth, error))
    return FALSE;

  expr = gegl_param_spec_get_property_key (pspec, key);

  if (! expr)
    {
      /* we use `default_value < 0` to specify that the key must exist */
      if (default_value < 0)
        {
          g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                       "key '%s' of property '%s' not found",
                       key,
                       g_param_spec_get_name (pspec));

          return FALSE;
        }

      return default_value;
    }

  gimp_prop_eval_read_token (&expr, &t, error);

  if (! *error)
    {
      result = gimp_prop_eval_boolean_or (config, pspec,
                                          &expr, &t, error, depth);
    }

  /* check for trailing tokens at the end of the expression */
  if (! *error && t)
    {
      g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                   "invalid expression");
    }

  g_free (t);

  if (*error)
    {
      g_prefix_error (error,
                      "in key '%s' of property '%s': ",
                      key,
                      g_param_spec_get_name (pspec));

      return FALSE;
    }

  return result;
}

static gboolean
gimp_prop_eval_boolean_or (GObject      *config,
                           GParamSpec   *pspec,
                           const gchar **expr,
                           gchar       **t,
                           GError      **error,
                           gint          depth)
{
  gboolean result;

  if (! gimp_prop_eval_depth_test (depth, error))
    return FALSE;

  result = gimp_prop_eval_boolean_and (config, pspec,
                                       expr, t, error, depth);

  while (! *error && ! g_strcmp0 (*t, "|"))
    {
      gimp_prop_eval_read_token (expr, t, error);

      if (*error)
        return FALSE;

      /* keep evaluating even if `result` is TRUE, because we still need to
       * parse the rest of the subexpression.
       */
      result |= gimp_prop_eval_boolean_and (config, pspec,
                                            expr, t, error, depth);
    }

  return result;
}

static gboolean
gimp_prop_eval_boolean_and (GObject      *config,
                            GParamSpec   *pspec,
                            const gchar **expr,
                            gchar       **t,
                            GError      **error,
                            gint          depth)
{
  gboolean result;

  if (! gimp_prop_eval_depth_test (depth, error))
    return FALSE;

  result = gimp_prop_eval_boolean_not (config, pspec,
                                       expr, t, error, depth);

  while (! *error && ! g_strcmp0 (*t, "&"))
    {
      gimp_prop_eval_read_token (expr, t, error);

      if (*error)
        return FALSE;

      /* keep evaluating even if `result` is FALSE, because we still need to
       * parse the rest of the subexpression.
       */
      result &= gimp_prop_eval_boolean_not (config, pspec,
                                            expr, t, error, depth);
    }

  return result;
}

static gboolean
gimp_prop_eval_boolean_not (GObject      *config,
                            GParamSpec   *pspec,
                            const gchar **expr,
                            gchar       **t,
                            GError      **error,
                            gint          depth)
{
  if (! gimp_prop_eval_depth_test (depth, error))
    return FALSE;

  if (! g_strcmp0 (*t, "!"))
    {
      gimp_prop_eval_read_token (expr, t, error);

      if (*error)
        return FALSE;

      return ! gimp_prop_eval_boolean_not (config, pspec,
                                           expr, t, error, depth + 1);
    }

  return gimp_prop_eval_boolean_group (config, pspec,
                                       expr, t, error, depth);
}

static gboolean
gimp_prop_eval_boolean_group (GObject      *config,
                              GParamSpec   *pspec,
                              const gchar **expr,
                              gchar       **t,
                              GError      **error,
                              gint          depth)
{
  if (! gimp_prop_eval_depth_test (depth, error))
    return FALSE;

  if (! g_strcmp0 (*t, "("))
    {
      gboolean result;

      gimp_prop_eval_read_token (expr, t, error);

      if (*error)
        return FALSE;

      result = gimp_prop_eval_boolean_or (config, pspec,
                                          expr, t, error, depth + 1);

      if (*error)
        return FALSE;

      if (g_strcmp0 (*t, ")"))
        {
          g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                       "unterminated group");

          return FALSE;
        }

      gimp_prop_eval_read_token (expr, t, error);

      return result;
    }

  return gimp_prop_eval_boolean_simple (config, pspec,
                                        expr, t, error, depth);
}

static gboolean
gimp_prop_eval_boolean_simple (GObject      *config,
                               GParamSpec   *pspec,
                               const gchar **expr,
                               gchar       **t,
                               GError      **error,
                               gint          depth)
{
  gboolean result;

  if (! gimp_prop_eval_depth_test (depth, error))
    return FALSE;

  if (! *t)
    {
      g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                   "invalid expression");

      return FALSE;
    }

  /* literal */
  if (! strcmp (*t, "0"))
    {
      result = FALSE;

      gimp_prop_eval_read_token (expr, t, error);
    }
  else if (! strcmp (*t, "1"))
    {
      result = TRUE;

      gimp_prop_eval_read_token (expr, t, error);
    }
  /* reference */
  else if (! strcmp (*t, "$"))
    {
      gchar *key;

      gimp_prop_eval_read_token (expr, t, error);

      if (*error)
        return FALSE;

      if (! gimp_prop_eval_parse_reference (config, pspec,
                                            expr, t, error, &pspec, &key))
        return FALSE;

      result = gimp_prop_eval_boolean_impl (config, pspec,
                                            key, -1, error, depth + 1);

      g_free (key);
    }
  /* dependency */
  else if (gimp_prop_eval_is_name (*t))
    {
      const gchar *property_name;
      GParamSpec  *pspec;
      GType        type;

      property_name = *t;

      pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (config),
                                            property_name);

      if (! pspec)
        {
          g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                       "property '%s' not found",
                       property_name);

          return TRUE;
        }

      property_name = g_param_spec_get_name (pspec);
      type          = G_PARAM_SPEC_VALUE_TYPE (pspec);

      if (g_type_is_a (type, G_TYPE_BOOLEAN))
        {
          g_object_get (config, property_name, &result, NULL);
        }
      else if (g_type_is_a (type, G_TYPE_ENUM))
        {
          GEnumClass *enum_class;
          gint        value;

          gimp_prop_eval_read_token (expr, t, error);

          if (*error)
            return FALSE;

          if (g_strcmp0 (*t , "{"))
            {
              g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                           "missing enum value set "
                           "for property '%s'",
                           property_name);

              return FALSE;
            }

          enum_class = g_type_class_peek (type);

          g_object_get (config, property_name, &value, NULL);

          result = FALSE;

          while (gimp_prop_eval_read_token (expr, t, error) &&
                 gimp_prop_eval_is_name (*t))
            {
              const gchar *nick;
              GEnumValue  *enum_value;

              nick       = *t;
              enum_value = g_enum_get_value_by_nick (enum_class, nick);

              if (! enum_value)
                {
                  g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                               "invalid enum value '%s' "
                               "for property '%s'",
                               nick,
                               property_name);

                  return FALSE;
                }

              if (value == enum_value->value)
                result = TRUE;

              gimp_prop_eval_read_token (expr, t, error);

              if (*error)
                return FALSE;

              if (! g_strcmp0 (*t, ","))
                {
                  continue;
                }
              else if (! g_strcmp0 (*t, "}"))
                {
                  break;
                }
              else
                {
                  g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                               "invalid enum value set "
                               "for property '%s'",
                               property_name);

                  return FALSE;
                }
            }

          if (*error)
            return FALSE;

          if (g_strcmp0 (*t, "}"))
            {
              g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                           "unterminated enum value set "
                           "for property '%s'",
                           property_name);

              return FALSE;
            }
        }
      else
        {
          g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                       "invalid type "
                       "for property '%s'",
                       property_name);

          return FALSE;
        }

      gimp_prop_eval_read_token (expr, t, error);
    }
  else
    {
      g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                   "invalid expression");

      return FALSE;
    }

  return result;
}

static gchar *
gimp_prop_eval_string_impl (GObject      *config,
                            GParamSpec   *pspec,
                            const gchar  *key,
                            const gchar  *default_value,
                            GError      **error,
                            gint          depth)
{
  const gchar *expr;
  gchar       *t      = NULL;
  gchar       *result = NULL;

  if (! gimp_prop_eval_depth_test (depth, error))
    return NULL;

  expr = gegl_param_spec_get_property_key (pspec, key);

  if (! expr)
    return g_strdup (default_value);

  gimp_prop_eval_read_token (&expr, &t, error);

  if (! *error)
    {
      result = gimp_prop_eval_string_selection (config, pspec,
                                                &expr, &t, error, depth);
    }

  /* check for trailing tokens at the end of the expression */
  if (! *error && t)
    {
      g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                   "invalid expression");

      g_free (result);
    }

  g_free (t);

  if (*error)
    {
      g_prefix_error (error,
                      "in key '%s' of property '%s': ",
                      key,
                      g_param_spec_get_name (pspec));

      return NULL;
    }

  if (result)
    return result;
  else
    return g_strdup (default_value);
}

static gchar *
gimp_prop_eval_string_selection (GObject      *config,
                                 GParamSpec   *pspec,
                                 const gchar **expr,
                                 gchar       **t,
                                 GError      **error,
                                 gint          depth)
{
  if (! gimp_prop_eval_depth_test (depth, error) || ! t)
    return NULL;

  if (! g_strcmp0 (*t, "["))
    {
      gboolean  match  = FALSE;
      gchar    *result = NULL;

      if (! g_strcmp0 (gimp_prop_eval_read_token (expr, t, error), "]"))
        return NULL;

      while (! *error)
        {
          gboolean  cond;
          gchar    *value;

          cond = gimp_prop_eval_boolean_or (config, pspec,
                                            expr, t, error, depth + 1);

          if (*error)
            break;

          if (g_strcmp0 (*t, ":"))
            {
              g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                           "missing string selection value");

              break;
            }

          gimp_prop_eval_read_token (expr, t, error);

          if (*error)
            break;

          value = gimp_prop_eval_string_selection (config, pspec,
                                                   expr, t, error, depth + 1);

          if (*error)
            break;

          if (! match && cond)
            {
              match  = TRUE;
              result = value;
            }

          if (! g_strcmp0 (*t, ","))
            {
              gimp_prop_eval_read_token (expr, t, error);

              continue;
            }
          else if (! g_strcmp0 (*t, "]"))
            {
              gimp_prop_eval_read_token (expr, t, error);

              break;
            }
          else
            {
              if (*t)
                {
                  g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                               "invalid string selection");
                }
              else
                {
                  g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                               "unterminated string selection");
                }

              break;
            }
        }

      if (*error)
        {
          g_free (result);

          return NULL;
        }

      return result;
    }

  return gimp_prop_eval_string_simple (config,
                                       pspec, expr, t, error, depth);
}

static gchar *
gimp_prop_eval_string_simple (GObject      *config,
                              GParamSpec   *pspec,
                              const gchar **expr,
                              gchar       **t,
                              GError      **error,
                              gint          depth)
{
  gchar *result = NULL;

  if (! gimp_prop_eval_depth_test (depth, error))
    return NULL;

  /* literal */
  if (*t && **t == '\'')
    {
      gchar *escaped;

      escaped = g_strndup (*t + 1, strlen (*t + 1) - 1);

      result = g_strcompress (escaped);

      g_free (escaped);

      gimp_prop_eval_read_token (expr, t, error);

      if (*error)
        {
          g_free (result);

          return NULL;
        }
    }
  /* reference */
  else if (! g_strcmp0 (*t, "$"))
    {
      gchar *key;

      gimp_prop_eval_read_token (expr, t, error);

      if (*error)
        return NULL;

      if (! gimp_prop_eval_parse_reference (config, pspec,
                                            expr, t, error, &pspec, &key))
        return NULL;

      result = gimp_prop_eval_string_impl (config, pspec,
                                           key, NULL, error, depth + 1);

      g_free (key);
    }
  /* deferred literal */
  else if (gimp_prop_eval_is_name (*t))
    {
      GParamSpec  *str_pspec;
      gchar       *str_key;
      const gchar *str;

      if (! gimp_prop_eval_parse_reference (config, pspec,
                                            expr, t, error,
                                            &str_pspec, &str_key))
        return NULL;

      str = gegl_param_spec_get_property_key (str_pspec, str_key);

      if (! str)
        {
          g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                       "key '%s' of property '%s' not found",
                       str_key,
                       g_param_spec_get_name (str_pspec));

          g_free (str_key);

          return NULL;
        }

      g_free (str_key);

      result = g_strdup (str);
    }
  else
    {
      g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                   "invalid expression");

      return NULL;
    }

  return result;
}

static gboolean
gimp_prop_eval_parse_reference (GObject      *config,
                                GParamSpec   *pspec,
                                const gchar **expr,
                                gchar       **t,
                                GError      **error,
                                GParamSpec  **ref_pspec,
                                gchar       **ref_key)
{
  if (! gimp_prop_eval_is_name (*t))
    {
      g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                   "invalid reference");

      return FALSE;
    }

  *ref_pspec = pspec;
  *ref_key   = g_strdup (*t);

  gimp_prop_eval_read_token (expr, t, error);

  if (*error)
    {
      g_free (*ref_key);

      return FALSE;
    }

  if (! g_strcmp0 (*t, "."))
    {
      gchar *property_name;

      property_name = *ref_key;

      if (! gimp_prop_eval_read_token (expr, t, error) ||
          ! gimp_prop_eval_is_name (*t))
        {
          if (! *error)
            {
              g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                           "invalid reference");
            }

          g_free (property_name);

          return FALSE;
        }

      *ref_pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (config),
                                                 property_name);

      if (! *ref_pspec)
        {
          g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                       "property '%s' not found",
                       property_name);

          g_free (property_name);

          return FALSE;
        }

      g_free (property_name);

      *ref_key = g_strdup (*t);

      gimp_prop_eval_read_token (expr, t, error);

      if (*error)
        {
          g_free (*ref_key);

          return FALSE;
        }
    }

  return TRUE;
}

static gboolean
gimp_prop_eval_depth_test (gint     depth,
                           GError **error)
{
  /* make sure we don't recurse too deep.  in particular, guard against
   * circular references.
   */
  if (depth == 100)
    {
      g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                   "maximal nesting level exceeded");

      return FALSE;
    }

  return TRUE;
}

static gchar *
gimp_prop_eval_read_token (const gchar **expr,
                           gchar       **t,
                           GError      **error)
{
  const gchar *token;

  g_free (*t);
  *t = NULL;

  /* skip whitespace */
  while (g_ascii_isspace (**expr))
    ++*expr;

  token = *expr;

  if (*token == '\0')
    return NULL;

  /* name */
  if (gimp_prop_eval_is_name (token))
    {
      do { ++*expr; } while (g_ascii_isalnum (**expr) ||
                             **expr == '_'            ||
                             **expr == '-');
    }
  /* string literal */
  else if (token[0] == '\'')
    {
      for (++*expr; **expr != '\0' && **expr != '\''; ++*expr)
        {
          if (**expr == '\\')
            {
              ++*expr;

              if (**expr == '\0')
                break;
            }
        }

      if (**expr == '\0')
        {
          g_set_error (error, GIMP_PROP_EVAL_ERROR, GIMP_PROP_EVAL_FAILED,
                       "unterminated string literal");

          return NULL;
        }

      ++*expr;
    }
  /* punctuation or boolean literal */
  else
    {
      ++*expr;
    }

  *t = g_strndup (token, *expr - token);

  return *t;
}

static gboolean
gimp_prop_eval_is_name (const gchar *token)
{
  return token && (g_ascii_isalpha (*token) || *token == '_');
}

static GQuark
gimp_prop_eval_error_quark (void)
{
  return g_quark_from_static_string ("gimp-prop-eval-error-quark");
}