element will be given a class
+ * 'groupchat'. I can't add another '%@' in Template.html because
+ * that breaks style-specific Template.html's. I have to either use libxml
+ * or conveniently play with WebKit's javascript engine. The javascript
+ * engine should work, but it's not an identical behavior.
+ */
+static void
+webkit_set_groupchat (GtkWebView *webview)
+{
+ gtk_webview_safe_execute_script (webview, "document.getElementById('Chat').className = 'groupchat'");
+}
+
+
+/**
+ * Called when either a new PurpleConversation is created
+ * or when a PidginConversation changes its active PurpleConversation
+ * This will not change the theme if the theme is already set.
+ * (This is to prevent accidental theme changes if a new
+ * PurpleConversation gets added.
+ *
+ * FIXME: it's not at all clear to me as to how
+ * Adium themes handle the case when the PurpleConversation
+ * changes.
+ */
+static void
+init_theme_for_webkit (PurpleConversation *conv, char *style_dir)
+{
+ GtkWidget *webkit = PIDGIN_CONVERSATION(conv)->webview;
+ char *header, *footer;
+ char *template;
+
+ char* basedir;
+ char* baseuri;
+ PidginMessageStyle *style, *oldStyle;
+ oldStyle = g_object_get_data (G_OBJECT(webkit), MESSAGE_STYLE_KEY);
+
+ if (oldStyle) return;
+
+ purple_debug_info ("webkit", "loading %s\n", style_dir);
+ style = pidgin_message_style_load (style_dir);
+ g_assert (style);
+ g_assert (style->template_html); /* debugging test? */
+
+ basedir = g_build_filename (style->style_dir, "Contents", "Resources", "Template.html", NULL);
+ baseuri = g_strdup_printf ("file://%s", basedir);
+ header = replace_header_tokens(style->header_html, strlen(style->header_html), conv);
+ g_assert (style);
+ footer = replace_header_tokens(style->footer_html, strlen(style->footer_html), conv);
+ template = replace_template_tokens(style, style->template_html, strlen(style->template_html) + strlen(style->header_html), header, footer);
+
+ g_assert(template);
+
+ purple_debug_info ("webkit", "template: %s\n", template);
+
+ set_theme_webkit_settings (WEBKIT_WEB_VIEW(webkit), style);
+ webkit_web_view_load_string(WEBKIT_WEB_VIEW(webkit), template, "text/html", "UTF-8", baseuri);
+
+ PidginMessageStyle *copy = pidgin_message_style_copy (style);
+ g_object_set_data (G_OBJECT(webkit), MESSAGE_STYLE_KEY, copy);
+
+ pidgin_message_style_unref (style);
+ /* I need to unref this style when the webkit object destroys */
+ g_signal_connect (G_OBJECT(webkit), "destroy", G_CALLBACK(webkit_on_webview_destroy), copy);
+
+ if (purple_conversation_get_type (conv) == PURPLE_CONV_TYPE_CHAT)
+ webkit_set_groupchat (GTK_WEBVIEW (webkit));
+ g_free (basedir);
+ g_free (baseuri);
+ g_free (header);
+ g_free (footer);
+ g_free (template);
+}
+
+
+/* restore the non theme version of the conversation window */
+static void
+finalize_theme_for_webkit (PurpleConversation *conv)
+{
+ GtkWidget *webview = PIDGIN_CONVERSATION(conv)->webview;
+ PidginMessageStyle *style = g_object_get_data (G_OBJECT(webview), MESSAGE_STYLE_KEY);
+
+ webkit_web_view_load_string(WEBKIT_WEB_VIEW(webview), "", "text/html", "UTF-8", "");
+
+ g_object_set_data (G_OBJECT(webview), MESSAGE_STYLE_KEY, NULL);
+ pidgin_message_style_unref (style);
+}
+
+static void
+webkit_on_webview_destroy (GtkObject *object, gpointer data)
+{
+ pidgin_message_style_unref ((PidginMessageStyle*) data);
+ g_object_set_data (G_OBJECT(object), MESSAGE_STYLE_KEY, NULL);
+}
+
+static gboolean webkit_on_displaying_im_msg (PurpleAccount *account,
+ const char* name,
+ char **pmessage,
+ PurpleConversation *conv,
+ PurpleMessageFlags flags,
+ gpointer data)
+{
+ GtkWidget *webkit;
+ char *message = *pmessage;
+ const char *alias = name; /* FIXME: signal doesn't give me alias */
+ char *stripped;
+ char *message_html;
+ char *msg;
+ char *escape;
+ char *script;
+ char *func = "appendMessage";
+ char *smileyed;
+ time_t mtime = time (NULL); /* FIXME: this should come from the write_conv calback, but the signal doesn't pass this to me */
+
+ PurpleMessageFlags old_flags = GPOINTER_TO_INT(purple_conversation_get_data(conv, "webkit-lastflags"));
+ PidginMessageStyle *style;
+
+ fprintf (stderr, "hmm.. here %s %s\n", name, message);
+ webkit = get_webkit(conv);
+ stripped = g_strdup(message);
+
+ style = g_object_get_data (G_OBJECT (webkit), MESSAGE_STYLE_KEY);
+ g_assert (style);
+
+ if (flags & PURPLE_MESSAGE_SEND && old_flags & PURPLE_MESSAGE_SEND) {
+ message_html = style->outgoing_next_content_html;
+ func = "appendNextMessage";
+ } else if (flags & PURPLE_MESSAGE_SEND) {
+ message_html = style->outgoing_content_html;
+ } else if (flags & PURPLE_MESSAGE_RECV && old_flags & PURPLE_MESSAGE_RECV) {
+ message_html = style->incoming_next_content_html;
+ func = "appendNextMessage";
+ } else if (flags & PURPLE_MESSAGE_RECV) {
+ message_html = style->incoming_content_html;
+ } else {
+ message_html = style->status_html;
+ }
+ purple_conversation_set_data(conv, "webkit-lastflags", GINT_TO_POINTER(flags));
+
+ smileyed = smiley_parse_markup(stripped, conv->account->protocol_id);
+ msg = replace_message_tokens(message_html, 0, conv, name, alias, smileyed, flags, mtime);
+ escape = gtk_webview_quote_js_string (msg);
+ script = g_strdup_printf("%s(%s)", func, escape);
+
+ purple_debug_info ("webkit", "JS: %s\n", script);
+ gtk_webview_safe_execute_script (GTK_WEBVIEW (webkit), script);
+
+ g_free(script);
+ g_free(smileyed);
+ g_free(msg);
+ g_free(stripped);
+ g_free(escape);
+
+ return TRUE; /* GtkConv should not handle this IM */
+}
+
+static gboolean webkit_on_displaying_chat_msg (PurpleAccount *account,
+ const char *who,
+ char **message,
+ PurpleConversation *conv,
+ PurpleMessageFlags flags,
+ gpointer userdata)
+{
+ /* handle exactly like an IM message for now */
+ return webkit_on_displaying_im_msg (account, who, message, conv, flags, NULL);
+}
+
+static void
+webkit_on_conversation_displayed (PidginConversation *gtkconv, gpointer data)
+{
+ init_theme_for_webkit (gtkconv->active_conv, cur_style_dir);
+}
+
+static void
+webkit_on_conversation_switched (PurpleConversation *conv, gpointer data)
+{
+ init_theme_for_webkit (conv, cur_style_dir);
+}
+
+static void
+webkit_on_conversation_hiding (PidginConversation *gtkconv, gpointer data)
+{
+ /*
+ * I'm not sure if I need to do anything here, but let's keep
+ * this anyway.
+ */
+}
+
+static GList*
+get_dir_dir_list (const char* dirname)
+{
+ GList *ret = NULL;
+ GDir *dir = g_dir_open (dirname, 0, NULL);
+ const char* subdir;
+
+ if (!dir) return NULL;
+ while ((subdir = g_dir_read_name (dir))) {
+ ret = g_list_append (ret, g_build_filename (dirname, subdir, NULL));
+ }
+
+ g_dir_close (dir);
+ return ret;
+}
+
+/**
+ * Get me a list of all the available themes specified by their
+ * directories. I don't guarrantee that these are valid themes, just
+ * that they are in the directories for themes.
+ */
+static GList*
+get_style_directory_list ()
+{
+ char *user_dir, *user_style_dir, *global_style_dir;
+ GList *list1, *list2;
+
+ user_dir = get_absolute_path (purple_user_dir ());
+
+ user_style_dir = g_build_filename (user_dir, "styles", NULL);
+ global_style_dir = g_build_filename (DATADIR, "pidgin", "styles", NULL);
+
+ list1 = get_dir_dir_list (user_style_dir);
+ list2 = get_dir_dir_list (global_style_dir);
+
+ g_free (global_style_dir);
+ g_free (user_style_dir);
+ g_free (user_dir);
+
+ return g_list_concat (list1, list2);
+}
+
+/**
+ * use heuristics or previous user options to figure out what
+ * theme to use as default in this Pidgin instance.
+ */
+static void
+style_set_default ()
+{
+ GList* styles = get_style_directory_list (), *iter;
+ const char *stylepath = purple_prefs_get_string ("/plugins/gtk/adiumthemes/stylepath");
+ g_assert (cur_style_dir == NULL);
+
+ if (stylepath)
+ styles = g_list_prepend (styles, g_strdup (stylepath));
+
+ /* pick any one that works. Note that we have first preference
+ * for the one in the userdir */
+ for (iter = styles; iter; iter = g_list_next (iter)) {
+ PidginMessageStyle *style = pidgin_message_style_load (iter->data);
+ if (style) {
+ cur_style_dir = (char*) g_strdup (iter->data);
+ pidgin_message_style_unref (style);
+ break;
+ }
+ purple_debug_info ("webkit", "Style %s is invalid\n", (char*) iter->data);
+ }
+
+ for (iter = styles; iter; iter = g_list_next (iter))
+ g_free (iter->data);
+ g_list_free (styles);
+}
+
+static gboolean
+plugin_load(PurplePlugin *plugin)
+{
+ style_set_default ();
+ if (!cur_style_dir) return FALSE; /* couldn't find a style */
+
+ purple_signal_connect (pidgin_conversations_get_handle (),
+ "displaying-im-msg",
+ webkit_plugin_get_handle (),
+ PURPLE_CALLBACK(webkit_on_displaying_im_msg),
+ NULL);
+
+ purple_signal_connect (pidgin_conversations_get_handle (),
+ "displaying-chat-msg",
+ webkit_plugin_get_handle (),
+ PURPLE_CALLBACK(webkit_on_displaying_chat_msg),
+ NULL);
+
+ purple_signal_connect (pidgin_conversations_get_handle (),
+ "conversation-displayed",
+ webkit_plugin_get_handle (),
+ PURPLE_CALLBACK(webkit_on_conversation_displayed),
+ NULL);
+
+ purple_signal_connect (pidgin_conversations_get_handle (),
+ "conversation-switched",
+ webkit_plugin_get_handle (),
+ PURPLE_CALLBACK(webkit_on_conversation_switched),
+ NULL);
+
+ purple_signal_connect (pidgin_conversations_get_handle (),
+ "conversation-hiding",
+ webkit_plugin_get_handle (),
+ PURPLE_CALLBACK(webkit_on_conversation_hiding),
+ NULL);
+
+ /* finally update each of the existing conversation windows */
+ {
+ GList* list = purple_get_conversations ();
+ for (;list; list = g_list_next(list))
+ init_theme_for_webkit (list->data, cur_style_dir);
+
+ }
+ return TRUE;
+}
+
+static gboolean
+plugin_unload(PurplePlugin *plugin)
+{
+ GList *list;
+
+ webkit_plugin_free_handle ();
+ cur_style_dir = NULL;
+ list = purple_get_conversations ();
+ while (list) {
+ finalize_theme_for_webkit(list->data);
+ list = g_list_next(list);
+ }
+
+ return TRUE;
+}
+
+/*
+ * UI config code
+ */
+
+static void
+style_changed (GtkWidget* combobox, gpointer null)
+{
+ char *name = gtk_combo_box_get_active_text (GTK_COMBO_BOX(combobox));
+ GtkWidget *dialog;
+ GList *styles = get_style_directory_list (), *iter;
+
+ /* find the full path for this name, I wish I could store this info in the combobox itself. :( */
+ for (iter = styles; iter; iter = g_list_next(iter)) {
+ char* basename = g_path_get_basename (iter->data);
+ if (g_str_equal (basename, name)) {
+ g_free (basename);
+ break;
+ }
+ g_free (basename);
+ }
+
+ g_assert (iter);
+ g_free (name);
+ g_free (cur_style_dir);
+ cur_style_dir = g_strdup (iter->data);;
+
+ /* inform the user that existing conversations haven't changed */
+ dialog = gtk_message_dialog_new (NULL, 0, GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "The style for existing conversations have not been changed. Please close and re-open the conversation for the changes to take effect.");
+ g_assert (dialog);
+ gtk_widget_show (dialog);
+ g_signal_connect_swapped (dialog, "response", G_CALLBACK(gtk_widget_destroy), dialog);
+}
+
+static GtkWidget*
+get_style_config_frame ()
+{
+ GtkWidget *combobox = gtk_combo_box_new_text ();
+ GList *styles = get_style_directory_list (), *iter;
+ int index = 0, selected = 0;
+
+ for (iter = styles; iter; iter = g_list_next (iter)) {
+ PidginMessageStyle *style = pidgin_message_style_load (iter->data);
+
+ if (style) {
+ char *text = g_path_get_basename (iter->data);
+ gtk_combo_box_append_text (GTK_COMBO_BOX(combobox), text);
+ g_free (text);
+
+ if (g_str_equal (iter->data, cur_style_dir))
+ selected = index;
+ index++;
+ pidgin_message_style_unref (style);
+ }
+ }
+ gtk_combo_box_set_active (GTK_COMBO_BOX(combobox), selected);
+ g_signal_connect_after (G_OBJECT(combobox), "changed", G_CALLBACK(style_changed), NULL);
+ return combobox;
+}
+
+static void
+variant_update_conversation (PurpleConversation *conv)
+{
+ PidginConversation *gtkconv = PIDGIN_CONVERSATION (conv);
+ WebKitWebView *webview = WEBKIT_WEB_VIEW (gtkconv->webview);
+ PidginMessageStyle *style = (PidginMessageStyle*) g_object_get_data (G_OBJECT(webview), MESSAGE_STYLE_KEY);
+ char *script;
+
+ g_assert (style);
+
+ script = g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\")", pidgin_message_style_get_css (style));
+ gtk_webview_safe_execute_script (GTK_WEBVIEW(webview), script);
+
+ set_theme_webkit_settings (WEBKIT_WEB_VIEW (gtkconv->webview), style);
+ g_free (script);
+}
+
+static void
+variant_changed (GtkWidget* combobox, gpointer null)
+{
+ char *name;
+ GList *list;
+ PidginMessageStyle *style = pidgin_message_style_load (cur_style_dir);
+
+ g_assert (style);
+ name = gtk_combo_box_get_active_text (GTK_COMBO_BOX (combobox));
+ pidgin_message_style_set_variant (style, name);
+ pidgin_message_style_save_state (style);
+
+ /* update conversations */
+ list = purple_get_conversations ();
+ while (list) {
+ variant_update_conversation (list->data);
+ list = g_list_next(list);
+ }
+
+ g_free (name);
+ pidgin_message_style_unref (style);
+}
+
+static GtkWidget *
+get_variant_config_frame()
+{
+ PidginMessageStyle *style = pidgin_message_style_load (cur_style_dir);
+ GList *variants = pidgin_message_style_get_variants (style), *iter;
+ char *cur_variant = pidgin_message_style_get_variant (style);
+ GtkWidget *combobox = gtk_combo_box_new_text();
+ int def = -1, index = 0;
+
+ pidgin_message_style_unref (style);
+
+ for (iter = variants; iter; iter = g_list_next (iter)) {
+ gtk_combo_box_append_text (GTK_COMBO_BOX(combobox), iter->data);
+
+ if (g_str_equal (cur_variant, iter->data))
+ def = index;
+ index ++;
+
+ }
+
+ gtk_combo_box_set_active (GTK_COMBO_BOX(combobox), def);
+ g_signal_connect (G_OBJECT(combobox), "changed", G_CALLBACK(variant_changed), NULL);
+
+ return combobox;
+}
+
+static void
+style_changed_reset_variants (GtkWidget* combobox, gpointer table)
+{
+ /* I hate to do this, I swear. But I don't know how to cleanly clean an existing combobox */
+ GtkWidget* variants = g_object_get_data (G_OBJECT(table), "variants-cbox");
+ gtk_widget_destroy (variants);
+ variants = get_variant_config_frame ();
+ gtk_table_attach_defaults (GTK_TABLE (table), variants, 1, 2, 1, 2);
+ gtk_widget_show_all (GTK_WIDGET(table));
+
+ g_object_set_data (G_OBJECT(table), "variants-cbox", variants);
+}
+
+static GtkWidget*
+get_config_frame(PurplePlugin* plugin)
+{
+ GtkWidget *table = gtk_table_new (2, 2, FALSE);
+ GtkWidget *style_config = get_style_config_frame ();
+ GtkWidget *variant_config = get_variant_config_frame ();
+
+ gtk_table_attach_defaults (GTK_TABLE(table), gtk_label_new ("Message Style"), 0, 1, 0, 1);
+ gtk_table_attach_defaults (GTK_TABLE(table), style_config, 1, 2, 0, 1);
+ gtk_table_attach_defaults (GTK_TABLE(table), gtk_label_new ("Style Variant"), 0, 1, 1, 2);
+ gtk_table_attach_defaults (GTK_TABLE(table), variant_config, 1, 2, 1, 2);
+
+
+ g_object_set_data (G_OBJECT(table), "variants-cbox", variant_config);
+ /* to clarify, this is a second signal connected on style config */
+ g_signal_connect_after (G_OBJECT(style_config), "changed", G_CALLBACK(style_changed_reset_variants), table);
+
+ return table;
+}
+
+PidginPluginUiInfo ui_info =
+{
+ get_config_frame,
+ 0, /* page_num (Reserved) */
+
+ /* padding */
+ NULL,
+ NULL,
+ NULL,
+ NULL
+};
+
+
+static PurplePluginInfo info =
+{
+ PURPLE_PLUGIN_MAGIC, /* Magic */
+ PURPLE_MAJOR_VERSION, /* Purple Major Version */
+ PURPLE_MINOR_VERSION, /* Purple Minor Version */
+ PURPLE_PLUGIN_STANDARD, /* plugin type */
+PIDGIN_PLUGIN_TYPE, /* ui requirement */
+ 0, /* flags */
+ NULL, /* dependencies */
+ PURPLE_PRIORITY_DEFAULT, /* priority */
+
+ PLUGIN_ID, /* plugin id */
+ NULL, /* name */
+ "0.1", /* version */
+ NULL, /* summary */
+ NULL, /* description */
+ PLUGIN_AUTHOR, /* author */
+ "http://pidgin.im", /* website */
+
+ plugin_load, /* load */
+ plugin_unload, /* unload */
+ NULL, /* destroy */
+
+ &ui_info, /* ui_info */
+ NULL, /* extra_info */
+ NULL, /* prefs_info */
+ NULL, /* actions */
+ NULL, /* reserved 1 */
+ NULL, /* reserved 2 */
+ NULL, /* reserved 3 */
+ NULL /* reserved 4 */
+};
+
+static void
+init_plugin(PurplePlugin *plugin) {
+ info.name = "Adium IMs";
+ info.summary = "Adium-like IMs with Pidgin";
+ info.description = "You can chat in Pidgin using Adium's WebKit view.";
+
+ purple_prefs_add_none ("/plugins");
+ purple_prefs_add_none ("/plugins/gtk");
+ purple_prefs_add_none ("/plugins/gtk/adiumthemes");
+ purple_prefs_add_string ("/plugins/gtk/adiumthemes/csspath", "");
+}
+
+PURPLE_INIT_PLUGIN(webkit, init_plugin, info)
diff -r b357216b7b79 -r 1ca2df744414 pidgin/smileyparser.c
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/smileyparser.c Tue Aug 25 17:01:49 2009 +0000
@@ -0,0 +1,144 @@
+
+#include
+#include "smileyparser.h"
+#include
+#include
+#include "gtkthemes.h"
+
+static char* get_fullpath (const char* filename)
+{
+ if (g_path_is_absolute (filename)) return g_strdup (filename);
+ else return g_build_path (g_get_current_dir (), filename, NULL);
+}
+
+static void
+parse_for_shortcut_plaintext (const char* text, const char* shortcut, const char* file, GString* ret)
+{
+ const char *tmp = text;
+
+ for(;*tmp;) {
+ const char *end = strstr (tmp, shortcut);
+ char *path;
+ char *escaped_path;
+
+ if (end == NULL) {
+ g_string_append (ret, tmp);
+ break;
+ }
+ path = get_fullpath (file);
+ escaped_path = g_markup_escape_text (path, -1);
+
+ g_string_append_len (ret, tmp, end-tmp);
+ g_string_append_printf (ret,"",
+ shortcut, escaped_path);
+ g_free (path);
+ g_free (escaped_path);
+ g_assert (strlen (tmp) >= strlen (shortcut));
+ tmp = end + strlen (shortcut);
+ }
+}
+
+static char*
+parse_for_shortcut (const char* markup, const char* shortcut, const char* file)
+{
+ GString* ret = g_string_new ("");
+ char *local_markup = g_strdup (markup);
+ char *escaped_shortcut = g_markup_escape_text (shortcut, -1);
+
+ char *temp = local_markup;
+
+ for (;*temp;) {
+ char *end = strchr (temp, '<');
+ char *end_of_tag;
+
+ if (!end) {
+ parse_for_shortcut_plaintext (temp, escaped_shortcut, file, ret);
+ break;
+ }
+
+ *end = 0;
+ parse_for_shortcut_plaintext (temp, escaped_shortcut, file, ret);
+ *end = '<';
+
+ /* if this is well-formed, then there should be no '>' within
+ * the tag. TODO: handle a comment tag better :( */
+ end_of_tag = strchr (end, '>');
+ if (!end_of_tag) {
+ g_string_append (ret, end);
+ break;
+ }
+
+ g_string_append_len (ret, end, end_of_tag-end+1);
+
+ temp = end_of_tag + 1;
+ }
+ g_free (local_markup);
+ g_free (escaped_shortcut);
+ return g_string_free (ret, FALSE);
+}
+
+static char*
+parse_for_purple_smiley (const char* markup, PurpleSmiley *smiley)
+{
+ char *file = purple_smiley_get_full_path (smiley);
+ char *ret = parse_for_shortcut (markup, purple_smiley_get_shortcut (smiley), file);
+ g_free (file);
+ return ret;
+}
+
+static char*
+parse_for_smiley_list (const char* markup, GHashTable* smileys)
+{
+ GHashTableIter iter;
+ char *key, *value;
+ char *ret = g_strdup (markup);
+
+ g_hash_table_iter_init (&iter, smileys);
+ while (g_hash_table_iter_next (&iter, (gpointer*)&key, (gpointer*)&value))
+ {
+ char* temp = parse_for_shortcut (ret, key, value);
+ g_free (ret);
+ ret = temp;
+ }
+ return ret;
+}
+
+char*
+smiley_parse_markup (const char* markup, const char *proto_id)
+{
+ GList *smileys = purple_smileys_get_all ();
+ char *temp = g_strdup (markup), *temp2;
+ struct smiley_list *list;
+ const char *proto_name = "default";
+
+ if (proto_id != NULL) {
+ PurplePlugin *proto;
+ proto = purple_find_prpl (proto_id);
+ proto_name = proto->info->name;
+ }
+
+ /* unnecessarily slow, but lets manage for now. */
+ for (; smileys; smileys = g_list_next (smileys)) {
+ temp2 = parse_for_purple_smiley (temp, PURPLE_SMILEY (smileys->data));
+ g_free (temp);
+ temp = temp2;
+ }
+
+ /* now for each theme smiley, observe that this does look nasty */
+
+ if (!current_smiley_theme || !(current_smiley_theme->list)) {
+ printf ("theme does not exist\n");
+ return temp;
+ }
+
+ for (list = current_smiley_theme->list; list; list = list->next) {
+ if (g_str_equal (list->sml, proto_name)) {
+ temp2 = parse_for_smiley_list (temp, list->files);
+ g_free (temp);
+ temp = temp2;
+ }
+ }
+
+ return temp;
+}
+
diff -r b357216b7b79 -r 1ca2df744414 pidgin/smileyparser.h
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/smileyparser.h Tue Aug 25 17:01:49 2009 +0000
@@ -0,0 +1,3 @@
+
+char*
+smiley_parse_markup (const char* markup, const char* sml);