[gnome-software] Parse the update descriptions as markdown



commit 7fce2f52705ca36870cd67b9489e4e59d37c54da
Author: Richard Hughes <richard hughsie com>
Date:   Tue Nov 12 11:30:48 2013 +0000

    Parse the update descriptions as markdown
    
    It's what gnome-packagekit and apper have done for a few releases now.

 src/Makefile.am        |    3 +
 src/gs-app-widget.c    |   18 +-
 src/gs-markdown.c      |  961 ++++++++++++++++++++++++++++++++++++++++++++++++
 src/gs-markdown.h      |   68 ++++
 src/gs-shell-updates.c |   22 +-
 5 files changed, 1068 insertions(+), 4 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index e41459f..d0ae431 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -120,6 +120,8 @@ gnome_software_service_SOURCES =                    \
        gs-screenshot.h                                 \
        gs-shell.c                                      \
        gs-shell.h                                      \
+       gs-markdown.c                                   \
+       gs-markdown.h                                   \
        gs-shell-details.c                              \
        gs-shell-details.h                              \
        gs-shell-category.c                             \
@@ -199,6 +201,7 @@ check_PROGRAMS =                                            \
 gs_self_test_SOURCES =                                         \
        gs-app.c                                                \
        gs-category.c                                           \
+       gs-markdown.c                                           \
        gs-plugin-loader-sync.c                                 \
        gs-plugin-loader.c                                      \
        gs-plugin.c                                             \
diff --git a/src/gs-app-widget.c b/src/gs-app-widget.c
index e39c8c4..77dd6a0 100644
--- a/src/gs-app-widget.c
+++ b/src/gs-app-widget.c
@@ -26,6 +26,7 @@
 #include <gtk/gtk.h>
 
 #include "gs-app-widget.h"
+#include "gs-markdown.h"
 #include "gs-utils.h"
 
 struct _GsAppWidgetPrivate
@@ -63,14 +64,26 @@ gs_app_widget_refresh (GsAppWidget *app_widget)
        GtkStyleContext *context;
        const gchar *tmp = NULL;
        GString *str = NULL;
+       gchar *update_desc = NULL;
 
        if (app_widget->priv->app == NULL)
                return;
 
        /* get the main body text */
        if (priv->show_update &&
-           gs_app_get_state (priv->app) == GS_APP_STATE_UPDATABLE)
+           gs_app_get_state (priv->app) == GS_APP_STATE_UPDATABLE) {
                tmp = gs_app_get_update_details (priv->app);
+               if (tmp != NULL) {
+                       GsMarkdown *markdown;
+                       markdown = gs_markdown_new ();
+                       gs_markdown_set_smart_quoting (markdown, TRUE);
+                       gs_markdown_set_autocode (markdown, TRUE);
+                       gs_markdown_set_output_kind (markdown, GS_MARKDOWN_OUTPUT_PANGO);
+                       update_desc = gs_markdown_parse (markdown, tmp);
+                       tmp = update_desc;
+                       g_object_unref (markdown);
+               }
+       }
        if (gs_app_get_kind (priv->app) == GS_APP_KIND_MISSING)
                tmp = gs_app_get_summary_missing (priv->app);
        if (tmp == NULL)
@@ -95,7 +108,7 @@ gs_app_widget_refresh (GsAppWidget *app_widget)
        str = g_string_new (tmp);
        gs_string_replace (str, "\n", " ");
 
-       gtk_label_set_label (GTK_LABEL (priv->description_label), str->str);
+       gtk_label_set_markup (GTK_LABEL (priv->description_label), str->str);
        g_string_free (str, TRUE);
 
        gtk_label_set_label (GTK_LABEL (priv->name_label),
@@ -174,6 +187,7 @@ gs_app_widget_refresh (GsAppWidget *app_widget)
        default:
                break;
        }
+       g_free (update_desc);
 }
 
 /**
diff --git a/src/gs-markdown.c b/src/gs-markdown.c
new file mode 100644
index 0000000..ef02dba
--- /dev/null
+++ b/src/gs-markdown.c
@@ -0,0 +1,961 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib.h>
+
+#include "gs-markdown.h"
+
+/*******************************************************************************
+ *
+ * This is a simple Markdown parser.
+ * It can output to Pango, HTML or plain text. The following limitations are
+ * already known, and properly deliberate:
+ *
+ * - No code section support
+ * - No ordered list support
+ * - No blockquote section support
+ * - No image support
+ * - No links or email support
+ * - No backslash escapes support
+ * - No HTML escaping support
+ * - Auto-escapes certain word patterns, like http://
+ *
+ * It does support the rest of the standard pretty well, although it's not
+ * been run against any conformance tests. The parsing is single pass, with
+ * a simple enumerated intepretor mode and a single line back-memory.
+ *
+ ******************************************************************************/
+
+typedef enum {
+       GS_MARKDOWN_MODE_BLANK,
+       GS_MARKDOWN_MODE_RULE,
+       GS_MARKDOWN_MODE_BULLETT,
+       GS_MARKDOWN_MODE_PARA,
+       GS_MARKDOWN_MODE_H1,
+       GS_MARKDOWN_MODE_H2,
+       GS_MARKDOWN_MODE_UNKNOWN
+} GsMarkdownMode;
+
+typedef struct {
+       const gchar *em_start;
+       const gchar *em_end;
+       const gchar *strong_start;
+       const gchar *strong_end;
+       const gchar *code_start;
+       const gchar *code_end;
+       const gchar *h1_start;
+       const gchar *h1_end;
+       const gchar *h2_start;
+       const gchar *h2_end;
+       const gchar *bullet_start;
+       const gchar *bullet_end;
+       const gchar *rule;
+} GsMarkdownTags;
+
+typedef struct {
+       GsMarkdownMode           mode;
+       GsMarkdownTags           tags;
+       GsMarkdownOutputKind     output;
+       gint                     max_lines;
+       gint                     line_count;
+       gboolean                 smart_quoting;
+       gboolean                 escape;
+       gboolean                 autocode;
+       GString                 *pending;
+       GString                 *processed;
+} GsMarkdownPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GsMarkdown, gs_markdown, G_TYPE_OBJECT)
+
+/**
+ * gs_markdown_to_text_line_is_rule:
+ *
+ * Horizontal rules are created by placing three or more hyphens, asterisks,
+ * or underscores on a line by themselves.
+ * You may use spaces between the hyphens or asterisks.
+ **/
+static gboolean
+gs_markdown_to_text_line_is_rule (const gchar *line)
+{
+       guint i;
+       guint len;
+       guint count = 0;
+       gchar *copy = NULL;
+       gboolean ret = FALSE;
+
+       len = strlen (line);
+       if (len == 0)
+               goto out;
+
+       /* replace non-rule chars with ~ */
+       copy = g_strdup (line);
+       g_strcanon (copy, "-*_ ", '~');
+       for (i = 0; i < len; i++) {
+               if (copy[i] == '~')
+                       goto out;
+               if (copy[i] != ' ')
+                       count++;
+       }
+
+       /* if we matched, return true */
+       if (count >= 3)
+               ret = TRUE;
+out:
+       g_free (copy);
+       return ret;
+}
+
+/**
+ * gs_markdown_to_text_line_is_bullet:
+ **/
+static gboolean
+gs_markdown_to_text_line_is_bullet (const gchar *line)
+{
+       return (g_str_has_prefix (line, "- ") ||
+               g_str_has_prefix (line, "* ") ||
+               g_str_has_prefix (line, "+ ") ||
+               g_str_has_prefix (line, " - ") ||
+               g_str_has_prefix (line, " * ") ||
+               g_str_has_prefix (line, " + "));
+}
+
+/**
+ * gs_markdown_to_text_line_is_header1:
+ **/
+static gboolean
+gs_markdown_to_text_line_is_header1 (const gchar *line)
+{
+       return g_str_has_prefix (line, "# ");
+}
+
+/**
+ * gs_markdown_to_text_line_is_header2:
+ **/
+static gboolean
+gs_markdown_to_text_line_is_header2 (const gchar *line)
+{
+       return g_str_has_prefix (line, "## ");
+}
+
+/**
+ * gs_markdown_to_text_line_is_header1_type2:
+ **/
+static gboolean
+gs_markdown_to_text_line_is_header1_type2 (const gchar *line)
+{
+       return g_str_has_prefix (line, "===");
+}
+
+/**
+ * gs_markdown_to_text_line_is_header2_type2:
+ **/
+static gboolean
+gs_markdown_to_text_line_is_header2_type2 (const gchar *line)
+{
+       return g_str_has_prefix (line, "---");
+}
+
+#if 0
+/**
+ * gs_markdown_to_text_line_is_code:
+ **/
+static gboolean
+gs_markdown_to_text_line_is_code (const gchar *line)
+{
+       return (g_str_has_prefix (line, "    ") ||
+               g_str_has_prefix (line, "\t"));
+}
+
+/**
+ * gs_markdown_to_text_line_is_blockquote:
+ **/
+static gboolean
+gs_markdown_to_text_line_is_blockquote (const gchar *line)
+{
+       return (g_str_has_prefix (line, "> "));
+}
+#endif
+
+/**
+ * gs_markdown_to_text_line_is_blank:
+ **/
+static gboolean
+gs_markdown_to_text_line_is_blank (const gchar *line)
+{
+       guint i;
+       guint len;
+       gboolean ret = FALSE;
+
+       /* a line with no characters is blank by definition */
+       len = strlen (line);
+       if (len == 0) {
+               ret = TRUE;
+               goto out;
+       }
+
+       /* find if there are only space chars */
+       for (i = 0; i < len; i++) {
+               if (line[i] != ' ' && line[i] != '\t')
+                       goto out;
+       }
+
+       /* if we matched, return true */
+       ret = TRUE;
+out:
+       return ret;
+}
+
+/**
+ * gs_markdown_replace:
+ **/
+static gchar *
+gs_markdown_replace (const gchar *haystack,
+                    const gchar *needle,
+                    const gchar *replace)
+{
+       gchar *new;
+       gchar **split;
+
+       split = g_strsplit (haystack, needle, -1);
+       new = g_strjoinv (replace, split);
+       g_strfreev (split);
+
+       return new;
+}
+
+/**
+ * gs_markdown_strstr_spaces:
+ **/
+static gchar *
+gs_markdown_strstr_spaces (const gchar *haystack, const gchar *needle)
+{
+       gchar *found;
+       const gchar *haystack_new = haystack;
+
+retry:
+       /* don't find if surrounded by spaces */
+       found = strstr (haystack_new, needle);
+       if (found == NULL)
+               return NULL;
+
+       /* start of the string, always valid */
+       if (found == haystack)
+               return found;
+
+       /* end of the string, always valid */
+       if (*(found-1) == ' ' && *(found+1) == ' ') {
+               haystack_new = found+1;
+               goto retry;
+       }
+       return found;
+}
+
+/**
+ * gs_markdown_to_text_line_formatter:
+ **/
+static gchar *
+gs_markdown_to_text_line_formatter (const gchar *line,
+                                   const gchar *formatter,
+                                   const gchar *left,
+                                   const gchar *right)
+{
+       guint len;
+       gchar *str1;
+       gchar *str2;
+       gchar *start = NULL;
+       gchar *middle = NULL;
+       gchar *end = NULL;
+       gchar *copy = NULL;
+       gchar *data = NULL;
+       gchar *temp;
+
+       /* needed to know for shifts */
+       len = strlen (formatter);
+       if (len == 0)
+               goto out;
+
+       /* find sections */
+       copy = g_strdup (line);
+       str1 = gs_markdown_strstr_spaces (copy, formatter);
+       if (str1 != NULL) {
+               *str1 = '\0';
+               str2 = gs_markdown_strstr_spaces (str1+len, formatter);
+               if (str2 != NULL) {
+                       *str2 = '\0';
+                       middle = str1 + len;
+                       start = copy;
+                       end = str2 + len;
+               }
+       }
+
+       /* if we found, replace and keep looking for the same string */
+       if (start != NULL && middle != NULL && end != NULL) {
+               temp = g_strdup_printf ("%s%s%s%s%s", start, left, middle, right, end);
+               /* recursive */
+               data = gs_markdown_to_text_line_formatter (temp, formatter, left, right);
+               g_free (temp);
+       } else {
+               /* not found, keep return as-is */
+               data = g_strdup (line);
+       }
+out:
+       g_free (copy);
+       return data;
+}
+
+/**
+ * gs_markdown_to_text_line_format_sections:
+ **/
+static gchar *
+gs_markdown_to_text_line_format_sections (GsMarkdown *self, const gchar *line)
+{
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       gchar *data = g_strdup (line);
+       gchar *temp;
+
+       /* bold1 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "**",
+                                                  priv->tags.strong_start,
+                                                  priv->tags.strong_end);
+       g_free (temp);
+
+       /* bold2 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "__",
+                                                  priv->tags.strong_start,
+                                                  priv->tags.strong_end);
+       g_free (temp);
+
+       /* italic1 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "*",
+                                                  priv->tags.em_start,
+                                                  priv->tags.em_end);
+       g_free (temp);
+
+       /* italic2 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "_",
+                                                  priv->tags.em_start,
+                                                  priv->tags.em_end);
+       g_free (temp);
+
+       /* em-dash */
+       temp = data;
+       data = gs_markdown_replace (temp, " -- ", " — ");
+       g_free (temp);
+
+       /* smart quoting */
+       if (priv->smart_quoting) {
+               temp = data;
+               data = gs_markdown_to_text_line_formatter (temp, "\"", "“", "”");
+               g_free (temp);
+
+               temp = data;
+               data = gs_markdown_to_text_line_formatter (temp, "'", "‘", "’");
+               g_free (temp);
+       }
+
+       return data;
+}
+
+/**
+ * gs_markdown_to_text_line_format:
+ **/
+static gchar *
+gs_markdown_to_text_line_format (GsMarkdown *self, const gchar *line)
+{
+       GString *string;
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       gboolean mode = FALSE;
+       gchar **codes;
+       gchar *text;
+       guint i;
+
+       /* optimise the trivial case where we don't have any code tags */
+       text = strstr (line, "`");
+       if (text == NULL) {
+               text = gs_markdown_to_text_line_format_sections (self, line);
+               goto out;
+       }
+
+       /* we want to parse the code sections without formatting */
+       codes = g_strsplit (line, "`", -1);
+       string = g_string_new ("");
+       for (i=0; codes[i] != NULL; i++) {
+               if (!mode) {
+                       text = gs_markdown_to_text_line_format_sections (self, codes[i]);
+                       g_string_append (string, text);
+                       g_free (text);
+                       mode = TRUE;
+               } else {
+                       /* just append without formatting */
+                       g_string_append (string, priv->tags.code_start);
+                       g_string_append (string, codes[i]);
+                       g_string_append (string, priv->tags.code_end);
+                       mode = FALSE;
+               }
+       }
+       text = g_string_free (string, FALSE);
+out:
+       return text;
+}
+
+/**
+ * gs_markdown_add_pending:
+ **/
+static gboolean
+gs_markdown_add_pending (GsMarkdown *self, const gchar *line)
+{
+       gchar *copy;
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+
+       /* would put us over the limit */
+       if (priv->max_lines > 0 && priv->line_count >= priv->max_lines)
+               return FALSE;
+
+       copy = g_strdup (line);
+
+       /* strip leading and trailing spaces */
+       g_strstrip (copy);
+
+       /* append */
+       g_string_append_printf (priv->pending, "%s ", copy);
+
+       g_free (copy);
+       return TRUE;
+}
+
+/**
+ * gs_markdown_add_pending_header:
+ **/
+static gboolean
+gs_markdown_add_pending_header (GsMarkdown *self, const gchar *line)
+{
+       gchar *copy;
+       gboolean ret;
+
+       /* strip trailing # */
+       copy = g_strdup (line);
+       g_strdelimit (copy, "#", ' ');
+       ret = gs_markdown_add_pending (self, copy);
+       g_free (copy);
+       return ret;
+}
+
+/**
+ * gs_markdown_count_chars_in_word:
+ **/
+static guint
+gs_markdown_count_chars_in_word (const gchar *text, gchar find)
+{
+       guint i;
+       guint len;
+       guint count = 0;
+
+       /* get length */
+       len = strlen (text);
+       if (len == 0)
+               goto out;
+
+       /* find matching chars */
+       for (i = 0; i < len; i++) {
+               if (text[i] == find)
+                       count++;
+       }
+out:
+       return count;
+}
+
+/**
+ * gs_markdown_word_is_code:
+ **/
+static gboolean
+gs_markdown_word_is_code (const gchar *text)
+{
+       /* already code */
+       if (g_str_has_prefix (text, "`"))
+               return FALSE;
+       if (g_str_has_suffix (text, "`"))
+               return FALSE;
+
+       /* paths */
+       if (g_str_has_prefix (text, "/"))
+               return TRUE;
+
+       /* bugzillas */
+       if (g_str_has_prefix (text, "#"))
+               return TRUE;
+
+       /* uri's */
+       if (g_str_has_prefix (text, "http://";))
+               return TRUE;
+       if (g_str_has_prefix (text, "https://";))
+               return TRUE;
+       if (g_str_has_prefix (text, "ftp://";))
+               return TRUE;
+
+       /* patch files */
+       if (g_strrstr (text, ".patch") != NULL)
+               return TRUE;
+       if (g_strrstr (text, ".diff") != NULL)
+               return TRUE;
+
+       /* function names */
+       if (g_strrstr (text, "()") != NULL)
+               return TRUE;
+
+       /* email addresses */
+       if (g_strrstr (text, "@") != NULL)
+               return TRUE;
+
+       /* compiler defines */
+       if (text[0] != '_' &&
+           gs_markdown_count_chars_in_word (text, '_') > 1)
+               return TRUE;
+
+       /* nothing special */
+       return FALSE;
+}
+
+/**
+ * gs_markdown_word_auto_format_code:
+ **/
+static gchar *
+gs_markdown_word_auto_format_code (const gchar *text)
+{
+       guint i;
+       gchar *temp;
+       gchar **words;
+       gboolean ret = FALSE;
+
+       /* split sentence up with space */
+       words = g_strsplit (text, " ", -1);
+
+       /* search each word */
+       for (i=0; words[i] != NULL; i++) {
+               if (gs_markdown_word_is_code (words[i])) {
+                       temp = g_strdup_printf ("`%s`", words[i]);
+                       g_free (words[i]);
+                       words[i] = temp;
+                       ret = TRUE;
+               }
+       }
+
+       /* no replacements, so just return a copy */
+       if (!ret) {
+               temp = g_strdup (text);
+               goto out;
+       }
+
+       /* join the array back into a string */
+       temp = g_strjoinv (" ", words);
+out:
+       g_strfreev (words);
+       return temp;
+}
+
+/**
+ * gs_markdown_flush_pending:
+ **/
+static void
+gs_markdown_flush_pending (GsMarkdown *self)
+{
+       gchar *copy;
+       gchar *temp;
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+
+       /* no data yet */
+       if (priv->mode == GS_MARKDOWN_MODE_UNKNOWN)
+               return;
+
+       /* remove trailing spaces */
+       while (g_str_has_suffix (priv->pending->str, " "))
+               g_string_set_size (priv->pending, priv->pending->len - 1);
+
+       /* pango requires escaping */
+       copy = g_strdup (priv->pending->str);
+       if (!priv->escape && priv->output == GS_MARKDOWN_OUTPUT_PANGO) {
+               g_strdelimit (copy, "<", '(');
+               g_strdelimit (copy, ">", ')');
+       }
+
+       /* check words for code */
+       if (priv->autocode &&
+           (priv->mode == GS_MARKDOWN_MODE_PARA ||
+            priv->mode == GS_MARKDOWN_MODE_BULLETT)) {
+               temp = gs_markdown_word_auto_format_code (copy);
+               g_free (copy);
+               copy = temp;
+       }
+
+       /* escape */
+       if (priv->escape) {
+               temp = g_markup_escape_text (copy, -1);
+               g_free (copy);
+               copy = temp;
+       }
+
+       /* do formatting */
+       temp = gs_markdown_to_text_line_format (self, copy);
+       if (priv->mode == GS_MARKDOWN_MODE_BULLETT) {
+               g_string_append_printf (priv->processed, "%s%s%s\n",
+                                       priv->tags.bullet_start,
+                                       temp,
+                                       priv->tags.bullet_end);
+               priv->line_count++;
+       } else if (priv->mode == GS_MARKDOWN_MODE_H1) {
+               g_string_append_printf (priv->processed, "%s%s%s\n",
+                                       priv->tags.h1_start,
+                                       temp,
+                                       priv->tags.h1_end);
+       } else if (priv->mode == GS_MARKDOWN_MODE_H2) {
+               g_string_append_printf (priv->processed, "%s%s%s\n",
+                                       priv->tags.h2_start,
+                                       temp,
+                                       priv->tags.h2_end);
+       } else if (priv->mode == GS_MARKDOWN_MODE_PARA ||
+                  priv->mode == GS_MARKDOWN_MODE_RULE) {
+               g_string_append_printf (priv->processed, "%s\n", temp);
+               priv->line_count++;
+       }
+
+       g_debug ("adding '%s'", temp);
+
+       /* clear */
+       g_string_truncate (priv->pending, 0);
+       g_free (copy);
+       g_free (temp);
+}
+
+/**
+ * gs_markdown_to_text_line_process:
+ **/
+static gboolean
+gs_markdown_to_text_line_process (GsMarkdown *self, const gchar *line)
+{
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       gboolean ret;
+
+       /* blank */
+       ret = gs_markdown_to_text_line_is_blank (line);
+       if (ret) {
+               g_debug ("blank: '%s'", line);
+               gs_markdown_flush_pending (self);
+               /* a new line after a list is the end of list, not a gap */
+               if (priv->mode != GS_MARKDOWN_MODE_BULLETT)
+                       ret = gs_markdown_add_pending (self, "\n");
+               priv->mode = GS_MARKDOWN_MODE_BLANK;
+               goto out;
+       }
+
+       /* header1_type2 */
+       ret = gs_markdown_to_text_line_is_header1_type2 (line);
+       if (ret) {
+               g_debug ("header1_type2: '%s'", line);
+               if (priv->mode == GS_MARKDOWN_MODE_PARA)
+                       priv->mode = GS_MARKDOWN_MODE_H1;
+               goto out;
+       }
+
+       /* header2_type2 */
+       ret = gs_markdown_to_text_line_is_header2_type2 (line);
+       if (ret) {
+               g_debug ("header2_type2: '%s'", line);
+               if (priv->mode == GS_MARKDOWN_MODE_PARA)
+                       priv->mode = GS_MARKDOWN_MODE_H2;
+               goto out;
+       }
+
+       /* rule */
+       ret = gs_markdown_to_text_line_is_rule (line);
+       if (ret) {
+               g_debug ("rule: '%s'", line);
+               gs_markdown_flush_pending (self);
+               priv->mode = GS_MARKDOWN_MODE_RULE;
+               ret = gs_markdown_add_pending (self, priv->tags.rule);
+               goto out;
+       }
+
+       /* bullet */
+       ret = gs_markdown_to_text_line_is_bullet (line);
+       if (ret) {
+               g_debug ("bullet: '%s'", line);
+               gs_markdown_flush_pending (self);
+               priv->mode = GS_MARKDOWN_MODE_BULLETT;
+               ret = gs_markdown_add_pending (self, &line[2]);
+               goto out;
+       }
+
+       /* header1 */
+       ret = gs_markdown_to_text_line_is_header1 (line);
+       if (ret) {
+               g_debug ("header1: '%s'", line);
+               gs_markdown_flush_pending (self);
+               priv->mode = GS_MARKDOWN_MODE_H1;
+               ret = gs_markdown_add_pending_header (self, &line[2]);
+               goto out;
+       }
+
+       /* header2 */
+       ret = gs_markdown_to_text_line_is_header2 (line);
+       if (ret) {
+               g_debug ("header2: '%s'", line);
+               gs_markdown_flush_pending (self);
+               priv->mode = GS_MARKDOWN_MODE_H2;
+               ret = gs_markdown_add_pending_header (self, &line[3]);
+               goto out;
+       }
+
+       /* paragraph */
+       if (priv->mode == GS_MARKDOWN_MODE_BLANK ||
+           priv->mode == GS_MARKDOWN_MODE_UNKNOWN) {
+               gs_markdown_flush_pending (self);
+               priv->mode = GS_MARKDOWN_MODE_PARA;
+       }
+
+       /* add to pending */
+       g_debug ("continue: '%s'", line);
+       ret = gs_markdown_add_pending (self, line);
+out:
+       /* if we failed to add, we don't know the mode */
+       if (!ret)
+               priv->mode = GS_MARKDOWN_MODE_UNKNOWN;
+       return ret;
+}
+
+/**
+ * gs_markdown_set_output_kind:
+ **/
+gboolean
+gs_markdown_set_output_kind (GsMarkdown *self, GsMarkdownOutputKind output)
+{
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       gboolean ret = TRUE;
+
+       g_return_val_if_fail (GS_IS_MARKDOWN (self), FALSE);
+
+       /* PangoMarkup */
+       if (output == GS_MARKDOWN_OUTPUT_PANGO) {
+               priv->tags.em_start = "<i>";
+               priv->tags.em_end = "</i>";
+               priv->tags.strong_start = "<b>";
+               priv->tags.strong_end = "</b>";
+               priv->tags.code_start = "<tt>";
+               priv->tags.code_end = "</tt>";
+               priv->tags.h1_start = "<big>";
+               priv->tags.h1_end = "</big>";
+               priv->tags.h2_start = "<b>";
+               priv->tags.h2_end = "</b>";
+               priv->tags.bullet_start = "• ";
+               priv->tags.bullet_end = "";
+               priv->tags.rule = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n";
+
+       /* XHTML */
+       } else if (output == GS_MARKDOWN_OUTPUT_HTML) {
+               priv->tags.em_start = "<em>";
+               priv->tags.em_end = "<em>";
+               priv->tags.strong_start = "<strong>";
+               priv->tags.strong_end = "</strong>";
+               priv->tags.code_start = "<code>";
+               priv->tags.code_end = "</code>";
+               priv->tags.h1_start = "<h1>";
+               priv->tags.h1_end = "</h1>";
+               priv->tags.h2_start = "<h2>";
+               priv->tags.h2_end = "</h2>";
+               priv->tags.bullet_start = "<li>";
+               priv->tags.bullet_end = "</li>";
+               priv->tags.rule = "<hr>";
+
+       /* plain text */
+       } else if (output == GS_MARKDOWN_OUTPUT_TEXT) {
+               priv->tags.em_start = "";
+               priv->tags.em_end = "";
+               priv->tags.strong_start = "";
+               priv->tags.strong_end = "";
+               priv->tags.code_start = "";
+               priv->tags.code_end = "";
+               priv->tags.h1_start = "[";
+               priv->tags.h1_end = "]";
+               priv->tags.h2_start = "-";
+               priv->tags.h2_end = "-";
+               priv->tags.bullet_start = "* ";
+               priv->tags.bullet_end = "";
+               priv->tags.rule = " ----- \n";
+
+       /* unknown */
+       } else {
+               g_warning ("unknown output enum");
+               ret = FALSE;
+       }
+
+       /* save if valid */
+       if (ret)
+               priv->output = output;
+       return ret;
+}
+
+/**
+ * gs_markdown_set_max_lines:
+ **/
+gboolean
+gs_markdown_set_max_lines (GsMarkdown *self, gint max_lines)
+{
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       g_return_val_if_fail (GS_IS_MARKDOWN (self), FALSE);
+       priv->max_lines = max_lines;
+       return TRUE;
+}
+
+/**
+ * gs_markdown_set_smart_quoting:
+ **/
+gboolean
+gs_markdown_set_smart_quoting (GsMarkdown *self, gboolean smart_quoting)
+{
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       g_return_val_if_fail (GS_IS_MARKDOWN (self), FALSE);
+       priv->smart_quoting = smart_quoting;
+       return TRUE;
+}
+
+/**
+ * gs_markdown_set_escape:
+ **/
+gboolean
+gs_markdown_set_escape (GsMarkdown *self, gboolean escape)
+{
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       g_return_val_if_fail (GS_IS_MARKDOWN (self), FALSE);
+       priv->escape = escape;
+       return TRUE;
+}
+
+/**
+ * gs_markdown_set_autocode:
+ **/
+gboolean
+gs_markdown_set_autocode (GsMarkdown *self, gboolean autocode)
+{
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       g_return_val_if_fail (GS_IS_MARKDOWN (self), FALSE);
+       priv->autocode = autocode;
+       return TRUE;
+}
+
+/**
+ * gs_markdown_parse:
+ **/
+gchar *
+gs_markdown_parse (GsMarkdown *self, const gchar *markdown)
+{
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       gboolean ret;
+       gchar **lines;
+       gchar *temp;
+       guint i;
+       guint len;
+
+       g_return_val_if_fail (GS_IS_MARKDOWN (self), NULL);
+       g_return_val_if_fail (priv->output != GS_MARKDOWN_OUTPUT_UNKNOWN, NULL);
+
+       g_debug ("input='%s'", markdown);
+
+       /* process */
+       priv->mode = GS_MARKDOWN_MODE_UNKNOWN;
+       priv->line_count = 0;
+       g_string_truncate (priv->pending, 0);
+       g_string_truncate (priv->processed, 0);
+       lines = g_strsplit (markdown, "\n", -1);
+       len = g_strv_length (lines);
+
+       /* process each line */
+       for (i = 0; i < len; i++) {
+               ret = gs_markdown_to_text_line_process (self, lines[i]);
+               if (!ret)
+                       break;
+       }
+       g_strfreev (lines);
+       gs_markdown_flush_pending (self);
+
+       /* remove trailing \n */
+       while (g_str_has_suffix (priv->processed->str, "\n"))
+               g_string_set_size (priv->processed, priv->processed->len - 1);
+
+       /* get a copy */
+       temp = g_strdup (priv->processed->str);
+       g_string_truncate (priv->pending, 0);
+       g_string_truncate (priv->processed, 0);
+
+       g_debug ("output='%s'", temp);
+
+       return temp;
+}
+
+static void
+gs_markdown_finalize (GObject *object)
+{
+       GsMarkdown *self;
+       GsMarkdownPrivate *priv;
+
+       g_return_if_fail (GS_IS_MARKDOWN (object));
+
+       self = GS_MARKDOWN (object);
+       priv = gs_markdown_get_instance_private (self);
+
+       g_string_free (priv->pending, TRUE);
+       g_string_free (priv->processed, TRUE);
+
+       G_OBJECT_CLASS (gs_markdown_parent_class)->finalize (object);
+}
+
+static void
+gs_markdown_class_init (GsMarkdownClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = gs_markdown_finalize;
+}
+
+static void
+gs_markdown_init (GsMarkdown *self)
+{
+       GsMarkdownPrivate *priv = gs_markdown_get_instance_private (self);
+       priv->mode = GS_MARKDOWN_MODE_UNKNOWN;
+       priv->output = GS_MARKDOWN_OUTPUT_UNKNOWN;
+       priv->pending = g_string_new ("");
+       priv->processed = g_string_new ("");
+       priv->max_lines = -1;
+       priv->smart_quoting = FALSE;
+       priv->escape = FALSE;
+       priv->autocode = FALSE;
+}
+
+GsMarkdown *
+gs_markdown_new (void)
+{
+       GsMarkdown *self;
+       self = g_object_new (GS_TYPE_MARKDOWN, NULL);
+       return GS_MARKDOWN (self);
+}
diff --git a/src/gs-markdown.h b/src/gs-markdown.h
new file mode 100644
index 0000000..04dbc1f
--- /dev/null
+++ b/src/gs-markdown.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008-2013 Richard Hughes <richard hughsie com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 02110-1301 USA.
+ */
+
+#ifndef __GS_MARKDOWN_H
+#define __GS_MARKDOWN_H
+
+#include <glib-object.h>
+
+typedef struct _GsMarkdown     GsMarkdown;
+typedef struct _GsMarkdownClass        GsMarkdownClass;
+
+#define GS_TYPE_MARKDOWN       (gs_markdown_get_type ())
+#define GS_MARKDOWN(o)         (G_TYPE_CHECK_INSTANCE_CAST ((o), GS_TYPE_MARKDOWN, GsMarkdown))
+#define GS_IS_MARKDOWN(o)      (G_TYPE_CHECK_INSTANCE_TYPE ((o), GS_TYPE_MARKDOWN))
+
+struct _GsMarkdown {
+       GObject          parent_instance;
+};
+
+struct _GsMarkdownClass {
+       GObjectClass     parent_class;
+};
+
+typedef enum {
+       GS_MARKDOWN_OUTPUT_UNKNOWN,
+       GS_MARKDOWN_OUTPUT_TEXT,
+       GS_MARKDOWN_OUTPUT_PANGO,
+       GS_MARKDOWN_OUTPUT_HTML,
+       GS_MARKDOWN_OUTPUT_LAST
+} GsMarkdownOutputKind;
+
+GType           gs_markdown_get_type                   (void);
+GsMarkdown     *gs_markdown_new                        (void);
+gboolean        gs_markdown_set_output_kind            (GsMarkdown             *self,
+                                                        GsMarkdownOutputKind    output);
+gboolean        gs_markdown_set_max_lines              (GsMarkdown             *self,
+                                                        gint                    max_lines);
+gboolean        gs_markdown_set_smart_quoting          (GsMarkdown             *self,
+                                                        gboolean                smart_quoting);
+gboolean        gs_markdown_set_escape                 (GsMarkdown             *self,
+                                                        gboolean                escape);
+gboolean        gs_markdown_set_autocode               (GsMarkdown             *self,
+                                                        gboolean                autocode);
+gchar          *gs_markdown_parse                      (GsMarkdown             *self,
+                                                        const gchar            *text);
+
+G_END_DECLS
+
+#endif /* __GS_MARKDOWN_H */
+
diff --git a/src/gs-shell-updates.c b/src/gs-shell-updates.c
index dab8fd5..6101884 100644
--- a/src/gs-shell-updates.c
+++ b/src/gs-shell-updates.c
@@ -29,6 +29,7 @@
 #include "gs-offline-updates.h"
 #include "gs-app.h"
 #include "gs-app-widget.h"
+#include "gs-markdown.h"
 
 static void    gs_shell_updates_finalize       (GObject        *object);
 
@@ -227,9 +228,11 @@ static void
 gs_shell_updates_set_updates_description_ui (GsShellUpdates *shell_updates, GsApp *app)
 {
        GsShellUpdatesPrivate *priv = shell_updates->priv;
-       gchar *tmp;
        GsAppKind kind;
+       GsMarkdown *markdown;
        GtkWidget *widget;
+       gchar *tmp;
+       gchar *update_desc;
 
        /* set window title */
        widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "dialog_update"));
@@ -244,6 +247,20 @@ gs_shell_updates_set_updates_description_ui (GsShellUpdates *shell_updates, GsAp
                g_free (tmp);
        }
 
+       /* get the update description */
+       if (gs_app_get_update_details (app) == NULL) {
+               /* TRANSLATORS: this is where the packager did not write a
+                * description for the update */
+               update_desc = g_strdup ("No update description");
+       } else {
+               markdown = gs_markdown_new ();
+               gs_markdown_set_smart_quoting (markdown, TRUE);
+               gs_markdown_set_autocode (markdown, TRUE);
+               gs_markdown_set_output_kind (markdown, GS_MARKDOWN_OUTPUT_PANGO);
+               update_desc = gs_markdown_parse (markdown, gs_app_get_update_details (app));
+               g_object_unref (markdown);
+       }
+
        /* set update header */
        widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "box_update_header"));
        gtk_widget_set_visible (widget, kind == GS_APP_KIND_NORMAL || kind == GS_APP_KIND_SYSTEM);
@@ -252,13 +269,14 @@ gs_shell_updates_set_updates_description_ui (GsShellUpdates *shell_updates, GsAp
        widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "scrolledwindow_update"));
        gtk_widget_set_visible (widget, kind == GS_APP_KIND_OS_UPDATE);
        widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "label_update_details"));
-       gtk_label_set_label (GTK_LABEL (widget), gs_app_get_update_details (app));
+       gtk_label_set_markup (GTK_LABEL (widget), update_desc);
        widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "image_update_icon"));
        gtk_image_set_from_pixbuf (GTK_IMAGE (widget), gs_app_get_pixbuf (app));
        widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "label_update_name"));
        gtk_label_set_label (GTK_LABEL (widget), gs_app_get_name (app));
        widget = GTK_WIDGET (gtk_builder_get_object (priv->builder, "label_update_summary"));
        gtk_label_set_label (GTK_LABEL (widget), gs_app_get_summary (app));
+       g_free (update_desc);
 }
 
 static void


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]