Blob Blame History Raw
/* GStreamer SAMI subtitle parser
 * Copyright (c) 2006, 2013 Young-Ho Cha <ganadist at gmail com>
 *
 * 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., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "samiparse.h"

#include <glib.h>
#include <string.h>
#include <stdlib.h>

#define ITALIC_TAG 'i'
#define SPAN_TAG   's'
#define RUBY_TAG   'r'
#define RT_TAG     't'
#define CLEAR_TAG  '0'

typedef struct _HtmlParser HtmlParser;
typedef struct _HtmlContext HtmlContext;
typedef struct _GstSamiContext GstSamiContext;

struct _GstSamiContext
{
  GString *buf;                 /* buffer to collect content */
  GString *rubybuf;             /* buffer to collect ruby content */
  GString *resultbuf;           /* when opening the next 'sync' tag, move
                                 * from 'buf' to avoid to append following
                                 * content */
  GString *state;               /* in many sami files there are tags that
                                 * are not closed, so for each open tag the
                                 * parser will append a tag flag here so
                                 * that tags can be closed properly on
                                 * 'sync' tags. See _context_push_state()
                                 * and _context_pop_state(). */
  HtmlContext *htmlctxt;        /* html parser context */
  gboolean has_result;          /* set when ready to push out result */
  gboolean in_sync;             /* flag to avoid appending anything except the
                                 * content of the sync elements to buf */
  guint64 time1;                /* previous start attribute in sync tag */
  guint64 time2;                /* current start attribute in sync tag  */
};

struct _HtmlParser
{
  void (*start_element) (HtmlContext * ctx,
      const gchar * name, const gchar ** attr, gpointer user_data);
  void (*end_element) (HtmlContext * ctx,
      const gchar * name, gpointer user_data);
  void (*text) (HtmlContext * ctx,
      const gchar * text, gsize text_len, gpointer user_data);
};

struct _HtmlContext
{
  const HtmlParser *parser;
  gpointer user_data;
  GString *buf;
};

static HtmlContext *
html_context_new (HtmlParser * parser, gpointer user_data)
{
  HtmlContext *ctxt = (HtmlContext *) g_new0 (HtmlContext, 1);
  ctxt->parser = parser;
  ctxt->user_data = user_data;
  ctxt->buf = g_string_new (NULL);
  return ctxt;
}

static void
html_context_free (HtmlContext * ctxt)
{
  g_string_free (ctxt->buf, TRUE);
  g_free (ctxt);
}

struct EntityMap
{
  const gunichar unescaped;
  const gchar *escaped;
};

struct EntityMap XmlEntities[] = {
  {34, "quot;"},
  {38, "amp;"},
  {39, "apos;"},
  {60, "lt;"},
  {62, "gt;"},
  {0, NULL},
};

struct EntityMap HtmlEntities[] = {
/* nbsp will handle manually
{ 160,	"nbsp;" }, */
  {161, "iexcl;"},
  {162, "cent;"},
  {163, "pound;"},
  {164, "curren;"},
  {165, "yen;"},
  {166, "brvbar;"},
  {167, "sect;"},
  {168, "uml;"},
  {169, "copy;"},
  {170, "ordf;"},
  {171, "laquo;"},
  {172, "not;"},
  {173, "shy;"},
  {174, "reg;"},
  {175, "macr;"},
  {176, "deg;"},
  {177, "plusmn;"},
  {178, "sup2;"},
  {179, "sup3;"},
  {180, "acute;"},
  {181, "micro;"},
  {182, "para;"},
  {183, "middot;"},
  {184, "cedil;"},
  {185, "sup1;"},
  {186, "ordm;"},
  {187, "raquo;"},
  {188, "frac14;"},
  {189, "frac12;"},
  {190, "frac34;"},
  {191, "iquest;"},
  {192, "Agrave;"},
  {193, "Aacute;"},
  {194, "Acirc;"},
  {195, "Atilde;"},
  {196, "Auml;"},
  {197, "Aring;"},
  {198, "AElig;"},
  {199, "Ccedil;"},
  {200, "Egrave;"},
  {201, "Eacute;"},
  {202, "Ecirc;"},
  {203, "Euml;"},
  {204, "Igrave;"},
  {205, "Iacute;"},
  {206, "Icirc;"},
  {207, "Iuml;"},
  {208, "ETH;"},
  {209, "Ntilde;"},
  {210, "Ograve;"},
  {211, "Oacute;"},
  {212, "Ocirc;"},
  {213, "Otilde;"},
  {214, "Ouml;"},
  {215, "times;"},
  {216, "Oslash;"},
  {217, "Ugrave;"},
  {218, "Uacute;"},
  {219, "Ucirc;"},
  {220, "Uuml;"},
  {221, "Yacute;"},
  {222, "THORN;"},
  {223, "szlig;"},
  {224, "agrave;"},
  {225, "aacute;"},
  {226, "acirc;"},
  {227, "atilde;"},
  {228, "auml;"},
  {229, "aring;"},
  {230, "aelig;"},
  {231, "ccedil;"},
  {232, "egrave;"},
  {233, "eacute;"},
  {234, "ecirc;"},
  {235, "euml;"},
  {236, "igrave;"},
  {237, "iacute;"},
  {238, "icirc;"},
  {239, "iuml;"},
  {240, "eth;"},
  {241, "ntilde;"},
  {242, "ograve;"},
  {243, "oacute;"},
  {244, "ocirc;"},
  {245, "otilde;"},
  {246, "ouml;"},
  {247, "divide;"},
  {248, "oslash;"},
  {249, "ugrave;"},
  {250, "uacute;"},
  {251, "ucirc;"},
  {252, "uuml;"},
  {253, "yacute;"},
  {254, "thorn;"},
  {255, "yuml;"},
  {338, "OElig;"},
  {339, "oelig;"},
  {352, "Scaron;"},
  {353, "scaron;"},
  {376, "Yuml;"},
  {402, "fnof;"},
  {710, "circ;"},
  {732, "tilde;"},
  {913, "Alpha;"},
  {914, "Beta;"},
  {915, "Gamma;"},
  {916, "Delta;"},
  {917, "Epsilon;"},
  {918, "Zeta;"},
  {919, "Eta;"},
  {920, "Theta;"},
  {921, "Iota;"},
  {922, "Kappa;"},
  {923, "Lambda;"},
  {924, "Mu;"},
  {925, "Nu;"},
  {926, "Xi;"},
  {927, "Omicron;"},
  {928, "Pi;"},
  {929, "Rho;"},
  {931, "Sigma;"},
  {932, "Tau;"},
  {933, "Upsilon;"},
  {934, "Phi;"},
  {935, "Chi;"},
  {936, "Psi;"},
  {937, "Omega;"},
  {945, "alpha;"},
  {946, "beta;"},
  {947, "gamma;"},
  {948, "delta;"},
  {949, "epsilon;"},
  {950, "zeta;"},
  {951, "eta;"},
  {952, "theta;"},
  {953, "iota;"},
  {954, "kappa;"},
  {955, "lambda;"},
  {956, "mu;"},
  {957, "nu;"},
  {958, "xi;"},
  {959, "omicron;"},
  {960, "pi;"},
  {961, "rho;"},
  {962, "sigmaf;"},
  {963, "sigma;"},
  {964, "tau;"},
  {965, "upsilon;"},
  {966, "phi;"},
  {967, "chi;"},
  {968, "psi;"},
  {969, "omega;"},
  {977, "thetasym;"},
  {978, "upsih;"},
  {982, "piv;"},
  {8194, "ensp;"},
  {8195, "emsp;"},
  {8201, "thinsp;"},
  {8204, "zwnj;"},
  {8205, "zwj;"},
  {8206, "lrm;"},
  {8207, "rlm;"},
  {8211, "ndash;"},
  {8212, "mdash;"},
  {8216, "lsquo;"},
  {8217, "rsquo;"},
  {8218, "sbquo;"},
  {8220, "ldquo;"},
  {8221, "rdquo;"},
  {8222, "bdquo;"},
  {8224, "dagger;"},
  {8225, "Dagger;"},
  {8226, "bull;"},
  {8230, "hellip;"},
  {8240, "permil;"},
  {8242, "prime;"},
  {8243, "Prime;"},
  {8249, "lsaquo;"},
  {8250, "rsaquo;"},
  {8254, "oline;"},
  {8260, "frasl;"},
  {8364, "euro;"},
  {8465, "image;"},
  {8472, "weierp;"},
  {8476, "real;"},
  {8482, "trade;"},
  {8501, "alefsym;"},
  {8592, "larr;"},
  {8593, "uarr;"},
  {8594, "rarr;"},
  {8595, "darr;"},
  {8596, "harr;"},
  {8629, "crarr;"},
  {8656, "lArr;"},
  {8657, "uArr;"},
  {8658, "rArr;"},
  {8659, "dArr;"},
  {8660, "hArr;"},
  {8704, "forall;"},
  {8706, "part;"},
  {8707, "exist;"},
  {8709, "empty;"},
  {8711, "nabla;"},
  {8712, "isin;"},
  {8713, "notin;"},
  {8715, "ni;"},
  {8719, "prod;"},
  {8721, "sum;"},
  {8722, "minus;"},
  {8727, "lowast;"},
  {8730, "radic;"},
  {8733, "prop;"},
  {8734, "infin;"},
  {8736, "ang;"},
  {8743, "and;"},
  {8744, "or;"},
  {8745, "cap;"},
  {8746, "cup;"},
  {8747, "int;"},
  {8756, "there4;"},
  {8764, "sim;"},
  {8773, "cong;"},
  {8776, "asymp;"},
  {8800, "ne;"},
  {8801, "equiv;"},
  {8804, "le;"},
  {8805, "ge;"},
  {8834, "sub;"},
  {8835, "sup;"},
  {8836, "nsub;"},
  {8838, "sube;"},
  {8839, "supe;"},
  {8853, "oplus;"},
  {8855, "otimes;"},
  {8869, "perp;"},
  {8901, "sdot;"},
  {8968, "lceil;"},
  {8969, "rceil;"},
  {8970, "lfloor;"},
  {8971, "rfloor;"},
  {9001, "lang;"},
  {9002, "rang;"},
  {9674, "loz;"},
  {9824, "spades;"},
  {9827, "clubs;"},
  {9829, "hearts;"},
  {9830, "diams;"},
  {0, NULL},
};

static gchar *
unescape_string (const gchar * text)
{
  gint i;
  GString *unescaped = g_string_new (NULL);

  while (*text) {
    if (*text == '&') {
      text++;

      /* unescape &nbsp and &nbsp; */
      if (!g_ascii_strncasecmp (text, "nbsp", 4)) {
        unescaped = g_string_append_unichar (unescaped, 160);
        text += 4;
        if (*text == ';') {
          text++;
        }
        goto next;
      }

      /* pass xml entities. these will be processed as pango markup */
      for (i = 0; XmlEntities[i].escaped; i++) {
        gssize len = strlen (XmlEntities[i].escaped);
        if (!g_ascii_strncasecmp (text, XmlEntities[i].escaped, len)) {
          unescaped = g_string_append_c (unescaped, '&');
          unescaped =
              g_string_append_len (unescaped, XmlEntities[i].escaped, len);
          text += len;
          goto next;
        }
      }

      /* convert html entities */
      for (i = 0; HtmlEntities[i].escaped; i++) {
        gssize len = strlen (HtmlEntities[i].escaped);
        if (!strncmp (text, HtmlEntities[i].escaped, len)) {
          unescaped =
              g_string_append_unichar (unescaped, HtmlEntities[i].unescaped);
          text += len;
          goto next;
        }
      }

      if (*text == '#') {
        gboolean is_hex = FALSE;
        gunichar l;
        gchar *end = NULL;

        text++;
        if (*text == 'x') {
          is_hex = TRUE;
          text++;
        }
        errno = 0;
        if (is_hex) {
          l = strtoul (text, &end, 16);
        } else {
          l = strtoul (text, &end, 10);
        }

        if (text == end || errno != 0) {
          /* error occured. pass it */
          goto next;
        }
        unescaped = g_string_append_unichar (unescaped, l);
        text = end;

        if (*text == ';') {
          text++;
        }
        goto next;
      }

      /* escape & */
      unescaped = g_string_append (unescaped, "&amp;");

    next:
      continue;

    } else if (g_ascii_isspace (*text)) {
      unescaped = g_string_append_c (unescaped, ' ');
      /* strip whitespace */
      do {
        text++;
      } while ((*text) && g_ascii_isspace (*text));
    } else {
      unescaped = g_string_append_c (unescaped, *text);
      text++;
    }
  }

  return g_string_free (unescaped, FALSE);
}

static const gchar *
string_token (const gchar * string, const gchar * delimiter, gchar ** first)
{
  gchar *next = strstr (string, delimiter);
  if (next) {
    *first = g_strndup (string, next - string);
  } else {
    *first = g_strdup (string);
  }
  return next;
}

static void
html_context_handle_element (HtmlContext * ctxt,
    const gchar * string, gboolean must_close)
{
  gchar *name = NULL;
  gint count = 0, i;
  gchar **attrs;
  const gchar *found, *next;

  /* split element name and attributes */
  next = string_token (string, " ", &name);

  if (next) {
    /* count attributes */
    found = next + 1;
    while (TRUE) {
      found = strchr (found, '=');
      if (!found)
        break;
      found++;
      count++;
    }
  } else {
    count = 0;
  }

  attrs = g_new0 (gchar *, (count + 1) * 2);

  for (i = 0; i < count && next != NULL; i += 2) {
    gchar *attr_name = NULL, *attr_value = NULL;
    gsize length;
    next = string_token (next + 1, "=", &attr_name);
    next = string_token (next + 1, " ", &attr_value);

    /* strip " or ' from attribute value */
    if (attr_value[0] == '"' || attr_value[0] == '\'') {
      gchar *tmp = g_strdup (attr_value + 1);
      g_free (attr_value);
      attr_value = tmp;
    }

    length = strlen (attr_value);
    if (length > 0 && (attr_value[length - 1] == '"'
            || attr_value[length - 1] == '\'')) {
      attr_value[length - 1] = '\0';
    }

    attrs[i] = attr_name;
    attrs[i + 1] = attr_value;
  }

  ctxt->parser->start_element (ctxt, name,
      (const gchar **) attrs, ctxt->user_data);
  if (must_close) {
    ctxt->parser->end_element (ctxt, name, ctxt->user_data);
  }
  g_strfreev (attrs);
  g_free (name);
}

static void
html_context_parse (HtmlContext * ctxt, gchar * text, gsize text_len)
{
  const gchar *next = NULL;
  ctxt->buf = g_string_append_len (ctxt->buf, text, text_len);
  next = ctxt->buf->str;
  while (TRUE) {
    if (next[0] == '<') {
      gchar *element = NULL;
      /* find <blahblah> */
      if (!strchr (next, '>')) {
        /* no tag end point. buffer will be process in next time */
        return;
      }

      next = string_token (next, ">", &element);
      next++;
      if (g_str_has_suffix (next, "/")) {
        /* handle <blah/> */
        element[strlen (element) - 1] = '\0';
        html_context_handle_element (ctxt, element + 1, TRUE);
      } else if (element[1] == '/') {
        /* handle </blah> */
        ctxt->parser->end_element (ctxt, element + 2, ctxt->user_data);
      } else {
        /* handle <blah> */
        html_context_handle_element (ctxt, element + 1, FALSE);
      }
      g_free (element);
    } else if (strchr (next, '<')) {
      gchar *text = NULL;
      gsize length;
      next = string_token (next, "<", &text);
      text = g_strstrip (text);
      length = strlen (text);
      ctxt->parser->text (ctxt, text, length, ctxt->user_data);
      g_free (text);

    } else {
      gchar *text = (gchar *) next;
      gsize length;
      text = g_strstrip (text);
      length = strlen (text);
      ctxt->parser->text (ctxt, text, length, ctxt->user_data);
      ctxt->buf = g_string_assign (ctxt->buf, "");
      return;
    }
  }

  ctxt->buf = g_string_assign (ctxt->buf, next);
}

static gchar *
has_tag (GString * str, const gchar tag)
{
  return strrchr (str->str, tag);
}

static void
sami_context_push_state (GstSamiContext * sctx, char state)
{
  GST_LOG ("state %c", state);
  g_string_append_c (sctx->state, state);
}

static void
sami_context_pop_state (GstSamiContext * sctx, char state)
{
  GString *str = g_string_new ("");
  GString *context_state = sctx->state;
  int i;

  GST_LOG ("state %c", state);
  for (i = context_state->len - 1; i >= 0; i--) {
    switch (context_state->str[i]) {
      case ITALIC_TAG:         /* <i> */
      {
        g_string_append (str, "</i>");
        break;
      }
      case SPAN_TAG:           /* <span foreground= > */
      {
        g_string_append (str, "</span>");
        break;
      }
      case RUBY_TAG:           /* <span size= >  -- ruby */
      {
        break;
      }
      case RT_TAG:             /*  ruby */
      {
        /* FIXME: support for furigana/ruby once implemented in pango */
        g_string_append (sctx->rubybuf, "</span>");
        if (has_tag (context_state, ITALIC_TAG)) {
          g_string_append (sctx->rubybuf, "</i>");
        }

        break;
      }
      default:
        break;
    }
    if (context_state->str[i] == state) {
      g_string_append (sctx->buf, str->str);
      g_string_free (str, TRUE);
      g_string_truncate (context_state, i);
      return;
    }
  }
  if (state == CLEAR_TAG) {
    g_string_append (sctx->buf, str->str);
    g_string_truncate (context_state, 0);
  }
  g_string_free (str, TRUE);
}

static void
handle_start_sync (GstSamiContext * sctx, const gchar ** atts)
{
  int i;

  sami_context_pop_state (sctx, CLEAR_TAG);
  if (atts != NULL) {
    for (i = 0; (atts[i] != NULL); i += 2) {
      const gchar *key, *value;

      key = atts[i];
      value = atts[i + 1];

      if (!value)
        continue;
      if (!g_ascii_strcasecmp ("start", key)) {
        /* Only set a new start time if we don't have text pending */
        if (sctx->resultbuf->len == 0)
          sctx->time1 = sctx->time2;

        sctx->time2 = atoi ((const char *) value) * GST_MSECOND;
        sctx->time2 = MAX (sctx->time2, sctx->time1);
        g_string_append (sctx->resultbuf, sctx->buf->str);
        sctx->has_result = (sctx->resultbuf->len != 0) ? TRUE : FALSE;
        g_string_truncate (sctx->buf, 0);
      }
    }
  }
}

static void
handle_start_font (GstSamiContext * sctx, const gchar ** atts)
{
  int i;

  sami_context_pop_state (sctx, SPAN_TAG);
  if (atts != NULL) {
    g_string_append (sctx->buf, "<span");
    for (i = 0; (atts[i] != NULL); i += 2) {
      const gchar *key, *value;

      key = atts[i];
      value = atts[i + 1];

      if (!value)
        continue;
      if (!g_ascii_strcasecmp ("color", key)) {
        /*
         * There are invalid color value in many
         * sami files.
         * It will fix hex color value that start without '#'
         */
        const gchar *sharp = "";
        int len = strlen (value);

        if (!(*value == '#' && len == 7)) {
          gchar *r;

          /* check if it looks like hex */
          if (strtol ((const char *) value, &r, 16) >= 0 &&
              ((gchar *) r == (value + 6) && len == 6)) {
            sharp = "#";
          }
        }
        /* some colours can be found in many sami files, but X RGB database
         * doesn't contain a colour by this name, so map explicitly */
        if (!g_ascii_strcasecmp ("aqua", value)) {
          value = "#00ffff";
        } else if (!g_ascii_strcasecmp ("crimson", value)) {
          value = "#dc143c";
        } else if (!g_ascii_strcasecmp ("fuchsia", value)) {
          value = "#ff00ff";
        } else if (!g_ascii_strcasecmp ("indigo", value)) {
          value = "#4b0082";
        } else if (!g_ascii_strcasecmp ("lime", value)) {
          value = "#00ff00";
        } else if (!g_ascii_strcasecmp ("olive", value)) {
          value = "#808000";
        } else if (!g_ascii_strcasecmp ("silver", value)) {
          value = "#c0c0c0";
        } else if (!g_ascii_strcasecmp ("teal", value)) {
          value = "#008080";
        }
        g_string_append_printf (sctx->buf, " foreground=\"%s%s\"", sharp,
            value);
      } else if (!g_ascii_strcasecmp ("face", key)) {
        g_string_append_printf (sctx->buf, " font_family=\"%s\"", value);
      }
    }
    g_string_append_c (sctx->buf, '>');
    sami_context_push_state (sctx, SPAN_TAG);
  }
}

static void
handle_start_element (HtmlContext * ctx, const gchar * name,
    const char **atts, gpointer user_data)
{
  GstSamiContext *sctx = (GstSamiContext *) user_data;

  GST_LOG ("name:%s", name);

  if (!g_ascii_strcasecmp ("sync", name)) {
    handle_start_sync (sctx, atts);
    sctx->in_sync = TRUE;
  } else if (!g_ascii_strcasecmp ("font", name)) {
    handle_start_font (sctx, atts);
  } else if (!g_ascii_strcasecmp ("ruby", name)) {
    sami_context_push_state (sctx, RUBY_TAG);
  } else if (!g_ascii_strcasecmp ("br", name)) {
    g_string_append_c (sctx->buf, '\n');
    /* FIXME: support for furigana/ruby once implemented in pango */
  } else if (!g_ascii_strcasecmp ("rt", name)) {
    if (has_tag (sctx->state, ITALIC_TAG)) {
      g_string_append (sctx->rubybuf, "<i>");
    }
    g_string_append (sctx->rubybuf, "<span size='xx-small' rise='-100'>");
    sami_context_push_state (sctx, RT_TAG);
  } else if (!g_ascii_strcasecmp ("i", name)) {
    g_string_append (sctx->buf, "<i>");
    sami_context_push_state (sctx, ITALIC_TAG);
  } else if (!g_ascii_strcasecmp ("p", name)) {
  }
}

static void
handle_end_element (HtmlContext * ctx, const char *name, gpointer user_data)
{
  GstSamiContext *sctx = (GstSamiContext *) user_data;

  GST_LOG ("name:%s", name);

  if (!g_ascii_strcasecmp ("sync", name)) {
    sctx->in_sync = FALSE;
  } else if ((!g_ascii_strcasecmp ("body", name)) ||
      (!g_ascii_strcasecmp ("sami", name))) {
    /* We will usually have one buffer left when the body is closed
     * as we need the next sync to actually send it */
    if (sctx->buf->len != 0) {
      /* Only set a new start time if we don't have text pending */
      if (sctx->resultbuf->len == 0)
        sctx->time1 = sctx->time2;

      sctx->time2 = GST_CLOCK_TIME_NONE;
      g_string_append (sctx->resultbuf, sctx->buf->str);
      sctx->has_result = (sctx->resultbuf->len != 0) ? TRUE : FALSE;
      g_string_truncate (sctx->buf, 0);
    }
  } else if (!g_ascii_strcasecmp ("font", name)) {
    sami_context_pop_state (sctx, SPAN_TAG);
  } else if (!g_ascii_strcasecmp ("ruby", name)) {
    sami_context_pop_state (sctx, RUBY_TAG);
  } else if (!g_ascii_strcasecmp ("i", name)) {
    sami_context_pop_state (sctx, ITALIC_TAG);
  }
}

static void
handle_text (HtmlContext * ctx, const gchar * text, gsize text_len,
    gpointer user_data)
{
  GstSamiContext *sctx = (GstSamiContext *) user_data;

  /* Skip everything except content of the sync elements */
  if (!sctx->in_sync)
    return;

  if (has_tag (sctx->state, RT_TAG)) {
    g_string_append_c (sctx->rubybuf, ' ');
    g_string_append (sctx->rubybuf, text);
    g_string_append_c (sctx->rubybuf, ' ');
  } else {
    g_string_append (sctx->buf, text);
  }
}

static HtmlParser samiParser = {
  handle_start_element,         /* start_element */
  handle_end_element,           /* end_element */
  handle_text,                  /* text */
};

void
sami_context_init (ParserState * state)
{
  GstSamiContext *context;

  g_assert (state->user_data == NULL);

  context = g_new0 (GstSamiContext, 1);

  context->htmlctxt = html_context_new (&samiParser, context);
  context->buf = g_string_new ("");
  context->rubybuf = g_string_new ("");
  context->resultbuf = g_string_new ("");
  context->state = g_string_new ("");

  state->user_data = context;
}

void
sami_context_deinit (ParserState * state)
{
  GstSamiContext *context = (GstSamiContext *) state->user_data;

  if (context) {
    html_context_free (context->htmlctxt);
    context->htmlctxt = NULL;
    g_string_free (context->buf, TRUE);
    g_string_free (context->rubybuf, TRUE);
    g_string_free (context->resultbuf, TRUE);
    g_string_free (context->state, TRUE);
    g_free (context);
    state->user_data = NULL;
  }
}

void
sami_context_reset (ParserState * state)
{
  GstSamiContext *context = (GstSamiContext *) state->user_data;

  if (context) {
    g_string_truncate (context->buf, 0);
    g_string_truncate (context->rubybuf, 0);
    g_string_truncate (context->resultbuf, 0);
    g_string_truncate (context->state, 0);
    context->has_result = FALSE;
    context->in_sync = FALSE;
    context->time1 = 0;
    context->time2 = 0;
  }
}

gchar *
parse_sami (ParserState * state, const gchar * line)
{
  gchar *ret = NULL;
  GstSamiContext *context = (GstSamiContext *) state->user_data;

  gchar *unescaped = unescape_string (line);
  html_context_parse (context->htmlctxt, (gchar *) unescaped,
      strlen (unescaped));
  g_free (unescaped);

  if (context->has_result) {
    if (context->rubybuf->len) {
      context->rubybuf = g_string_append_c (context->rubybuf, '\n');
      g_string_prepend (context->resultbuf, context->rubybuf->str);
      context->rubybuf = g_string_truncate (context->rubybuf, 0);
    }

    ret = g_string_free (context->resultbuf, FALSE);
    context->resultbuf = g_string_new ("");
    state->start_time = context->time1;
    state->duration = context->time2 - context->time1;
    context->has_result = FALSE;
  }
  return ret;
}