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