Blob Blame History Raw
/* Pango
 * pango-markup.c: Parse markup into attributed text
 *
 * Copyright (C) 2000 Red Hat Software
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include "config.h"
#include <string.h>
#include <stdlib.h>
#include <errno.h>

#include "pango-attributes.h"
#include "pango-font.h"
#include "pango-enum-types.h"
#include "pango-impl-utils.h"
#include "pango-utils-internal.h"

/* FIXME */
#define _(x) x

/* CSS size levels */
typedef enum
{
  XXSmall = -3,
  XSmall = -2,
  Small = -1,
  Medium = 0,
  Large = 1,
  XLarge = 2,
  XXLarge = 3
} SizeLevel;

typedef struct _MarkupData MarkupData;

struct _MarkupData
{
  PangoAttrList *attr_list;
  GString *text;
  GSList *tag_stack;
  gsize index;
  GSList *to_apply;
  gunichar accel_marker;
  gunichar accel_char;
};

typedef struct _OpenTag OpenTag;

struct _OpenTag
{
  GSList *attrs;
  gsize start_index;
  /* Current total scale level; reset whenever
   * an absolute size is set.
   * Each "larger" ups it 1, each "smaller" decrements it 1
   */
  gint scale_level;
  /* Our impact on scale_level, so we know whether we
   * need to create an attribute ourselves on close
   */
  gint scale_level_delta;
  /* Base scale factor currently in effect
   * or size that this tag
   * forces, or parent's scale factor or size.
   */
  double base_scale_factor;
  int base_font_size;
  guint has_base_font_size : 1;
};

typedef gboolean (*TagParseFunc) (MarkupData            *md,
				  OpenTag               *tag,
				  const gchar          **names,
				  const gchar          **values,
				  GMarkupParseContext   *context,
				  GError               **error);

static gboolean b_parse_func        (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean big_parse_func      (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean span_parse_func     (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean i_parse_func        (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean markup_parse_func   (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean s_parse_func        (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean sub_parse_func      (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean sup_parse_func      (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean small_parse_func    (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean tt_parse_func       (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);
static gboolean u_parse_func        (MarkupData           *md,
				     OpenTag              *tag,
				     const gchar         **names,
				     const gchar         **values,
				     GMarkupParseContext  *context,
				     GError              **error);

static double
scale_factor (int scale_level, double base)
{
  double factor = base;
  int i;

  /* 1.2 is the CSS scale factor between sizes */

  if (scale_level > 0)
    {
      i = 0;
      while (i < scale_level)
	{
	  factor *= 1.2;

	  ++i;
	}
    }
  else if (scale_level < 0)
    {
      i = scale_level;
      while (i < 0)
	{
	  factor /= 1.2;

	  ++i;
	}
    }

  return factor;
}

static void
open_tag_free (OpenTag *ot)
{
  g_slist_foreach (ot->attrs, (GFunc) pango_attribute_destroy, NULL);
  g_slist_free (ot->attrs);
  g_slice_free (OpenTag, ot);
}

static void
open_tag_set_absolute_font_size (OpenTag *ot,
				 int      font_size)
{
  ot->base_font_size = font_size;
  ot->has_base_font_size = TRUE;
  ot->scale_level = 0;
  ot->scale_level_delta = 0;
}

static void
open_tag_set_absolute_font_scale (OpenTag *ot,
				  double   scale)
{
  ot->base_scale_factor = scale;
  ot->has_base_font_size = FALSE;
  ot->scale_level = 0;
  ot->scale_level_delta = 0;
}

static OpenTag*
markup_data_open_tag (MarkupData   *md)
{
  OpenTag *ot;
  OpenTag *parent = NULL;

  if (md->attr_list == NULL)
    return NULL;

  if (md->tag_stack)
    parent = md->tag_stack->data;

  ot = g_slice_new (OpenTag);
  ot->attrs = NULL;
  ot->start_index = md->index;
  ot->scale_level_delta = 0;

  if (parent == NULL)
    {
      ot->base_scale_factor = 1.0;
      ot->base_font_size = 0;
      ot->has_base_font_size = FALSE;
      ot->scale_level = 0;
    }
  else
    {
      ot->base_scale_factor = parent->base_scale_factor;
      ot->base_font_size = parent->base_font_size;
      ot->has_base_font_size = parent->has_base_font_size;
      ot->scale_level = parent->scale_level;
    }

  md->tag_stack = g_slist_prepend (md->tag_stack, ot);

  return ot;
}

static void
markup_data_close_tag (MarkupData *md)
{
  OpenTag *ot;
  GSList *tmp_list;

  if (md->attr_list == NULL)
    return;

  /* pop the stack */
  ot = md->tag_stack->data;
  md->tag_stack = g_slist_delete_link (md->tag_stack,
				       md->tag_stack);

  /* Adjust end indexes, and push each attr onto the front of the
   * to_apply list. This means that outermost tags are on the front of
   * that list; if we apply the list in order, then the innermost
   * tags will "win" which is correct.
   */
  tmp_list = ot->attrs;
  while (tmp_list != NULL)
    {
      PangoAttribute *a = tmp_list->data;

      a->start_index = ot->start_index;
      a->end_index = md->index;

      md->to_apply = g_slist_prepend (md->to_apply, a);

      tmp_list = g_slist_next (tmp_list);
    }

  if (ot->scale_level_delta != 0)
    {
      /* We affected relative font size; create an appropriate
       * attribute and reverse our effects on the current level
       */
      PangoAttribute *a;

      if (ot->has_base_font_size)
	{
	  /* Create a font using the absolute point size
	   * as the base size to be scaled from
	   */
	  a = pango_attr_size_new (scale_factor (ot->scale_level,
						 1.0) *
				   ot->base_font_size);
	}
      else
	{
	  /* Create a font using the current scale factor
	   * as the base size to be scaled from
	   */
	  a = pango_attr_scale_new (scale_factor (ot->scale_level,
						  ot->base_scale_factor));
	}

      a->start_index = ot->start_index;
      a->end_index = md->index;

      md->to_apply = g_slist_prepend (md->to_apply, a);
    }

  g_slist_free (ot->attrs);
  g_slice_free (OpenTag, ot);
}

static void
start_element_handler  (GMarkupParseContext *context,
			const gchar         *element_name,
			const gchar        **attribute_names,
			const gchar        **attribute_values,
			gpointer             user_data,
			GError             **error)
{
  TagParseFunc parse_func = NULL;
  OpenTag *ot;

  switch (*element_name)
    {
    case 'b':
      if (strcmp ("b", element_name) == 0)
	parse_func = b_parse_func;
      else if (strcmp ("big", element_name) == 0)
	parse_func = big_parse_func;
      break;

    case 'i':
      if (strcmp ("i", element_name) == 0)
	parse_func = i_parse_func;
      break;

    case 'm':
      if (strcmp ("markup", element_name) == 0)
	parse_func = markup_parse_func;
      break;

    case 's':
      if (strcmp ("span", element_name) == 0)
	parse_func = span_parse_func;
      else if (strcmp ("s", element_name) == 0)
	parse_func = s_parse_func;
      else if (strcmp ("sub", element_name) == 0)
	parse_func = sub_parse_func;
      else if (strcmp ("sup", element_name) == 0)
	parse_func = sup_parse_func;
      else if (strcmp ("small", element_name) == 0)
	parse_func = small_parse_func;
      break;

    case 't':
      if (strcmp ("tt", element_name) == 0)
	parse_func = tt_parse_func;
      break;

    case 'u':
      if (strcmp ("u", element_name) == 0)
	parse_func = u_parse_func;
      break;
    }

  if (parse_func == NULL)
    {
      gint line_number, char_number;

      g_markup_parse_context_get_position (context,
					   &line_number, &char_number);

      g_set_error (error,
		   G_MARKUP_ERROR,
		   G_MARKUP_ERROR_UNKNOWN_ELEMENT,
		   _("Unknown tag '%s' on line %d char %d"),
		   element_name,
		   line_number, char_number);

      return;
    }

  ot = markup_data_open_tag (user_data);

  /* note ot may be NULL if the user didn't want the attribute list */

  if (!(*parse_func) (user_data, ot,
		      attribute_names, attribute_values,
		      context, error))
    {
      /* there's nothing to do; we return an error, and end up
       * freeing ot off the tag stack later.
       */
    }
}

static void
end_element_handler    (GMarkupParseContext *context G_GNUC_UNUSED,
			const gchar         *element_name G_GNUC_UNUSED,
			gpointer             user_data,
			GError             **error G_GNUC_UNUSED)
{
  markup_data_close_tag (user_data);
}

static void
text_handler           (GMarkupParseContext *context G_GNUC_UNUSED,
			const gchar         *text,
			gsize                text_len,
			gpointer             user_data,
			GError             **error G_GNUC_UNUSED)
{
  MarkupData *md = user_data;

  if (md->accel_marker == 0)
    {
      /* Just append all the text */

      md->index += text_len;

      g_string_append_len (md->text, text, text_len);
    }
  else
    {
      /* Parse the accelerator */
      const gchar *p;
      const gchar *end;
      const gchar *range_start;
      const gchar *range_end;
      gssize uline_index = -1;
      gsize uline_len = 0;	/* Quiet GCC */

      range_end = NULL;
      range_start = text;
      p = text;
      end = text + text_len;

      while (p != end)
	{
	  gunichar c;

	  c = g_utf8_get_char (p);

	  if (range_end)
	    {
	      if (c == md->accel_marker)
		{
		  /* escaped accel marker; move range_end
		   * past the accel marker that came before,
		   * append the whole thing
		   */
		  range_end = g_utf8_next_char (range_end);
		  g_string_append_len (md->text,
				       range_start,
				       range_end - range_start);
		  md->index += range_end - range_start;

		  /* set next range_start, skipping accel marker */
		  range_start = g_utf8_next_char (p);
		}
	      else
		{
		  /* Don't append the accel marker (leave range_end
		   * alone); set the accel char to c; record location for
		   * underline attribute
		   */
		  if (md->accel_char == 0)
		    md->accel_char = c;

		  g_string_append_len (md->text,
				       range_start,
				       range_end - range_start);
		  md->index += range_end - range_start;

		  /* The underline should go underneath the char
		   * we're setting as the next range_start
		   */
		  uline_index = md->index;
		  uline_len = g_utf8_next_char (p) - p;

		  /* set next range_start to include this char */
		  range_start = p;
		}

	      /* reset range_end */
	      range_end = NULL;
	    }
	  else if (c == md->accel_marker)
	    {
	      range_end = p;
	    }

	  p = g_utf8_next_char (p);
	}

      if (range_end)
	{
	  g_string_append_len (md->text,
			       range_start,
			       range_end - range_start);
	  md->index += range_end - range_start;
	}
      else
	{
	  g_string_append_len (md->text,
			       range_start,
			       end - range_start);
	  md->index += end - range_start;
	}

      if (md->attr_list != NULL && uline_index >= 0)
	{
	  /* Add the underline indicating the accelerator */
	  PangoAttribute *attr;

	  attr = pango_attr_underline_new (PANGO_UNDERLINE_LOW);

	  attr->start_index = uline_index;
	  attr->end_index = uline_index + uline_len;

	  pango_attr_list_change (md->attr_list, attr);
	}
    }
}

static gboolean
xml_isspace (char c)
{
  return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}

static const GMarkupParser pango_markup_parser = {
  start_element_handler,
  end_element_handler,
  text_handler,
  NULL,
  NULL
};

static void
destroy_markup_data (MarkupData *md)
{
  g_slist_free_full (md->tag_stack, (GDestroyNotify) open_tag_free);
  g_slist_free_full (md->to_apply, (GDestroyNotify) pango_attribute_destroy);
  if (md->text)
      g_string_free (md->text, TRUE);

  if (md->attr_list)
    pango_attr_list_unref (md->attr_list);

  g_slice_free (MarkupData, md);
}

static GMarkupParseContext *
pango_markup_parser_new_internal (char       accel_marker,
				  GError   **error,
				  gboolean   want_attr_list)
{
  MarkupData *md;
  GMarkupParseContext *context;

  md = g_slice_new (MarkupData);

  /* Don't bother creating these if they weren't requested;
   * might be useful e.g. if you just want to validate
   * some markup.
   */
  if (want_attr_list)
    md->attr_list = pango_attr_list_new ();
  else
    md->attr_list = NULL;

  md->text = g_string_new (NULL);

  md->accel_marker = accel_marker;
  md->accel_char = 0;

  md->index = 0;
  md->tag_stack = NULL;
  md->to_apply = NULL;

  context = g_markup_parse_context_new (&pango_markup_parser,
					0, md,
                                        (GDestroyNotify)destroy_markup_data);

  if (!g_markup_parse_context_parse (context,
                                     "<markup>",
                                     -1,
                                     error))
    goto error;

  return context;

 error:
  g_markup_parse_context_free (context);
  return NULL;
}

/**
 * pango_parse_markup:
 * @markup_text: markup to parse (see <link linkend="PangoMarkupFormat">markup format</link>)
 * @length: length of @markup_text, or -1 if nul-terminated
 * @accel_marker: character that precedes an accelerator, or 0 for none
 * @attr_list: (out) (allow-none): address of return location for a #PangoAttrList, or %NULL
 * @text: (out) (allow-none): address of return location for text with tags stripped, or %NULL
 * @accel_char: (out) (allow-none): address of return location for accelerator char, or %NULL
 * @error: address of return location for errors, or %NULL
 *
 * Parses marked-up text (see
 * <link linkend="PangoMarkupFormat">markup format</link>) to create
 * a plain-text string and an attribute list.
 *
 * If @accel_marker is nonzero, the given character will mark the
 * character following it as an accelerator. For example, @accel_marker
 * might be an ampersand or underscore. All characters marked
 * as an accelerator will receive a %PANGO_UNDERLINE_LOW attribute,
 * and the first character so marked will be returned in @accel_char.
 * Two @accel_marker characters following each other produce a single
 * literal @accel_marker character.
 *
 * To parse a stream of pango markup incrementally, use pango_markup_parser_new().
 *
 * If any error happens, none of the output arguments are touched except
 * for @error.
 *
 * Return value: %FALSE if @error is set, otherwise %TRUE
 **/
gboolean
pango_parse_markup (const char                 *markup_text,
		    int                         length,
		    gunichar                    accel_marker,
		    PangoAttrList             **attr_list,
		    char                      **text,
		    gunichar                   *accel_char,
		    GError                    **error)
{
  GMarkupParseContext *context = NULL;
  gboolean ret = FALSE;
  const char *p;
  const char *end;

  g_return_val_if_fail (markup_text != NULL, FALSE);

  if (length < 0)
    length = strlen (markup_text);

  p = markup_text;
  end = markup_text + length;
  while (p != end && xml_isspace (*p))
    ++p;

  context = pango_markup_parser_new_internal (accel_marker,
                                              error,
                                              (attr_list != NULL));
  if (context == NULL)
    goto out;

  if (!g_markup_parse_context_parse (context,
                                     markup_text,
                                     length,
                                     error))
    goto out;

  if (!pango_markup_parser_finish (context,
                                   attr_list,
                                   text,
                                   accel_char,
                                   error))
    goto out;

  ret = TRUE;

 out:
  if (context != NULL)
    g_markup_parse_context_free (context);
  return ret;
}

/**
 * pango_markup_parser_new:
 * @accel_marker: character that precedes an accelerator, or 0 for none
 *
 * Parses marked-up text (see
 * <link linkend="PangoMarkupFormat">markup format</link>) to create
 * a plain-text string and an attribute list.
 *
 * If @accel_marker is nonzero, the given character will mark the
 * character following it as an accelerator. For example, @accel_marker
 * might be an ampersand or underscore. All characters marked
 * as an accelerator will receive a %PANGO_UNDERLINE_LOW attribute,
 * and the first character so marked will be returned in @accel_char,
 * when calling finish(). Two @accel_marker characters following each
 * other produce a single literal @accel_marker character.
 *
 * To feed markup to the parser, use g_markup_parse_context_parse()
 * on the returned #GMarkupParseContext. When done with feeding markup
 * to the parser, use pango_markup_parser_finish() to get the data out
 * of it, and then use g_markup_parse_context_free() to free it.
 *
 * This function is designed for applications that read pango markup
 * from streams. To simply parse a string containing pango markup,
 * the simpler pango_parse_markup() API is recommended instead.
 *
 * Return value: (transfer none): a #GMarkupParseContext that should be
 * destroyed with g_markup_parse_context_free().
 *
 * Since: 1.31.0
 **/
GMarkupParseContext *
pango_markup_parser_new (gunichar               accel_marker)
{
  GError *error = NULL;
  GMarkupParseContext *context;
  context = pango_markup_parser_new_internal (accel_marker, &error, TRUE);

  if (context == NULL)
    g_critical ("Had error when making markup parser: %s\n", error->message);

  return context;
}

/**
 * pango_markup_parser_finish:
 * @context: A valid parse context that was returned from pango_markup_parser_new()
 * @attr_list: (out) (allow-none): address of return location for a #PangoAttrList, or %NULL
 * @text: (out) (allow-none): address of return location for text with tags stripped, or %NULL
 * @accel_char: (out) (allow-none): address of return location for accelerator char, or %NULL
 * @error: address of return location for errors, or %NULL
 *
 * After feeding a pango markup parser some data with g_markup_parse_context_parse(),
 * use this function to get the list of pango attributes and text out of the
 * markup. This function will not free @context, use g_markup_parse_context_free()
 * to do so.
 *
 * Return value: %FALSE if @error is set, otherwise %TRUE
 *
 * Since: 1.31.0
 */
gboolean
pango_markup_parser_finish (GMarkupParseContext   *context,
                            PangoAttrList        **attr_list,
                            char                 **text,
                            gunichar              *accel_char,
                            GError               **error)
{
  gboolean ret = FALSE;
  MarkupData *md = g_markup_parse_context_get_user_data (context);
  GSList *tmp_list;

  if (!g_markup_parse_context_parse (context,
                                     "</markup>",
                                     -1,
                                     error))
    goto out;

  if (!g_markup_parse_context_end_parse (context, error))
    goto out;

  if (md->attr_list)
    {
      /* The apply list has the most-recently-closed tags first;
       * we want to apply the least-recently-closed tag last.
       */
      tmp_list = md->to_apply;
      while (tmp_list != NULL)
	{
	  PangoAttribute *attr = tmp_list->data;

	  /* Innermost tags before outermost */
	  pango_attr_list_insert (md->attr_list, attr);

	  tmp_list = g_slist_next (tmp_list);
	}
      g_slist_free (md->to_apply);
      md->to_apply = NULL;
    }

  if (attr_list)
    {
      *attr_list = md->attr_list;
      md->attr_list = NULL;
    }

  if (text)
    {
      *text = g_string_free (md->text, FALSE);
      md->text = NULL;
    }

  if (accel_char)
    *accel_char = md->accel_char;

  g_assert (md->tag_stack == NULL);
  ret = TRUE;

 out:
  return ret;
}

static void
set_bad_attribute (GError             **error,
		   GMarkupParseContext *context,
		   const char          *element_name,
		   const char          *attribute_name)
{
  gint line_number, char_number;

  g_markup_parse_context_get_position (context,
				       &line_number, &char_number);

  g_set_error (error,
	       G_MARKUP_ERROR,
	       G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
	       _("Tag '%s' does not support attribute '%s' on line %d char %d"),
	       element_name,
	       attribute_name,
	       line_number, char_number);
}

static void
add_attribute (OpenTag        *ot,
	       PangoAttribute *attr)
{
  if (ot == NULL)
    pango_attribute_destroy (attr);
  else
    ot->attrs = g_slist_prepend (ot->attrs, attr);
}

#define CHECK_NO_ATTRS(elem) G_STMT_START {                    \
	 if (*names != NULL) {                                 \
	   set_bad_attribute (error, context, (elem), *names); \
	   return FALSE;                                       \
	 } }G_STMT_END

static gboolean
b_parse_func        (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values G_GNUC_UNUSED,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  CHECK_NO_ATTRS("b");
  add_attribute (tag, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
  return TRUE;
}

static gboolean
big_parse_func      (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values G_GNUC_UNUSED,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  CHECK_NO_ATTRS("big");

  /* Grow text one level */
  if (tag)
    {
      tag->scale_level_delta += 1;
      tag->scale_level += 1;
    }

  return TRUE;
}

static gboolean
parse_absolute_size (OpenTag               *tag,
		     const char            *size)
{
  SizeLevel level = Medium;
  double factor;

  if (strcmp (size, "xx-small") == 0)
    level = XXSmall;
  else if (strcmp (size, "x-small") == 0)
    level = XSmall;
  else if (strcmp (size, "small") == 0)
    level = Small;
  else if (strcmp (size, "medium") == 0)
    level = Medium;
  else if (strcmp (size, "large") == 0)
    level = Large;
  else if (strcmp (size, "x-large") == 0)
    level = XLarge;
  else if (strcmp (size, "xx-large") == 0)
    level = XXLarge;
  else
    return FALSE;

  /* This is "absolute" in that it's relative to the base font,
   * but not to sizes created by any other tags
   */
  factor = scale_factor (level, 1.0);
  add_attribute (tag, pango_attr_scale_new (factor));
  if (tag)
    open_tag_set_absolute_font_scale (tag, factor);

  return TRUE;
}

/* a string compare func that ignores '-' vs '_' differences */
static gint
attr_strcmp (gconstpointer pa,
	     gconstpointer pb)
{
  const char *a = pa;
  const char *b = pb;

  int ca;
  int cb;

  while (*a && *b)
    {
      ca = *a++;
      cb = *b++;

      if (ca == cb)
	continue;

      ca = ca == '_' ? '-' : ca;
      cb = cb == '_' ? '-' : cb;

      if (ca != cb)
	return cb - ca;
    }

  ca = *a;
  cb = *b;

  return cb - ca;
}

static gboolean
span_parse_int (const char *attr_name,
		const char *attr_val,
		int *val,
		int line_number,
		GError **error)
{
  const char *end = attr_val;

  if (!_pango_scan_int (&end, val) || *end != '\0')
    {
      g_set_error (error,
		   G_MARKUP_ERROR,
		   G_MARKUP_ERROR_INVALID_CONTENT,
		   _("Value of '%s' attribute on <span> tag "
		     "on line %d could not be parsed; "
		     "should be an integer, not '%s'"),
		   attr_name, line_number, attr_val);
      return FALSE;
    }

  return TRUE;
}

static gboolean
span_parse_boolean (const char *attr_name,
		    const char *attr_val,
		    gboolean *val,
		    int line_number,
		    GError **error)
{
  if (strcmp (attr_val, "true") == 0 ||
      strcmp (attr_val, "yes") == 0 ||
      strcmp (attr_val, "t") == 0 ||
      strcmp (attr_val, "y") == 0)
    *val = TRUE;
  else if (strcmp (attr_val, "false") == 0 ||
	   strcmp (attr_val, "no") == 0 ||
	   strcmp (attr_val, "f") == 0 ||
	   strcmp (attr_val, "n") == 0)
    *val = FALSE;
  else
    {
      g_set_error (error,
		   G_MARKUP_ERROR,
		   G_MARKUP_ERROR_INVALID_CONTENT,
		   _("Value of '%s' attribute on <span> tag "
		     "line %d should have one of "
		     "'true/yes/t/y' or 'false/no/f/n': '%s' is not valid"),
		   attr_name, line_number, attr_val);
      return FALSE;
    }

  return TRUE;
}

static gboolean
span_parse_color (const char *attr_name,
		  const char *attr_val,
		  PangoColor *color,
                  guint16 *alpha,
		  int line_number,
		  GError **error)
{
  if (!_pango_color_parse_with_alpha (color, alpha, attr_val))
    {
      g_set_error (error,
		   G_MARKUP_ERROR,
		   G_MARKUP_ERROR_INVALID_CONTENT,
		   _("Value of '%s' attribute on <span> tag "
		     "on line %d could not be parsed; "
		     "should be a color specification, not '%s'"),
		   attr_name, line_number, attr_val);
      return FALSE;
    }

  return TRUE;
}

static gboolean
span_parse_alpha (const char  *attr_name,
                  const char  *attr_val,
                  guint16     *val,
                  int          line_number,
                  GError     **error)
{
  const char *end = attr_val;
  int int_val;

  if (_pango_scan_int (&end, &int_val))
    {
      if (*end == '\0' && int_val > 0 && int_val <= 0xffff)
        {
          *val = (guint16)int_val;
          return TRUE;
        }
      else if (*end == '%' && int_val > 0 && int_val <= 100)
        {
          *val = (guint16)(int_val * 0xffff / 100);
          return TRUE;
        }
      else
        {
          g_set_error (error,
                       G_MARKUP_ERROR,
                       G_MARKUP_ERROR_INVALID_CONTENT,
                       _("Value of '%s' attribute on <span> tag "
                         "on line %d could not be parsed; "
                         "should be between 0 and 65536 or a "
                         "percentage, not '%s'"),
                         attr_name, line_number, attr_val);
          return FALSE;
        }
    }
  else
    {
      g_set_error (error,
		   G_MARKUP_ERROR,
		   G_MARKUP_ERROR_INVALID_CONTENT,
		   _("Value of '%s' attribute on <span> tag "
		     "on line %d could not be parsed; "
		     "should be an integer, not '%s'"),
		   attr_name, line_number, attr_val);
      return FALSE;
    }

  return TRUE;
}

static gboolean
span_parse_enum (const char *attr_name,
		 const char *attr_val,
		 GType type,
		 int *val,
		 int line_number,
		 GError **error)
{
  char *possible_values = NULL;

  if (!_pango_parse_enum (type, attr_val, val, FALSE, &possible_values))
    {
      g_set_error (error,
		   G_MARKUP_ERROR,
		   G_MARKUP_ERROR_INVALID_CONTENT,
		   _("'%s' is not a valid value for the '%s' "
		     "attribute on <span> tag, line %d; valid "
		     "values are %s"),
		   attr_val, attr_name, line_number, possible_values);
      g_free (possible_values);
      return FALSE;
    }

  return TRUE;
}

static gboolean
span_parse_func     (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  int line_number, char_number;
  int i;

  const char *family = NULL;
  const char *size = NULL;
  const char *style = NULL;
  const char *weight = NULL;
  const char *variant = NULL;
  const char *stretch = NULL;
  const char *desc = NULL;
  const char *foreground = NULL;
  const char *background = NULL;
  const char *underline = NULL;
  const char *underline_color = NULL;
  const char *strikethrough = NULL;
  const char *strikethrough_color = NULL;
  const char *rise = NULL;
  const char *letter_spacing = NULL;
  const char *lang = NULL;
  const char *fallback = NULL;
  const char *gravity = NULL;
  const char *gravity_hint = NULL;
  const char *font_features = NULL;
  const char *alpha = NULL;
  const char *background_alpha = NULL;

  g_markup_parse_context_get_position (context,
				       &line_number, &char_number);

#define CHECK_DUPLICATE(var) G_STMT_START{                              \
	  if ((var) != NULL) {                                          \
	    g_set_error (error, G_MARKUP_ERROR,                         \
			 G_MARKUP_ERROR_INVALID_CONTENT,                \
			 _("Attribute '%s' occurs twice on <span> tag " \
			   "on line %d char %d, may only occur once"),  \
			 names[i], line_number, char_number);           \
	    return FALSE;                                               \
	  }}G_STMT_END
#define CHECK_ATTRIBUTE2(var, name) \
	if (attr_strcmp (names[i], (name)) == 0) { \
	  CHECK_DUPLICATE (var); \
	  (var) = values[i]; \
	  found = TRUE; \
	  break; \
	}
#define CHECK_ATTRIBUTE(var) CHECK_ATTRIBUTE2 (var, G_STRINGIFY (var))

  i = 0;
  while (names[i])
    {
      gboolean found = FALSE;

      switch (names[i][0]) {
      case 'a':
        CHECK_ATTRIBUTE (alpha);
        break;
      case 'b':
	CHECK_ATTRIBUTE (background);
	CHECK_ATTRIBUTE2(background, "bgcolor");
        CHECK_ATTRIBUTE (background_alpha);
        CHECK_ATTRIBUTE2(background_alpha, "bgalpha");
        break;
      case 'c':
	CHECK_ATTRIBUTE2(foreground, "color");
        break;
      case 'f':
	CHECK_ATTRIBUTE (fallback);
	CHECK_ATTRIBUTE2(desc, "font");
	CHECK_ATTRIBUTE2(desc, "font_desc");
	CHECK_ATTRIBUTE2(family, "face");

	CHECK_ATTRIBUTE2(family, "font_family");
	CHECK_ATTRIBUTE2(size, "font_size");
	CHECK_ATTRIBUTE2(stretch, "font_stretch");
	CHECK_ATTRIBUTE2(style, "font_style");
	CHECK_ATTRIBUTE2(variant, "font_variant");
	CHECK_ATTRIBUTE2(weight, "font_weight");

	CHECK_ATTRIBUTE (foreground);
	CHECK_ATTRIBUTE2(foreground, "fgcolor");
	CHECK_ATTRIBUTE2(alpha, "fgalpha");

	CHECK_ATTRIBUTE (font_features);
	break;
      case 's':
	CHECK_ATTRIBUTE (size);
	CHECK_ATTRIBUTE (stretch);
	CHECK_ATTRIBUTE (strikethrough);
	CHECK_ATTRIBUTE (strikethrough_color);
	CHECK_ATTRIBUTE (style);
	break;
      case 'g':
	CHECK_ATTRIBUTE (gravity);
	CHECK_ATTRIBUTE (gravity_hint);
	break;
      case 'l':
	CHECK_ATTRIBUTE (lang);
	CHECK_ATTRIBUTE (letter_spacing);
	break;
      case 'u':
	CHECK_ATTRIBUTE (underline);
	CHECK_ATTRIBUTE (underline_color);
	break;
      default:
	CHECK_ATTRIBUTE (rise);
	CHECK_ATTRIBUTE (variant);
	CHECK_ATTRIBUTE (weight);
	break;
      }

      if (!found)
	{
	  g_set_error (error, G_MARKUP_ERROR,
		       G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
		       _("Attribute '%s' is not allowed on the <span> tag "
			 "on line %d char %d"),
		       names[i], line_number, char_number);
	  return FALSE;
	}

      ++i;
    }

  /* Parse desc first, then modify it with other font-related attributes. */
  if (G_UNLIKELY (desc))
    {
      PangoFontDescription *parsed;

      parsed = pango_font_description_from_string (desc);
      if (parsed)
	{
	  add_attribute (tag, pango_attr_font_desc_new (parsed));
	  if (tag)
	    open_tag_set_absolute_font_size (tag, pango_font_description_get_size (parsed));
	  pango_font_description_free (parsed);
	}
    }

  if (G_UNLIKELY (family))
    {
      add_attribute (tag, pango_attr_family_new (family));
    }

  if (G_UNLIKELY (size))
    {
      if (g_ascii_isdigit (*size))
	{
	  const char *end;
	  gint n;

	  if ((end = size, !_pango_scan_int (&end, &n)) || *end != '\0' || n < 0)
	    {
	      g_set_error (error,
			   G_MARKUP_ERROR,
			   G_MARKUP_ERROR_INVALID_CONTENT,
			   _("Value of 'size' attribute on <span> tag on line %d "
			     "could not be parsed; should be an integer no more than %d,"
			     " or a string such as 'small', not '%s'"),
			   line_number, INT_MAX, size);
	      goto error;
	    }

	  add_attribute (tag, pango_attr_size_new (n));
	  if (tag)
	    open_tag_set_absolute_font_size (tag, n);
	}
      else if (strcmp (size, "smaller") == 0)
	{
	  if (tag)
	    {
	      tag->scale_level_delta -= 1;
	      tag->scale_level -= 1;
	    }
	}
      else if (strcmp (size, "larger") == 0)
	{
	  if (tag)
	    {
	      tag->scale_level_delta += 1;
	      tag->scale_level += 1;
	    }
	}
      else if (parse_absolute_size (tag, size))
	; /* nothing */
      else
	{
	  g_set_error (error,
		       G_MARKUP_ERROR,
		       G_MARKUP_ERROR_INVALID_CONTENT,
		       _("Value of 'size' attribute on <span> tag on line %d "
			 "could not be parsed; should be an integer, or a "
			 "string such as 'small', not '%s'"),
		       line_number, size);
	  goto error;
	}
    }

  if (G_UNLIKELY (style))
    {
      PangoStyle pango_style;

      if (pango_parse_style (style, &pango_style, FALSE))
	add_attribute (tag, pango_attr_style_new (pango_style));
      else
	{
	  g_set_error (error,
		       G_MARKUP_ERROR,
		       G_MARKUP_ERROR_INVALID_CONTENT,
		       _("'%s' is not a valid value for the 'style' attribute "
			 "on <span> tag, line %d; valid values are "
			 "'normal', 'oblique', 'italic'"),
		       style, line_number);
	  goto error;
	}
    }

  if (G_UNLIKELY (weight))
    {
      PangoWeight pango_weight;

      if (pango_parse_weight (weight, &pango_weight, FALSE))
	add_attribute (tag,
		       pango_attr_weight_new (pango_weight));
      else
	{
	  g_set_error (error,
		       G_MARKUP_ERROR,
		       G_MARKUP_ERROR_INVALID_CONTENT,
		       _("'%s' is not a valid value for the 'weight' "
			 "attribute on <span> tag, line %d; valid "
			 "values are for example 'light', 'ultrabold' or a number"),
		       weight, line_number);
	  goto error;
	}
    }

  if (G_UNLIKELY (variant))
    {
      PangoVariant pango_variant;

      if (pango_parse_variant (variant, &pango_variant, FALSE))
	add_attribute (tag, pango_attr_variant_new (pango_variant));
      else
	{
	  g_set_error (error,
		       G_MARKUP_ERROR,
		       G_MARKUP_ERROR_INVALID_CONTENT,
		       _("'%s' is not a valid value for the 'variant' "
			 "attribute on <span> tag, line %d; valid values are "
			 "'normal', 'smallcaps'"),
		       variant, line_number);
	  goto error;
	}
    }

  if (G_UNLIKELY (stretch))
    {
      PangoStretch pango_stretch;

      if (pango_parse_stretch (stretch, &pango_stretch, FALSE))
	add_attribute (tag, pango_attr_stretch_new (pango_stretch));
      else
	{
	  g_set_error (error,
		       G_MARKUP_ERROR,
		       G_MARKUP_ERROR_INVALID_CONTENT,
		       _("'%s' is not a valid value for the 'stretch' "
			 "attribute on <span> tag, line %d; valid "
			 "values are for example 'condensed', "
			 "'ultraexpanded', 'normal'"),
		       stretch, line_number);
	  goto error;
	}
    }

  if (G_UNLIKELY (foreground))
    {
      PangoColor color;
      guint16 alpha;

      if (!span_parse_color ("foreground", foreground, &color, &alpha, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_foreground_new (color.red, color.green, color.blue));
      if (alpha != 0)
        add_attribute (tag, pango_attr_foreground_alpha_new (alpha));
    }

  if (G_UNLIKELY (background))
    {
      PangoColor color;
      guint16 alpha;

      if (!span_parse_color ("background", background, &color, &alpha, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_background_new (color.red, color.green, color.blue));
      if (alpha != 0)
        add_attribute (tag, pango_attr_background_alpha_new (alpha));
    }

  if (G_UNLIKELY (alpha))
    {
      guint16 val;

      if (!span_parse_alpha ("alpha", alpha, &val, line_number, error))
        goto error;

      add_attribute (tag, pango_attr_foreground_alpha_new (val));
    }

  if (G_UNLIKELY (background_alpha))
    {
      guint16 val;

      if (!span_parse_alpha ("background_alpha", background_alpha, &val, line_number, error))
        goto error;

      add_attribute (tag, pango_attr_background_alpha_new (val));
    }

  if (G_UNLIKELY (underline))
    {
      PangoUnderline ul = PANGO_UNDERLINE_NONE;

      if (!span_parse_enum ("underline", underline, PANGO_TYPE_UNDERLINE, (int*)(void*)&ul, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_underline_new (ul));
    }

  if (G_UNLIKELY (underline_color))
    {
      PangoColor color;

      if (!span_parse_color ("underline_color", underline_color, &color, NULL, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_underline_color_new (color.red, color.green, color.blue));
    }

  if (G_UNLIKELY (gravity))
    {
      PangoGravity gr = PANGO_GRAVITY_SOUTH;

      if (!span_parse_enum ("gravity", gravity, PANGO_TYPE_GRAVITY, (int*)(void*)&gr, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_gravity_new (gr));
    }

  if (G_UNLIKELY (gravity_hint))
    {
      PangoGravityHint hint = PANGO_GRAVITY_HINT_NATURAL;

      if (!span_parse_enum ("gravity_hint", gravity_hint, PANGO_TYPE_GRAVITY_HINT, (int*)(void*)&hint, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_gravity_hint_new (hint));
    }

  if (G_UNLIKELY (strikethrough))
    {
      gboolean b = FALSE;

      if (!span_parse_boolean ("strikethrough", strikethrough, &b, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_strikethrough_new (b));
    }

  if (G_UNLIKELY (strikethrough_color))
    {
      PangoColor color;

      if (!span_parse_color ("strikethrough_color", strikethrough_color, &color, NULL, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_strikethrough_color_new (color.red, color.green, color.blue));
    }

  if (G_UNLIKELY (fallback))
    {
      gboolean b = FALSE;

      if (!span_parse_boolean ("fallback", fallback, &b, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_fallback_new (b));
    }

  if (G_UNLIKELY (rise))
    {
      gint n = 0;

      if (!span_parse_int ("rise", rise, &n, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_rise_new (n));
    }

  if (G_UNLIKELY (letter_spacing))
    {
      gint n = 0;

      if (!span_parse_int ("letter_spacing", letter_spacing, &n, line_number, error))
	goto error;

      add_attribute (tag, pango_attr_letter_spacing_new (n));
    }

  if (G_UNLIKELY (lang))
    {
      add_attribute (tag,
		     pango_attr_language_new (pango_language_from_string (lang)));
    }

  if (G_UNLIKELY (font_features))
    {
      add_attribute (tag, pango_attr_font_features_new (font_features));
    }

  return TRUE;

 error:

  return FALSE;
}

static gboolean
i_parse_func        (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values G_GNUC_UNUSED,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  CHECK_NO_ATTRS("i");
  add_attribute (tag, pango_attr_style_new (PANGO_STYLE_ITALIC));

  return TRUE;
}

static gboolean
markup_parse_func (MarkupData            *md G_GNUC_UNUSED,
		   OpenTag               *tag G_GNUC_UNUSED,
		   const gchar          **names G_GNUC_UNUSED,
		   const gchar          **values G_GNUC_UNUSED,
		   GMarkupParseContext   *context G_GNUC_UNUSED,
		   GError               **error G_GNUC_UNUSED)
{
  /* We don't do anything with this tag at the moment. */

  return TRUE;
}

static gboolean
s_parse_func        (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values G_GNUC_UNUSED,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  CHECK_NO_ATTRS("s");
  add_attribute (tag, pango_attr_strikethrough_new (TRUE));

  return TRUE;
}

#define SUPERSUB_RISE 5000

static gboolean
sub_parse_func      (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values G_GNUC_UNUSED,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  CHECK_NO_ATTRS("sub");

  /* Shrink font, and set a negative rise */
  if (tag)
    {
      tag->scale_level_delta -= 1;
      tag->scale_level -= 1;
    }

  add_attribute (tag, pango_attr_rise_new (-SUPERSUB_RISE));

  return TRUE;
}

static gboolean
sup_parse_func      (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values G_GNUC_UNUSED,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  CHECK_NO_ATTRS("sup");

  /* Shrink font, and set a positive rise */
  if (tag)
    {
      tag->scale_level_delta -= 1;
      tag->scale_level -= 1;
    }

  add_attribute (tag, pango_attr_rise_new (SUPERSUB_RISE));

  return TRUE;
}

static gboolean
small_parse_func    (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values G_GNUC_UNUSED,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  CHECK_NO_ATTRS("small");

  /* Shrink text one level */
  if (tag)
    {
      tag->scale_level_delta -= 1;
      tag->scale_level -= 1;
    }

  return TRUE;
}

static gboolean
tt_parse_func       (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values G_GNUC_UNUSED,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  CHECK_NO_ATTRS("tt");

  add_attribute (tag, pango_attr_family_new ("Monospace"));

  return TRUE;
}

static gboolean
u_parse_func        (MarkupData            *md G_GNUC_UNUSED,
		     OpenTag               *tag,
		     const gchar          **names,
		     const gchar          **values G_GNUC_UNUSED,
		     GMarkupParseContext   *context,
		     GError               **error)
{
  CHECK_NO_ATTRS("u");
  add_attribute (tag, pango_attr_underline_new (PANGO_UNDERLINE_SINGLE));

  return TRUE;
}