/* MySpaceIM Protocol Plugin - markup * * Copyright (C) 2007, Jeff Connelly * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA */ #include "myspace.h" typedef int (*MSIM_XMLNODE_CONVERT)(MsimSession *, xmlnode *, gchar **, gchar **); /* Globals */ /* The names in in emoticon_names (for ) map to corresponding * entries in emoticon_symbols (for the ASCII representation of the emoticon). * * Multiple emoticon symbols in Pidgin can map to one name. List the * canonical form, as inserted by the "Smile!" dialog, first. For example, * :) comes before :-), because although both are recognized as 'happy', * the first is inserted by the smiley button (first symbol in theme). * * Note that symbols are case-sensitive in Pidgin -- :-X is not :-x. */ static struct MSIM_EMOTICON { gchar *name; gchar *symbol; } msim_emoticons[] = { /* Unfortunately, this list duplicates much of the file * pidgin/pidgin/pixmaps/emotes/default/22/default.theme.in, because * that file is part of Pidgin, but we're part of libpurple. */ { "bigsmile", ":D" }, { "bigsmile", ":-D" }, { "devil", "}:)" }, { "frazzled", ":Z" }, { "geek", "B)" }, { "googles", "%)" }, { "growl", ":E" }, { "laugh", ":))" }, /* Must be before ':)' */ { "happy", ":)" }, { "happy", ":-)" }, { "happi", ":)" }, { "heart", ":X" }, { "mohawk", "-:" }, { "mad", "X(" }, { "messed", "X)" }, { "nerd", "Q)" }, { "oops", ":G" }, { "pirate", "P)" }, { "scared", ":O" }, { "sidefrown", ":{" }, { "sinister", ":B" }, { "smirk", ":," }, { "straight", ":|" }, { "tongue", ":P" }, { "tongue", ":p" }, { "tongy", ":P" }, { "upset", "B|" }, { "wink", ";-)" }, { "wink", ";)" }, { "winc", ";)" }, { "worried", ":[" }, { "kiss", ":x" }, { NULL, NULL } }; /* Indexes of this array + 1 map HTML font size to scale of normal font size. * * Based on _point_sizes from libpurple/gtkimhtml.c * 1 2 3 4 5 6 7 */ static gdouble _font_scale[] = { .85, .95, 1, 1.2, 1.44, 1.728, 2.0736 }; /* Purple maximum font size. Equivalent to sizeof(_font_scale) / sizeof(_font_scale[0]) */ #define MAX_FONT_SIZE 7 #define POINTS_PER_INCH 72 /* How many pt's in an inch */ /* Text formatting bits for */ #define MSIM_TEXT_BOLD 1 #define MSIM_TEXT_ITALIC 2 #define MSIM_TEXT_UNDERLINE 4 /* Default baseline size of purple's fonts, in points. What is size 3 in points. * _font_scale specifies scaling factor relative to this point size. Note this * is only the default; it is configurable in account options. */ #define MSIM_BASE_FONT_POINT_SIZE 8 /* Default display's DPI. 96 is common but it can differ. Also configurable * in account options. */ #define MSIM_DEFAULT_DPI 96 /* round is part of C99, but sometimes is unavailable before then. * Based on http://forums.belution.com/en/cpp/000/050/13.shtml */ static double msim_round(double value) { if (value < 0) { return -(floor(-value + 0.5)); } else { return floor( value + 0.5); } } /** * Convert typographical font point size to HTML font size. * Based on libpurple/gtkimhtml.c */ static guint msim_point_to_purple_size(MsimSession *session, guint point) { guint size, this_point, base; base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE); for (size = 0; size < MAX_FONT_SIZE; ++size) { this_point = (guint)msim_round(base * _font_scale[size]); if (this_point >= point) { purple_debug_info("msim", "msim_point_to_purple_size: %d pt -> size=%d\n", point, size); return size; } } /* No HTML font size was this big; return largest possible. */ return this_point; } /** * Convert HTML font size to point size. */ static guint msim_purple_size_to_point(MsimSession *session, guint size) { gdouble scale; guint point; guint base; scale = _font_scale[CLAMP(size, 1, MAX_FONT_SIZE) - 1]; base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE); point = (guint)msim_round(scale * base); purple_debug_info("msim", "msim_purple_size_to_point: size=%d -> %d pt\n", size, point); return point; } /** * Convert a msim markup font pixel height to the more usual point size, for incoming messages. */ static guint msim_height_to_point(MsimSession *session, guint height) { guint dpi; dpi = purple_account_get_int(session->account, "dpi", MSIM_DEFAULT_DPI); return (guint)msim_round((POINTS_PER_INCH * 1. / dpi) * height); /* See also: libpurple/protocols/bonjour/jabber.c * _font_size_ichat_to_purple */ } /** * Convert point size to msim pixel height font size specification, for outgoing messages. */ static guint msim_point_to_height(MsimSession *session, guint point) { guint dpi; dpi = purple_account_get_int(session->account, "dpi", MSIM_DEFAULT_DPI); return (guint)msim_round((dpi * 1. / POINTS_PER_INCH) * point); } /** * Convert the msim markup (font) tag into HTML. */ static void msim_markup_f_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) { const gchar *face, *height_str, *decor_str; GString *gs_end, *gs_begin; guint decor, height; face = xmlnode_get_attrib(root, "f"); height_str = xmlnode_get_attrib(root, "h"); decor_str = xmlnode_get_attrib(root, "s"); /* Validate the font face, to avoid constructing invalid HTML later */ if (face != NULL && strchr(face, '\'') != NULL) face = NULL; height = height_str != NULL ? atol(height_str) : 12; decor = decor_str != NULL ? atol(decor_str) : 0; /* * The HTML we're constructing here is a bit redudant. Ideally we * would use only the font-family and font-size CSS span, but Pidgin * doesn't support it (it's included for other UIs). For Pidgin we * wrap the whole thing in an ugly font tag, and Pidgin will happily * ignore the . */ gs_begin = g_string_new(""); if (height && !face) { guint point_size = msim_height_to_point(session, height); g_string_printf(gs_begin, "", msim_point_to_purple_size(session, point_size), point_size); } else if (height && face) { guint point_size = msim_height_to_point(session, height); g_string_printf(gs_begin, "", face, msim_point_to_purple_size(session, point_size), face, point_size); } else { g_string_printf(gs_begin, ""); } gs_end = g_string_new(""); if (decor & MSIM_TEXT_BOLD) { g_string_append(gs_begin, ""); g_string_prepend(gs_end, ""); } if (decor & MSIM_TEXT_ITALIC) { g_string_append(gs_begin, ""); g_string_append(gs_end, ""); } if (decor & MSIM_TEXT_UNDERLINE) { g_string_append(gs_begin, ""); g_string_append(gs_end, ""); } *begin = g_string_free(gs_begin, FALSE); *end = g_string_free(gs_end, FALSE); } /** * Convert a msim markup color to a color suitable for libpurple. * * @param msim Either a color name, or an rgb(x,y,z) code. * * @return A new string, either a color name or #rrggbb code. Must g_free(). */ static char * msim_color_to_purple(const char *msim) { guint red, green, blue; if (!msim) { return g_strdup("black"); } if (sscanf(msim, "rgb(%d,%d,%d)", &red, &green, &blue) != 3) { /* Color name. */ return g_strdup(msim); } /* TODO: rgba (alpha). */ return g_strdup_printf("#%.2x%.2x%.2x", red, green, blue); } /** * Convert the msim markup (anchor) tag into HTML. */ static void msim_markup_a_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) { const gchar *href; href = xmlnode_get_attrib(root, "h"); if (!href) { href = ""; } *begin = g_strdup_printf("%s", href, href); *end = g_strdup(""); } /** * Convert the msim markup

(paragraph) tag into HTML. */ static void msim_markup_p_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) { /* Just pass through unchanged. * * Note: attributes currently aren't passed, if there are any. */ *begin = g_strdup("

"); *end = g_strdup("

"); } /** * Convert the msim markup tag (text color) into HTML. */ static void msim_markup_c_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) { const gchar *color; gchar *purple_color; color = xmlnode_get_attrib(root, "v"); if (!color) { purple_debug_info("msim", "msim_markup_c_to_html: tag w/o v attr\n"); *begin = g_strdup(""); *end = g_strdup(""); /* TODO: log as unrecognized */ return; } purple_color = msim_color_to_purple(color); #ifdef USE_CSS_FORMATTING *begin = g_strdup_printf("", purple_color); *end = g_strdup(""); #else *begin = g_strdup_printf("", purple_color); *end = g_strdup(""); #endif g_free(purple_color); } /** * Convert the msim markup tag (background color) into HTML. */ static void msim_markup_b_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) { const gchar *color; gchar *purple_color; color = xmlnode_get_attrib(root, "v"); if (!color) { *begin = g_strdup(""); *end = g_strdup(""); purple_debug_info("msim", "msim_markup_b_to_html: w/o v attr\n"); /* TODO: log as unrecognized. */ return; } purple_color = msim_color_to_purple(color); #ifdef USE_CSS_FORMATTING *begin = g_strdup_printf("", purple_color); *end = g_strdup(""); #else *begin = g_strdup_printf("", purple_color); *end = g_strdup(""); #endif g_free(purple_color); } /** * Convert the msim markup tag (emoticon image) into HTML. */ static void msim_markup_i_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) { const gchar *name; guint i; struct MSIM_EMOTICON *emote; name = xmlnode_get_attrib(root, "n"); if (!name) { purple_debug_info("msim", "msim_markup_i_to_html: w/o n\n"); *begin = g_strdup(""); *end = g_strdup(""); /* TODO: log as unrecognized */ return; } /* Find and use canonical form of smiley symbol. */ for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) { if (g_str_equal(name, emote->name)) { *begin = g_strdup(emote->symbol); *end = g_strdup(""); return; } } /* Couldn't find it, sorry. Try to degrade gracefully. */ *begin = g_strdup_printf("**%s**", name); *end = g_strdup(""); } /** * Convert an individual msim markup tag to HTML. */ static int msim_markup_tag_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) { g_return_val_if_fail(root != NULL, 0); if (g_str_equal(root->name, "f")) { msim_markup_f_to_html(session, root, begin, end); } else if (g_str_equal(root->name, "a")) { msim_markup_a_to_html(session, root, begin, end); } else if (g_str_equal(root->name, "p")) { msim_markup_p_to_html(session, root, begin, end); } else if (g_str_equal(root->name, "c")) { msim_markup_c_to_html(session, root, begin, end); } else if (g_str_equal(root->name, "b")) { msim_markup_b_to_html(session, root, begin, end); } else if (g_str_equal(root->name, "i")) { msim_markup_i_to_html(session, root, begin, end); } else { purple_debug_info("msim", "msim_markup_tag_to_html: " "unknown tag name=%s, ignoring\n", root->name ? root->name : "(NULL)"); *begin = g_strdup(""); *end = g_strdup(""); } return 0; } /** * Convert an individual HTML tag to msim markup. */ static int html_tag_to_msim_markup(MsimSession *session, xmlnode *root, gchar **begin, gchar **end) { int ret = 0; if (!purple_utf8_strcasecmp(root->name, "root") || !purple_utf8_strcasecmp(root->name, "html")) { *begin = g_strdup(""); *end = g_strdup(""); /* TODO: Coalesce nested tags into one tag! * Currently, the 's' value will be overwritten when b/i/u is nested * within another one, and only the inner-most formatting will be * applied to the text. */ } else if (!purple_utf8_strcasecmp(root->name, "b")) { if (root->child->type == XMLNODE_TYPE_DATA) { *begin = g_strdup_printf("", MSIM_TEXT_BOLD); *end = g_strdup(""); } else { if (!purple_utf8_strcasecmp(root->child->name,"i")) { ret++; if (root->child->child->type == XMLNODE_TYPE_DATA) { *begin = g_strdup_printf("", (MSIM_TEXT_BOLD + MSIM_TEXT_ITALIC)); *end = g_strdup(""); } else { if (!purple_utf8_strcasecmp(root->child->child->name,"u")) { ret++; *begin = g_strdup_printf("", (MSIM_TEXT_BOLD + MSIM_TEXT_ITALIC + MSIM_TEXT_UNDERLINE)); *end = g_strdup(""); } } } else if (!purple_utf8_strcasecmp(root->child->name,"u")) { ret++; *begin = g_strdup_printf("", (MSIM_TEXT_BOLD + MSIM_TEXT_UNDERLINE)); *end = g_strdup(""); } } } else if (!purple_utf8_strcasecmp(root->name, "i")) { if (root->child->type == XMLNODE_TYPE_DATA) { *begin = g_strdup_printf("", MSIM_TEXT_ITALIC); *end = g_strdup(""); } else { if (!purple_utf8_strcasecmp(root->child->name,"u")) { ret++; *begin = g_strdup_printf("", (MSIM_TEXT_ITALIC + MSIM_TEXT_UNDERLINE)); *end = g_strdup(""); } } } else if (!purple_utf8_strcasecmp(root->name, "u")) { *begin = g_strdup_printf("", MSIM_TEXT_UNDERLINE); *end = g_strdup(""); } else if (!purple_utf8_strcasecmp(root->name, "a")) { const gchar *href; gchar *link_text; href = xmlnode_get_attrib(root, "href"); if (!href) { href = xmlnode_get_attrib(root, "HREF"); } link_text = xmlnode_get_data(root); if (href) { if (g_str_equal(link_text, href)) { /* Purple gives us: URL * Translate to * Displayed as text of URL with link to URL */ *begin = g_strdup_printf("", href); } else { /* But if we get: text * Translate to: text: * * Because official client only supports self-closed * tags; you can't change the link text. */ *begin = g_strdup_printf("%s: ", link_text, href); } } else { *begin = g_strdup(""); } /* Sorry, kid. MySpace doesn't support you within tags. */ xmlnode_free(root->child); g_free(link_text); root->child = NULL; *end = g_strdup(""); } else if (!purple_utf8_strcasecmp(root->name, "font")) { GString *tmpbegin, *tmpend; const gchar *size; const gchar *face; const gchar *color; size = xmlnode_get_attrib(root, "size"); face = xmlnode_get_attrib(root, "face"); color = xmlnode_get_attrib(root, "color"); tmpbegin = g_string_new(""); if (face != NULL) g_string_append_printf(tmpbegin, " f='%s'", face); if (size != NULL) g_string_append_printf(tmpbegin, " h='%d'", msim_point_to_height(session, msim_purple_size_to_point(session, atoi(size)))); /* Close the tag */ g_string_append(tmpbegin, ">"); if (color != NULL) { g_string_append_printf(tmpbegin, "", color); g_string_prepend(tmpend, ""); } *begin = g_string_free(tmpbegin, FALSE); *end = g_string_free(tmpend, FALSE); } else if (!purple_utf8_strcasecmp(root->name, "body")) { const gchar *bgcolor; bgcolor = xmlnode_get_attrib(root, "bgcolor"); if (bgcolor != NULL) { *begin = g_strdup_printf("", bgcolor); *end = g_strdup(""); } } else { gchar *err; #ifdef MSIM_MARKUP_SHOW_UNKNOWN_TAGS *begin = g_strdup_printf("[%s]", root->name); *end = g_strdup_printf("[/%s]", root->name); #else *begin = g_strdup(""); *end = g_strdup(""); #endif err = g_strdup_printf("html_tag_to_msim_markup: unrecognized " "HTML tag %s was sent by the IM client; ignoring", root->name ? root->name : "(NULL)"); msim_unrecognized(NULL, NULL, err); g_free(err); } return ret; } /** * Convert an xmlnode of msim markup or HTML to an HTML string or msim markup. * * @param f Function to convert tags. * * @return An HTML string. Caller frees. */ static void msim_convert_xmlnode(MsimSession *session, GString *out, xmlnode *root, MSIM_XMLNODE_CONVERT f, int nodes_processed) { xmlnode *node; gchar *begin, *inner, *end, *tmp; int descended = nodes_processed; if (!root || !root->name) return; purple_debug_info("msim", "msim_convert_xmlnode: got root=%s\n", root->name); begin = inner = end = NULL; if (descended == 0) /* We've not formatted this yet.. :) */ descended = f(session, root, &begin, &end); /* Get the value that our format function has already descended for us */ g_string_append(out, begin); g_free(begin); /* Loop over all child nodes. */ for (node = root->child; node != NULL; node = node->next) { switch (node->type) { case XMLNODE_TYPE_ATTRIB: /* Attributes handled above. */ break; case XMLNODE_TYPE_TAG: /* A tag or tag with attributes. Recursively descend. */ msim_convert_xmlnode(session, out, node, f, descended); purple_debug_info("msim", " ** node name=%s\n", node->name ? node->name : "(NULL)"); break; case XMLNODE_TYPE_DATA: /* Literal text. */ /* * TODO: Why is it necessary to escape here? I thought * node->data was already escaped? */ tmp = g_markup_escape_text(node->data, node->data_sz); g_string_append(out, tmp); g_free(tmp); break; default: purple_debug_warning("msim", "msim_convert_xmlnode: unknown node type\n"); } } /* TODO: Note that msim counts each piece of text enclosed by as * a paragraph and will display each on its own line. You actually have * to _nest_ tags to intersperse different text in one paragraph! * Comment out this line below to see. */ g_string_append(out, end); g_free(end); } /** * Convert XML to something based on MSIM_XMLNODE_CONVERT. */ static gchar * msim_convert_xml(MsimSession *session, const gchar *raw, MSIM_XMLNODE_CONVERT f) { xmlnode *root; GString *str; gchar *enclosed_raw; g_return_val_if_fail(raw != NULL, NULL); /* Enclose text in one root tag, to try to make it valid XML for parsing. */ enclosed_raw = g_strconcat("", raw, "", NULL); root = xmlnode_from_str(enclosed_raw, -1); if (!root) { purple_debug_warning("msim", "msim_markup_to_html: couldn't parse " "%s as XML, returning raw: %s\n", enclosed_raw, raw); /* TODO: msim_unrecognized */ g_free(enclosed_raw); return g_strdup(raw); } g_free(enclosed_raw); str = g_string_new(NULL); msim_convert_xmlnode(session, str, root, f, 0); xmlnode_free(root); purple_debug_info("msim", "msim_markup_to_html: returning %s\n", str->str); return g_string_free(str, FALSE); } /** * Convert plaintext smileys to markup tags. * * @param before Original text with ASCII smileys. Will be freed. * @return A new string with tags, if applicable. Must be g_free()'d. */ static gchar * msim_convert_smileys_to_markup(gchar *before) { gchar *old, *new, *replacement; guint i; struct MSIM_EMOTICON *emote; old = before; new = NULL; for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) { gchar *name, *symbol; name = emote->name; symbol = emote->symbol; replacement = g_strdup_printf("", name); purple_debug_info("msim", "msim_convert_smileys_to_markup: %s->%s\n", symbol ? symbol : "(NULL)", replacement ? replacement : "(NULL)"); new = purple_strreplace(old, symbol, replacement); g_free(replacement); g_free(old); old = new; } return new; } /** * High-level function to convert MySpaceIM markup to Purple (HTML) markup. * * @return Purple markup string, must be g_free()'d. */ gchar * msim_markup_to_html(MsimSession *session, const gchar *raw) { return msim_convert_xml(session, raw, msim_markup_tag_to_html); } /** * High-level function to convert Purple (HTML) to MySpaceIM markup. * * TODO: consider using purple_markup_html_to_xhtml() to make valid XML. * * @return HTML markup string, must be g_free()'d. */ gchar * html_to_msim_markup(MsimSession *session, const gchar *raw) { gchar *markup; markup = msim_convert_xml(session, raw, html_tag_to_msim_markup); if (purple_account_get_bool(session->account, "emoticons", TRUE)) { /* Frees markup and allocates a new one. */ markup = msim_convert_smileys_to_markup(markup); } return markup; }