diff options
author | Leandro A. F. Pereira <leandro@hardinfo.org> | 2010-05-03 09:25:43 -0300 |
---|---|---|
committer | Leandro A. F. Pereira <leandro@hardinfo.org> | 2010-05-03 09:25:43 -0300 |
commit | 9a50155ec3e27aa6cedf3f118196f1947c769a29 (patch) | |
tree | c23cc6949b227b9e09432af038a44ccb81d92ce6 /hardinfo2/help-viewer | |
parent | ca5a3e84296a34c2391374942001cfaf1d8395a6 (diff) |
Use CMake instead of ToscoConf.
Diffstat (limited to 'hardinfo2/help-viewer')
-rw-r--r-- | hardinfo2/help-viewer/egg-markdown.c | 1586 | ||||
-rw-r--r-- | hardinfo2/help-viewer/help-viewer.c | 514 | ||||
-rw-r--r-- | hardinfo2/help-viewer/markdown-text-view.c | 637 |
3 files changed, 2737 insertions, 0 deletions
diff --git a/hardinfo2/help-viewer/egg-markdown.c b/hardinfo2/help-viewer/egg-markdown.c new file mode 100644 index 00000000..4056d4f9 --- /dev/null +++ b/hardinfo2/help-viewer/egg-markdown.c @@ -0,0 +1,1586 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * + * Copyright (C) 2008 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2009 Leandro Pereira <leandro@hardinfo.org> + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +#include "config.h" + +#define _GNU_SOURCE +#include <stdio.h> +#include <string.h> +#include <glib.h> + +#include "egg-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. + * + ******************************************************************************/ + +static void egg_markdown_finalize (GObject *object); + +#define EGG_MARKDOWN_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), EGG_TYPE_MARKDOWN, EggMarkdownPrivate)) + +typedef gchar *(EggMarkdownLinkBuilder)(gchar *title, gchar *uri, gint link_id); +typedef gchar *(EggMarkdownImageBuilder)(gchar *alt_text, gchar *path, gint link_id); + +typedef enum { + EGG_MARKDOWN_MODE_BLANK, + EGG_MARKDOWN_MODE_RULE, + EGG_MARKDOWN_MODE_BULLETT, + EGG_MARKDOWN_MODE_PARA, + EGG_MARKDOWN_MODE_H1, + EGG_MARKDOWN_MODE_H2, + EGG_MARKDOWN_MODE_UNKNOWN +} EggMarkdownMode; + +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 *bullett_start; + const gchar *bullett_end; + const gchar *rule; + + EggMarkdownLinkBuilder *link_builder; + EggMarkdownImageBuilder *image_builder; +} EggMarkdownTags; + +struct EggMarkdownPrivate +{ + EggMarkdownMode mode; + EggMarkdownTags tags; + EggMarkdownOutput output; + gint max_lines; + guint line_count; + gboolean smart_quoting; + gboolean escape; + gboolean autocode; + GString *pending; + GString *processed; + GArray *link_table; +}; + +G_DEFINE_TYPE (EggMarkdown, egg_markdown, G_TYPE_OBJECT) + +/** + * egg_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 +egg_markdown_to_text_line_is_rule (const gchar *line) +{ + guint i; + guint len; + guint count = 0; + gchar *copy = NULL; + gboolean ret = FALSE; + + len = strnlen (line, EGG_MARKDOWN_MAX_LINE_LENGTH); + 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; +} + +/** + * egg_markdown_to_text_line_is_bullett: + **/ +static gboolean +egg_markdown_to_text_line_is_bullett (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, " + ")); +} + +/** + * egg_markdown_to_text_line_is_header1: + **/ +static gboolean +egg_markdown_to_text_line_is_header1 (const gchar *line) +{ + return g_str_has_prefix (line, "# "); +} + +/** + * egg_markdown_to_text_line_is_header2: + **/ +static gboolean +egg_markdown_to_text_line_is_header2 (const gchar *line) +{ + return g_str_has_prefix (line, "## "); +} + +/** + * egg_markdown_to_text_line_is_header1_type2: + **/ +static gboolean +egg_markdown_to_text_line_is_header1_type2 (const gchar *line) +{ + return g_str_has_prefix (line, "==="); +} + +/** + * egg_markdown_to_text_line_is_header2_type2: + **/ +static gboolean +egg_markdown_to_text_line_is_header2_type2 (const gchar *line) +{ + return g_str_has_prefix (line, "---"); +} + +#if 0 +/** + * egg_markdown_to_text_line_is_code: + **/ +static gboolean +egg_markdown_to_text_line_is_code (const gchar *line) +{ + return (g_str_has_prefix (line, " ") || g_str_has_prefix (line, "\t")); +} + +/** + * egg_markdown_to_text_line_is_blockquote: + **/ +static gboolean +egg_markdown_to_text_line_is_blockquote (const gchar *line) +{ + return (g_str_has_prefix (line, "> ")); +} +#endif + +/** + * egg_markdown_to_text_line_is_blank: + **/ +static gboolean +egg_markdown_to_text_line_is_blank (const gchar *line) +{ + guint i; + guint len; + gboolean ret = FALSE; + + len = strnlen (line, EGG_MARKDOWN_MAX_LINE_LENGTH); + + /* a line with no characters is blank by definition */ + 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; +} + +/** + * egg_markdown_replace: + **/ +static gchar * +egg_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; +} + +/** + * egg_markdown_strstr_spaces: + **/ +static gchar * +egg_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; +} + + +/** + * egg_markdown_to_text_line_formatter: + **/ +static gchar * +egg_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 = strnlen (formatter, EGG_MARKDOWN_MAX_LINE_LENGTH); + if (len == 0) + goto out; + + /* find sections */ + copy = g_strdup (line); + str1 = egg_markdown_strstr_spaces (copy, formatter); + if (str1 != NULL) { + *str1 = '\0'; + str2 = egg_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 = egg_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; +} + +static gchar * +egg_markdown_to_text_line_formatter_image (EggMarkdown *self, const gchar *line) +{ + const guint len = 2; /* needed to know for shifts */ + gchar *str1; + gchar *str2; + gchar *start = NULL; + gchar *path = NULL; + gchar *alt_text = NULL; + gchar *end = NULL; + gchar *copy = NULL; + gchar *data = NULL; + + /* find sections */ + copy = g_strdup (line); + str1 = egg_markdown_strstr_spaces (copy, "!["); + if (str1 != NULL) { + *str1 = '\0'; + str2 = egg_markdown_strstr_spaces (str1+len, "]"); + if (str2 != NULL) { + *str2 = '\0'; + start = copy; + alt_text = str1 + len; + + str2 = strstr (str2 + 1, "("); + if (str2 != NULL) { + *str2 = '\0'; + + str1 = strstr (str2 + 1, ")"); + if (str1 != NULL) { + *str1 = '\0'; + path = str2 + 1; + end = str1 + 1; + } + } + } + } + + /* if we found, replace and keep looking for the same string */ + if (start && (path && *path) && alt_text && end) { + gchar *formatted_img; + gchar *path_copy = g_strdup(path); + + g_array_append_val(self->priv->link_table, path_copy); + + formatted_img = self->priv->tags.image_builder(alt_text, + path, + self->priv->link_table->len - 1); + + data = g_strdup_printf ("%s%s%s", + start, formatted_img, end); + + g_free(formatted_img); + } else { + /* not found, keep return as-is */ + data = g_strdup (line); + } + + g_free (copy); + return data; +} + + +/** + * egg_markdown_to_text_line_formatter_link: + **/ +static gchar * +egg_markdown_to_text_line_formatter_link (EggMarkdown *self, const gchar *line) +{ + const guint len = 1; /* needed to know for shifts */ + gchar *str1; + gchar *str2; + gchar *start = NULL; + gchar *link = NULL; + gchar *link_title = NULL; + gchar *end = NULL; + gchar *copy = NULL; + gchar *data = NULL; + + /* find sections */ + copy = g_strdup (line); + str1 = egg_markdown_strstr_spaces (copy, "["); + if (str1 != NULL) { + *str1 = '\0'; + str2 = egg_markdown_strstr_spaces (str1+len, "]"); + if (str2 != NULL) { + *str2 = '\0'; + start = copy; + link = str1 + len; + end = str2 + len; + + str2 = strstr (link, " "); + if (str2 != NULL) { + *str2 = '\0'; + link_title = str2 + len; + } + } + } + + /* if we found, replace and keep looking for the same string */ + if (start && (link && *link) && link_title && end) { + gchar *formatted_link; + gchar *link_copy = g_strdup(link); + + g_array_append_val(self->priv->link_table, link_copy); + + formatted_link = self->priv->tags.link_builder(link_title, + link, + self->priv->link_table->len - 1); + + data = g_strdup_printf ("%s%s%s", + start, formatted_link, end); + + g_free(formatted_link); + } else { + /* not found, keep return as-is */ + data = g_strdup (line); + } + + g_free (copy); + return data; +} + +void +egg_markdown_clear(EggMarkdown *self) +{ + int i; + + for (i = 0; i < self->priv->link_table->len; i++) { + g_free(g_array_index(self->priv->link_table, gchar *, i)); + } + + g_array_free(self->priv->link_table, TRUE); + self->priv->link_table = g_array_new(FALSE, FALSE, sizeof(gchar *)); +} + +gchar * +egg_markdown_get_link_uri(EggMarkdown *self, const gint link_id) +{ + g_return_val_if_fail(link_id < self->priv->link_table->len, NULL); + + return g_strdup(g_array_index(self->priv->link_table, gchar *, link_id)); +} + +/** + * egg_markdown_to_text_line_format_sections: + **/ +static gchar * +egg_markdown_to_text_line_format_sections (EggMarkdown *self, const gchar *line) +{ + gchar *data = g_strdup (line); + gchar *temp; + + /* smart quoting */ + if (self->priv->smart_quoting) { + if (self->priv->escape) { + temp = data; + data = egg_markdown_to_text_line_formatter (temp, """, "“", "”"); + g_free (temp); + + temp = data; + data = egg_markdown_to_text_line_formatter (temp, "'", "‘", "’"); + g_free (temp); + } else { + temp = data; + data = egg_markdown_to_text_line_formatter (temp, "\"", "“", "”"); + g_free (temp); + + temp = data; + data = egg_markdown_to_text_line_formatter (temp, "'", "‘", "’"); + g_free (temp); + } + } + + /* image */ + temp = data; + data = egg_markdown_to_text_line_formatter_image (self, temp); + g_free(temp); + + /* link */ + temp = data; + data = egg_markdown_to_text_line_formatter_link (self, temp); + g_free(temp); + + /* bold1 */ + temp = data; + data = egg_markdown_to_text_line_formatter (temp, "**", self->priv->tags.strong_start, self->priv->tags.strong_end); + g_free (temp); + + /* bold2 */ + temp = data; + data = egg_markdown_to_text_line_formatter (temp, "__", self->priv->tags.strong_start, self->priv->tags.strong_end); + g_free (temp); + + /* italic1 */ + temp = data; + data = egg_markdown_to_text_line_formatter (temp, "*", self->priv->tags.em_start, self->priv->tags.em_end); + g_free (temp); + + /* italic2 */ + temp = data; + data = egg_markdown_to_text_line_formatter (temp, "_", self->priv->tags.em_start, self->priv->tags.em_end); + g_free (temp); + + /* em-dash */ + temp = data; + data = egg_markdown_replace (temp, " -- ", " — "); + g_free (temp); + + return data; +} + +/** + * egg_markdown_to_text_line_format: + **/ +static gchar * +egg_markdown_to_text_line_format (EggMarkdown *self, const gchar *line) +{ + guint i; + gchar *text; + gboolean mode = FALSE; + gchar **codes; + GString *string; + + /* optimise the trivial case where we don't have any code tags */ + text = strstr (line, "`"); + if (text == NULL) { + text = egg_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 = egg_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, self->priv->tags.code_start); + g_string_append (string, codes[i]); + g_string_append (string, self->priv->tags.code_end); + mode = FALSE; + } + } + text = g_string_free (string, FALSE); +out: + return text; +} + +/** + * egg_markdown_add_pending: + **/ +static gboolean +egg_markdown_add_pending (EggMarkdown *self, const gchar *line) +{ + gchar *copy; + + /* would put us over the limit */ + if (self->priv->line_count >= self->priv->max_lines) + return FALSE; + + copy = g_strdup (line); + + /* strip leading and trailing spaces */ + g_strstrip (copy); + + /* append */ + g_string_append_printf (self->priv->pending, "%s ", copy); + + g_free (copy); + return TRUE; +} + +/** + * egg_markdown_add_pending_header: + **/ +static gboolean +egg_markdown_add_pending_header (EggMarkdown *self, const gchar *line) +{ + gchar *copy; + gboolean ret; + + /* strip trailing # */ + copy = g_strdup (line); + g_strdelimit (copy, "#", ' '); + ret = egg_markdown_add_pending (self, copy); + g_free (copy); + return ret; +} + +/** + * egg_markdown_count_chars_in_word: + **/ +static guint +egg_markdown_count_chars_in_word (const gchar *text, gchar find) +{ + guint i; + guint len; + guint count = 0; + + /* get length */ + len = strnlen (text, EGG_MARKDOWN_MAX_LINE_LENGTH); + if (len == 0) + goto out; + + /* find matching chars */ + for (i=0; i<len; i++) { + if (text[i] == find) + count++; + } +out: + return count; +} + +/** + * egg_markdown_word_is_code: + **/ +static gboolean +egg_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] != '_' && + egg_markdown_count_chars_in_word (text, '_') > 1) + return TRUE; + + /* nothing special */ + return FALSE; +} + +/** + * egg_markdown_word_auto_format_code: + **/ +static gchar * +egg_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 (egg_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; +} + +/** + * egg_markdown_flush_pending: + **/ +static void +egg_markdown_flush_pending (EggMarkdown *self) +{ + gchar *copy; + gchar *temp; + + /* no data yet */ + if (self->priv->mode == EGG_MARKDOWN_MODE_UNKNOWN) + return; + + /* remove trailing spaces */ + while (g_str_has_suffix (self->priv->pending->str, " ")) + g_string_set_size (self->priv->pending, self->priv->pending->len - 1); + + /* pango requires escaping */ + copy = g_strdup (self->priv->pending->str); + if (!self->priv->escape && self->priv->output == EGG_MARKDOWN_OUTPUT_PANGO) { + g_strdelimit (copy, "<", '('); + g_strdelimit (copy, ">", ')'); + } + + /* check words for code */ + if (self->priv->autocode && + (self->priv->mode == EGG_MARKDOWN_MODE_PARA || + self->priv->mode == EGG_MARKDOWN_MODE_BULLETT)) { + temp = egg_markdown_word_auto_format_code (copy); + g_free (copy); + copy = temp; + } + + /* escape */ + if (self->priv->escape) { + temp = g_markup_escape_text (copy, -1); + g_free (copy); + copy = temp; + } + + /* do formatting */ + temp = egg_markdown_to_text_line_format (self, copy); + if (self->priv->mode == EGG_MARKDOWN_MODE_BULLETT) { + g_string_append_printf (self->priv->processed, "%s%s%s\n", self->priv->tags.bullett_start, temp, self->priv->tags.bullett_end); + self->priv->line_count++; + } else if (self->priv->mode == EGG_MARKDOWN_MODE_H1) { + g_string_append_printf (self->priv->processed, "%s%s%s\n", self->priv->tags.h1_start, temp, self->priv->tags.h1_end); + } else if (self->priv->mode == EGG_MARKDOWN_MODE_H2) { + g_string_append_printf (self->priv->processed, "%s%s%s\n", self->priv->tags.h2_start, temp, self->priv->tags.h2_end); + } else if (self->priv->mode == EGG_MARKDOWN_MODE_PARA || + self->priv->mode == EGG_MARKDOWN_MODE_RULE) { + g_string_append_printf (self->priv->processed, "%s\n", temp); + self->priv->line_count++; + } + + DEBUG ("adding '%s'", temp); + + /* clear */ + g_string_truncate (self->priv->pending, 0); + g_free (copy); + g_free (temp); +} + +/** + * egg_markdown_to_text_line_process: + **/ +static gboolean +egg_markdown_to_text_line_process (EggMarkdown *self, const gchar *line) +{ + gboolean ret; + + /* blank */ + ret = egg_markdown_to_text_line_is_blank (line); + if (ret) { + DEBUG ("blank: '%s'", line); + egg_markdown_flush_pending (self); + /* a new line after a list is the end of list, not a gap */ + if (self->priv->mode != EGG_MARKDOWN_MODE_BULLETT) + ret = egg_markdown_add_pending (self, "\n"); + self->priv->mode = EGG_MARKDOWN_MODE_BLANK; + goto out; + } + + /* header1_type2 */ + ret = egg_markdown_to_text_line_is_header1_type2 (line); + if (ret) { + DEBUG ("header1_type2: '%s'", line); + if (self->priv->mode == EGG_MARKDOWN_MODE_PARA) + self->priv->mode = EGG_MARKDOWN_MODE_H1; + goto out; + } + + /* header2_type2 */ + ret = egg_markdown_to_text_line_is_header2_type2 (line); + if (ret) { + DEBUG ("header2_type2: '%s'", line); + if (self->priv->mode == EGG_MARKDOWN_MODE_PARA) + self->priv->mode = EGG_MARKDOWN_MODE_H2; + goto out; + } + + /* rule */ + ret = egg_markdown_to_text_line_is_rule (line); + if (ret) { + DEBUG ("rule: '%s'", line); + egg_markdown_flush_pending (self); + self->priv->mode = EGG_MARKDOWN_MODE_RULE; + ret = egg_markdown_add_pending (self, self->priv->tags.rule); + goto out; + } + + /* bullett */ + ret = egg_markdown_to_text_line_is_bullett (line); + if (ret) { + DEBUG ("bullett: '%s'", line); + egg_markdown_flush_pending (self); + self->priv->mode = EGG_MARKDOWN_MODE_BULLETT; + ret = egg_markdown_add_pending (self, &line[2]); + goto out; + } + + /* header1 */ + ret = egg_markdown_to_text_line_is_header1 (line); + if (ret) { + DEBUG ("header1: '%s'", line); + egg_markdown_flush_pending (self); + self->priv->mode = EGG_MARKDOWN_MODE_H1; + ret = egg_markdown_add_pending_header (self, &line[2]); + goto out; + } + + /* header2 */ + ret = egg_markdown_to_text_line_is_header2 (line); + if (ret) { + DEBUG ("header2: '%s'", line); + egg_markdown_flush_pending (self); + self->priv->mode = EGG_MARKDOWN_MODE_H2; + ret = egg_markdown_add_pending_header (self, &line[3]); + goto out; + } + + /* paragraph */ + if (self->priv->mode == EGG_MARKDOWN_MODE_BLANK || self->priv->mode == EGG_MARKDOWN_MODE_UNKNOWN) { + egg_markdown_flush_pending (self); + self->priv->mode = EGG_MARKDOWN_MODE_PARA; + } + + /* add to pending */ + DEBUG ("continue: '%s'", line); + ret = egg_markdown_add_pending (self, line); +out: + /* if we failed to add, we don't know the mode */ + if (!ret) + self->priv->mode = EGG_MARKDOWN_MODE_UNKNOWN; + return ret; +} + +/** + * egg_markdown_linkbuilder_pango: + **/ +static gchar * +egg_markdown_linkbuilder_pango (gchar *title, gchar *uri, gint link_id) +{ + /* FIXME: This is a nasty hack, since extending Pango markup to allow new tags + * is too complicated. We use the language code as a link index + * since it won't allow anything besides letters or numbers. + * To obtain the link URI, use egg_markdown_get_link_uri(). */ + return g_strdup_printf("<span lang=\"%d\" foreground=\"blue\"><u>%s</u></span>", + link_id, title); +} + +/** + * egg_markdown_linkbuilder_html + **/ +static gchar * +egg_markdown_linkbuilder_html (gchar *title, gchar *uri, gint link_id) +{ + return g_strdup_printf("<a href=\"%s\">%s</a>", uri, title); +} + +/** + * egg_markdown_linkbuilder_text + **/ +static gchar * +egg_markdown_linkbuilder_text (gchar *title, gchar *uri, gint link_id) +{ + return g_strdup_printf("%s (%s)", title, uri); +} + +/** + * egg_markdown_imagebuilder_pango: + **/ +static gchar * +egg_markdown_imagebuilder_pango (gchar *alt_text, gchar *uri, gint link_id) +{ + /* FIXME See egg_markdown_linkbuilder_pango() */ + return g_strdup_printf("<span lang=\"%d\" underline=\"double\">%s</span>", + link_id, alt_text); +} + +/** + * egg_markdown_imagebuilder_html + **/ +static gchar * +egg_markdown_imagebuilder_html (gchar *alt_text, gchar *uri, gint link_id) +{ + return g_strdup_printf("<img src=\"%s\" alt=\"%s\">", uri, alt_text); +} + +/** + * egg_markdown_imagebuilder_text + **/ +static gchar * +egg_markdown_imagebuilder_text (gchar *alt_text, gchar *uri, gint link_id) +{ + return g_strdup(alt_text); +} + +/** + * egg_markdown_set_output: + **/ +gboolean +egg_markdown_set_output (EggMarkdown *self, EggMarkdownOutput output) +{ + gboolean ret = TRUE; + g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE); + + /* PangoMarkup */ + if (output == EGG_MARKDOWN_OUTPUT_PANGO) { + self->priv->tags.em_start = "<i>"; + self->priv->tags.em_end = "</i>"; + self->priv->tags.strong_start = "<b>"; + self->priv->tags.strong_end = "</b>"; + self->priv->tags.code_start = "<tt><span bgcolor=\"#eee\">"; + self->priv->tags.code_end = "</span></tt>"; + self->priv->tags.h1_start = "<span color=\"#444\" size=\"xx-large\"><b>"; + self->priv->tags.h1_end = "</b></span>"; + self->priv->tags.h2_start = "<big><b>"; + self->priv->tags.h2_end = "</b></big>"; + self->priv->tags.bullett_start = " • "; + self->priv->tags.bullett_end = ""; + self->priv->tags.rule = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n"; + self->priv->tags.link_builder = egg_markdown_linkbuilder_pango; + self->priv->tags.image_builder = egg_markdown_imagebuilder_pango; + + /* XHTML */ + } else if (output == EGG_MARKDOWN_OUTPUT_HTML) { + self->priv->tags.em_start = "<em>"; + self->priv->tags.em_end = "<em>"; + self->priv->tags.strong_start = "<strong>"; + self->priv->tags.strong_end = "</strong>"; + self->priv->tags.code_start = "<code>"; + self->priv->tags.code_end = "</code>"; + self->priv->tags.h1_start = "<h1>"; + self->priv->tags.h1_end = "</h1>"; + self->priv->tags.h2_start = "<h2>"; + self->priv->tags.h2_end = "</h2>"; + self->priv->tags.bullett_start = "<li>"; + self->priv->tags.bullett_end = "</li>"; + self->priv->tags.rule = "<hr>"; + self->priv->tags.link_builder = egg_markdown_linkbuilder_html; + self->priv->tags.image_builder = egg_markdown_imagebuilder_html; + + /* plain text */ + } else if (output == EGG_MARKDOWN_OUTPUT_TEXT) { + self->priv->tags.em_start = ""; + self->priv->tags.em_end = ""; + self->priv->tags.strong_start = ""; + self->priv->tags.strong_end = ""; + self->priv->tags.code_start = ""; + self->priv->tags.code_end = ""; + self->priv->tags.h1_start = "["; + self->priv->tags.h1_end = "]"; + self->priv->tags.h2_start = "-"; + self->priv->tags.h2_end = "-"; + self->priv->tags.bullett_start = "* "; + self->priv->tags.bullett_end = ""; + self->priv->tags.rule = " ----- \n"; + self->priv->tags.link_builder = egg_markdown_linkbuilder_text; + self->priv->tags.image_builder = egg_markdown_imagebuilder_text; + + /* unknown */ + } else { + g_warning ("unknown output enum"); + ret = FALSE; + } + + /* save if valid */ + if (ret) + self->priv->output = output; + return ret; +} + +/** + * egg_markdown_set_max_lines: + **/ +gboolean +egg_markdown_set_max_lines (EggMarkdown *self, gint max_lines) +{ + g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE); + self->priv->max_lines = max_lines; + return TRUE; +} + +/** + * egg_markdown_set_smart_quoting: + **/ +gboolean +egg_markdown_set_smart_quoting (EggMarkdown *self, gboolean smart_quoting) +{ + g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE); + self->priv->smart_quoting = smart_quoting; + return TRUE; +} + +/** + * egg_markdown_set_escape: + **/ +gboolean +egg_markdown_set_escape (EggMarkdown *self, gboolean escape) +{ + g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE); + self->priv->escape = escape; + return TRUE; +} + +/** + * egg_markdown_set_autocode: + **/ +gboolean +egg_markdown_set_autocode (EggMarkdown *self, gboolean autocode) +{ + g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE); + self->priv->autocode = autocode; + return TRUE; +} + +/** + * egg_markdown_parse: + **/ +gchar * +egg_markdown_parse (EggMarkdown *self, const gchar *markdown) +{ + gchar **lines; + guint i; + guint len; + gchar *temp; + gboolean ret; + + g_return_val_if_fail (EGG_IS_MARKDOWN (self), NULL); + g_return_val_if_fail (self->priv->output != EGG_MARKDOWN_OUTPUT_UNKNOWN, NULL); + + DEBUG ("input='%s'", markdown); + + /* process */ + self->priv->mode = EGG_MARKDOWN_MODE_UNKNOWN; + self->priv->line_count = 0; + g_string_truncate (self->priv->pending, 0); + g_string_truncate (self->priv->processed, 0); + lines = g_strsplit (markdown, "\n", -1); + len = g_strv_length (lines); + + /* process each line */ + for (i=0; i<len; i++) { + ret = egg_markdown_to_text_line_process (self, lines[i]); + if (!ret) + break; + } + g_strfreev (lines); + egg_markdown_flush_pending (self); + + /* remove trailing \n */ + while (g_str_has_suffix (self->priv->processed->str, "\n")) + g_string_set_size (self->priv->processed, self->priv->processed->len - 1); + + /* get a copy */ + temp = g_strdup (self->priv->processed->str); + g_string_truncate (self->priv->pending, 0); + g_string_truncate (self->priv->processed, 0); + + DEBUG ("output='%s'", temp); + + return temp; +} + +/** + * egg_markdown_class_init: + * @klass: The EggMarkdownClass + **/ +static void +egg_markdown_class_init (EggMarkdownClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = egg_markdown_finalize; + g_type_class_add_private (klass, sizeof (EggMarkdownPrivate)); +} + +/** + * egg_markdown_init: + **/ +static void +egg_markdown_init (EggMarkdown *self) +{ + self->priv = EGG_MARKDOWN_GET_PRIVATE (self); + + self->priv->mode = EGG_MARKDOWN_MODE_UNKNOWN; + self->priv->output = EGG_MARKDOWN_OUTPUT_UNKNOWN; + self->priv->pending = g_string_new (""); + self->priv->processed = g_string_new (""); + self->priv->link_table = g_array_new(FALSE, FALSE, sizeof(gchar *)); + self->priv->max_lines = -1; + self->priv->smart_quoting = FALSE; + self->priv->escape = FALSE; + self->priv->autocode = FALSE; +} + +/** + * egg_markdown_finalize: + * @object: The object to finalize + **/ +static void +egg_markdown_finalize (GObject *object) +{ + EggMarkdown *self; + int i; + + g_return_if_fail (EGG_IS_MARKDOWN (object)); + + self = EGG_MARKDOWN (object); + + g_return_if_fail (self->priv != NULL); + g_string_free (self->priv->pending, TRUE); + g_string_free (self->priv->processed, TRUE); + + for (i = 0; i < self->priv->link_table->len; i++) { + g_free(g_array_index(self->priv->link_table, gchar *, i)); + } + g_array_free (self->priv->link_table, TRUE); + + G_OBJECT_CLASS (egg_markdown_parent_class)->finalize (object); +} + +/** + * egg_markdown_new: + * + * Return value: a new EggMarkdown object. + **/ +EggMarkdown * +egg_markdown_new (void) +{ + EggMarkdown *self; + self = g_object_new (EGG_TYPE_MARKDOWN, NULL); + return EGG_MARKDOWN (self); +} + +/*************************************************************************** + *** MAKE CHECK TESTS *** + ***************************************************************************/ +#ifdef EGG_TEST +#include "egg-test.h" + +void +egg_markdown_test (EggTest *test) +{ + EggMarkdown *self; + gchar *text; + gboolean ret; + const gchar *markdown; + const gchar *markdown_expected; + + if (!egg_test_start (test, "EggMarkdown")) + return; + + /************************************************************ + **************** line_is_rule ************** + ************************************************************/ + ret = egg_markdown_to_text_line_is_rule ("* * *"); + egg_test_title_assert (test, "is rule (1)", ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_rule ("***"); + egg_test_title_assert (test, "is rule (2)", ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_rule ("*****"); + egg_test_title_assert (test, "is rule (3)", ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_rule ("- - -"); + egg_test_title_assert (test, "is rule (4)", ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_rule ("---------------------------------------"); + egg_test_title_assert (test, "is rule (5)", ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_rule (""); + egg_test_title_assert (test, "is rule (blank)", !ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_rule ("richard hughes"); + egg_test_title_assert (test, "is rule (text)", !ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_rule ("- richard-hughes"); + egg_test_title_assert (test, "is rule (bullet)", !ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_blank (""); + egg_test_title_assert (test, "is blank (blank)", ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_blank (" \t "); + egg_test_title_assert (test, "is blank (mix)", ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_blank ("richard hughes"); + egg_test_title_assert (test, "is blank (name)", !ret); + + /************************************************************/ + ret = egg_markdown_to_text_line_is_blank ("ccccccccc"); + egg_test_title_assert (test, "is blank (full)", !ret); + + + /************************************************************ + **************** replace ************** + ************************************************************/ + text = egg_markdown_replace ("summary -- really -- sure!", " -- ", " – "); + egg_test_title (test, "replace (multiple)"); + if (g_str_equal (text, "summary – really – sure!")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************ + **************** formatter ************** + ************************************************************/ + text = egg_markdown_to_text_line_formatter ("**is important** text", "**", "<b>", "</b>"); + egg_test_title (test, "formatter (left)"); + if (g_str_equal (text, "<b>is important</b> text")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************/ + text = egg_markdown_to_text_line_formatter ("this is **important**", "**", "<b>", "</b>"); + egg_test_title (test, "formatter (right)"); + if (g_str_equal (text, "this is <b>important</b>")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************/ + text = egg_markdown_to_text_line_formatter ("**important**", "**", "<b>", "</b>"); + egg_test_title (test, "formatter (only)"); + if (g_str_equal (text, "<b>important</b>")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************/ + text = egg_markdown_to_text_line_formatter ("***important***", "**", "<b>", "</b>"); + egg_test_title (test, "formatter (only)"); + if (g_str_equal (text, "<b>*important</b>*")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************/ + text = egg_markdown_to_text_line_formatter ("I guess * this is * not bold", "*", "<i>", "</i>"); + egg_test_title (test, "formatter (with spaces)"); + if (g_str_equal (text, "I guess * this is * not bold")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************/ + text = egg_markdown_to_text_line_formatter ("this **is important** text in **several** places", "**", "<b>", "</b>"); + egg_test_title (test, "formatter (middle, multiple)"); + if (g_str_equal (text, "this <b>is important</b> text in <b>several</b> places")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************/ + text = egg_markdown_word_auto_format_code ("this is http://www.hughsie.com/with_spaces_in_url inline link"); + egg_test_title (test, "auto formatter (url)"); + if (g_str_equal (text, "this is `http://www.hughsie.com/with_spaces_in_url` inline link")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************/ + text = egg_markdown_to_text_line_formatter ("this was \"triffic\" it was", "\"", "“", "”"); + egg_test_title (test, "formatter (quotes)"); + if (g_str_equal (text, "this was “triffic” it was")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************/ + text = egg_markdown_to_text_line_formatter ("This isn't a present", "'", "‘", "’"); + egg_test_title (test, "formatter (one quote)"); + if (g_str_equal (text, "This isn't a present")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got %s", text); + g_free (text); + + /************************************************************ + **************** markdown ************** + ************************************************************/ + egg_test_title (test, "get EggMarkdown object"); + self = egg_markdown_new (); + egg_test_assert (test, self != NULL); + + /************************************************************/ + ret = egg_markdown_set_output (self, EGG_MARKDOWN_OUTPUT_PANGO); + egg_test_title_assert (test, "set pango output", ret); + + /************************************************************/ + markdown = "OEMs\n" + "====\n" + " - Bullett\n"; + markdown_expected = + "<big>OEMs</big>\n" + "• Bullett"; + egg_test_title (test, "markdown (type2 header)"); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + + /************************************************************/ + markdown = "this is http://www.hughsie.com/with_spaces_in_url inline link\n"; + markdown_expected = "this is <tt>http://www.hughsie.com/with_spaces_in_url</tt> inline link"; + egg_test_title (test, "markdown (autocode)"); + egg_markdown_set_autocode (self, TRUE); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + /************************************************************/ + markdown = "*** This software is currently in alpha state ***\n"; + markdown_expected = "<b><i> This software is currently in alpha state </b></i>"; + egg_test_title (test, "markdown some invalid header"); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + /************************************************************/ + markdown = " - This is a *very*\n" + " short paragraph\n" + " that is not usual.\n" + " - Another"; + markdown_expected = + "• This is a <i>very</i> short paragraph that is not usual.\n" + "• Another"; + egg_test_title (test, "markdown (complex1)"); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + /************************************************************/ + markdown = "* This is a *very*\n" + " short paragraph\n" + " that is not usual.\n" + "* This is the second\n" + " bullett point.\n" + "* And the third.\n" + " \n" + "* * *\n" + " \n" + "Paragraph one\n" + "isn't __very__ long at all.\n" + "\n" + "Paragraph two\n" + "isn't much better."; + markdown_expected = + "• This is a <i>very</i> short paragraph that is not usual.\n" + "• This is the second bullett point.\n" + "• And the third.\n" + "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n" + "Paragraph one isn't <b>very</b> long at all.\n" + "Paragraph two isn't much better."; + egg_test_title (test, "markdown (complex1)"); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + /************************************************************/ + markdown = "This is a spec file description or\n" + "an **update** description in bohdi.\n" + "\n" + "* * *\n" + "# Big title #\n" + "\n" + "The *following* things 'were' fixed:\n" + "- Fix `dave`\n" + "* Fubar update because of \"security\"\n"; + markdown_expected = + "This is a spec file description or an <b>update</b> description in bohdi.\n" + "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n" + "<big>Big title</big>\n" + "The <i>following</i> things 'were' fixed:\n" + "• Fix <tt>dave</tt>\n" + "• Fubar update because of \"security\""; + egg_test_title (test, "markdown (complex2)"); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + /************************************************************/ + markdown = "* list seporated with spaces -\n" + " first item\n" + "\n" + "* second item\n" + "\n" + "* third item\n"; + markdown_expected = + "• list seporated with spaces - first item\n" + "• second item\n" + "• third item"; + egg_test_title (test, "markdown (list with spaces)"); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + /************************************************************/ + ret = egg_markdown_set_max_lines (self, 1); + egg_test_title_assert (test, "set max_lines", ret); + + /************************************************************/ + markdown = "* list seporated with spaces -\n" + " first item\n" + "* second item\n"; + markdown_expected = + "• list seporated with spaces - first item"; + egg_test_title (test, "markdown (one line limit)"); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + /************************************************************/ + ret = egg_markdown_set_max_lines (self, 1); + egg_test_title_assert (test, "set max_lines", ret); + + /************************************************************/ + markdown = "* list & spaces"; + markdown_expected = + "• list & spaces"; + egg_test_title (test, "markdown (escaping)"); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + /************************************************************/ + egg_test_title (test, "markdown (free text)"); + text = egg_markdown_parse (self, "This isn't a present"); + if (g_str_equal (text, "This isn't a present")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s'", text); + g_free (text); + + /************************************************************/ + egg_test_title (test, "markdown (autotext underscore)"); + text = egg_markdown_parse (self, "This isn't CONFIG_UEVENT_HELPER_PATH present"); + if (g_str_equal (text, "This isn't <tt>CONFIG_UEVENT_HELPER_PATH</tt> present")) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s'", text); + g_free (text); + + /************************************************************/ + markdown = "*Thu Mar 12 12:00:00 2009* Dan Walsh <dwalsh@redhat.com> - 2.0.79-1\n" + "- Update to upstream \n" + " * Netlink socket handoff patch from Adam Jackson.\n" + " * AVC caching of compute_create results by Eric Paris.\n" + "\n" + "*Tue Mar 10 12:00:00 2009* Dan Walsh <dwalsh@redhat.com> - 2.0.78-5\n" + "- Add patch from ajax to accellerate X SELinux \n" + "- Update eparis patch\n"; + markdown_expected = + "<i>Thu Mar 12 12:00:00 2009</i> Dan Walsh <tt><dwalsh@redhat.com></tt> - 2.0.79-1\n" + "• Update to upstream\n" + "• Netlink socket handoff patch from Adam Jackson.\n" + "• AVC caching of compute_create results by Eric Paris.\n" + "<i>Tue Mar 10 12:00:00 2009</i> Dan Walsh <tt><dwalsh@redhat.com></tt> - 2.0.78-5\n" + "• Add patch from ajax to accellerate X SELinux\n" + "• Update eparis patch"; + egg_test_title (test, "markdown (end of bullett)"); + egg_markdown_set_escape (self, TRUE); + ret = egg_markdown_set_max_lines (self, 1024); + text = egg_markdown_parse (self, markdown); + if (g_str_equal (text, markdown_expected)) + egg_test_success (test, NULL); + else + egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected); + g_free (text); + + g_object_unref (self); + + egg_test_end (test); +} +#endif + diff --git a/hardinfo2/help-viewer/help-viewer.c b/hardinfo2/help-viewer/help-viewer.c new file mode 100644 index 00000000..ace6ef37 --- /dev/null +++ b/hardinfo2/help-viewer/help-viewer.c @@ -0,0 +1,514 @@ +/* + * HelpViewer - Simple Help file browser + * Copyright (C) 2009 Leandro A. F. Pereira <leandro@hardinfo.org> + * + * 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, version 2. + * + * 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 St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#define _GNU_SOURCE /* for strcasestr() */ +#include <string.h> +#include <stdlib.h> +#include <gtk/gtk.h> + +#include "config.h" +#include "shell.h" +#include "markdown-text-view.h" +#include "help-viewer.h" +#include "hardinfo.h" + +static void do_search(HelpViewer *hv, gchar *text); + +static void forward_clicked(GtkWidget *widget, gpointer data) +{ + HelpViewer *hv = (HelpViewer *)data; + GSList *temp; + + /* puts the current file on the back stack */ + hv->back_stack = g_slist_prepend(hv->back_stack, g_strdup(hv->current_file)); + + /* enables the back button */ + gtk_widget_set_sensitive(hv->btn_back, TRUE); + + /* loads the new current file */ + if (g_str_has_prefix(hv->forward_stack->data, "search://")) { + do_search(hv, hv->forward_stack->data + sizeof("search://") - 1); + } else { + markdown_textview_load_file(MARKDOWN_TEXTVIEW(hv->text_view), hv->forward_stack->data); + } + + /* pops the stack */ + temp = hv->forward_stack->next; + g_free(hv->forward_stack->data); + g_slist_free1(hv->forward_stack); + hv->forward_stack = temp; + + /* if there aren't items on forward stack anymore, disables the button */ + if (!hv->forward_stack) { + gtk_widget_set_sensitive(hv->btn_forward, FALSE); + } +} + +static void back_clicked(GtkWidget *widget, gpointer data) +{ + HelpViewer *hv = (HelpViewer *)data; + GSList *temp; + + /* puts the current file on the forward stack */ + hv->forward_stack = g_slist_prepend(hv->forward_stack, g_strdup(hv->current_file)); + + /* enables the forward button */ + gtk_widget_set_sensitive(hv->btn_forward, TRUE); + + /* loads the new current file */ + if (g_str_has_prefix(hv->back_stack->data, "search://")) { + do_search(hv, hv->back_stack->data + sizeof("search://") - 1); + } else { + markdown_textview_load_file(MARKDOWN_TEXTVIEW(hv->text_view), hv->back_stack->data); + } + + /* pops the stack */ + temp = hv->back_stack->next; + g_free(hv->back_stack->data); + g_slist_free1(hv->back_stack); + hv->back_stack = temp; + + /* if there aren't items on back stack anymore, disables the button */ + if (!hv->back_stack) { + gtk_widget_set_sensitive(hv->btn_back, FALSE); + } +} + +static void link_clicked(MarkdownTextView *text_view, gchar *link, gpointer data) +{ + HelpViewer *hv = (HelpViewer *)data; + + if (g_str_has_prefix(link, "http://")) { + open_url(link); + } else { + /* adds the current file to the back stack (before loading the new file */ + hv->back_stack = g_slist_prepend(hv->back_stack, g_strdup(hv->current_file)); + gtk_widget_set_sensitive(hv->btn_back, TRUE); + + gtk_statusbar_pop(GTK_STATUSBAR(hv->status_bar), 1); + markdown_textview_load_file(text_view, link); + } +} + +static void file_load_complete(MarkdownTextView *text_view, gchar *file, gpointer data) +{ + HelpViewer *hv = (HelpViewer *)data; + + /* sets the currently-loaded file */ + g_free(hv->current_file); + hv->current_file = g_strdup(file); + + gtk_statusbar_push(GTK_STATUSBAR(hv->status_bar), 1, "Done."); +} + +static void hovering_over_link(MarkdownTextView *text_view, gchar *link, gpointer data) +{ + HelpViewer *hv = (HelpViewer *)data; + gchar *temp; + + temp = g_strconcat("Link to ", link, NULL); + + gtk_statusbar_push(GTK_STATUSBAR(hv->status_bar), 1, temp); + + g_free(temp); +} + +static void hovering_over_text(MarkdownTextView *text_view, gpointer data) +{ + HelpViewer *hv = (HelpViewer *)data; + + gtk_statusbar_pop(GTK_STATUSBAR(hv->status_bar), 1); +} + +static void do_search(HelpViewer *hv, gchar *text) +{ + GString *markdown; + GDir *dir; + gchar **terms; + gint no_results = 0; + int term; + + /* + * FIXME: This search is currently pretty slow; think on a better way to do search. + * Ideas: + * - Build a index the first time the help file is opened + * - Search only titles and subtitles + */ + + terms = g_strsplit(text, " ", 0); + markdown = g_string_new("# Search Results\n"); + g_string_append_printf(markdown, "Search terms: *%s*\n****\n", text); + + gtk_widget_set_sensitive(hv->window, FALSE); + + if ((dir = g_dir_open(hv->help_directory, 0, NULL))) { + const gchar *name; + + while ((name = g_dir_read_name(dir))) { +#if GTK_CHECK_VERSION(2,16,0) + gtk_entry_progress_pulse(GTK_ENTRY(hv->text_search)); +#endif /* GTK_CHECK_VERSION(2,16,0) */ + + if (g_str_has_suffix(name, ".hlp")) { + FILE *file; + gchar *path; + gchar buffer[256]; + + path = g_build_filename(hv->help_directory, name, NULL); + if ((file = fopen(path, "rb"))) { + gboolean found = FALSE; + gchar *title = NULL; + + while (!found && fgets(buffer, sizeof buffer, file)) { + if (!title && (g_str_has_prefix(buffer, "# ") || g_str_has_prefix(buffer, " # "))) { + title = g_strstrip(strchr(buffer, '#') + 1); + title = g_strdup(title); + } + + for (term = 0; !found && terms[term]; term++) { +#ifdef strcasestr + found = strcasestr(buffer, terms[term]) != NULL; +#else + gchar *upper1, *upper2; + + upper1 = g_utf8_strup(buffer, -1); + upper2 = g_utf8_strup(terms[term], -1); + + found = strstr(upper1, upper2) != NULL; + + g_free(upper1); + g_free(upper2); +#endif + } + } + + if (found) { + no_results++; + + if (title) { + g_string_append_printf(markdown, + "* [%s %s]\n", name, title); + } else { + g_string_append_printf(markdown, + "* [%s %s]\n", name, name); + } + } + + g_free(title); + fclose(file); + } + + g_free(path); + } + } + + g_dir_close(dir); + } + + + if (no_results == 0) { + g_string_append_printf(markdown, + "Search returned no results."); + } else { + g_string_append_printf(markdown, + "****\n%d results found.", no_results); + } + + /* shows the results inside the textview */ + markdown_textview_set_text(MARKDOWN_TEXTVIEW(hv->text_view), markdown->str); + + g_free(hv->current_file); + hv->current_file = g_strdup_printf("search://%s", text); + +#if GTK_CHECK_VERSION(2,16,0) + gtk_entry_set_progress_fraction(GTK_ENTRY(hv->text_search), 0.0f); +#endif /* GTK_CHECK_VERSION(2,16,0) */ + gtk_widget_set_sensitive(hv->window, TRUE); + + g_string_free(markdown, TRUE); + g_strfreev(terms); +} + +static void activate(GtkEntry *entry, gpointer data) +{ + HelpViewer *hv = (HelpViewer *)data; + + /* adds the current file to the back stack (before loading the new file */ + hv->back_stack = g_slist_prepend(hv->back_stack, g_strdup(hv->current_file)); + gtk_widget_set_sensitive(hv->btn_back, TRUE); + + do_search((HelpViewer *)data, (gchar *)gtk_entry_get_text(entry)); +} + +#if GTK_CHECK_VERSION(2,16,0) +static void icon_press(GtkEntry *entry, gint position, + GdkEventButton *event, gpointer data) +{ + if (position == GTK_ENTRY_ICON_SECONDARY) + activate(entry, data); +} +#endif /* GTK_CHECK_VERSION(2,16,0) */ + +static void home_clicked(GtkWidget *button, gpointer data) +{ + HelpViewer *hv = (HelpViewer *)data; + + help_viewer_open_page(hv, "index.hlp"); +} + +void help_viewer_open_page(HelpViewer *hv, const gchar *page) +{ + gchar *temp; + + temp = g_strdup(hv->current_file); + + if (!markdown_textview_load_file(MARKDOWN_TEXTVIEW(hv->text_view), page)) { + GtkWidget *dialog; + Shell *shell; + + shell = shell_get_main_shell(); + dialog = gtk_message_dialog_new(GTK_WINDOW(shell->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "Cannot open help file (%s).", + page); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + g_free(temp); + } else { + /* adds the current file to the back stack (before loading the new file */ + hv->back_stack = g_slist_prepend(hv->back_stack, temp); + gtk_widget_set_sensitive(hv->btn_back, TRUE); + gtk_window_present(GTK_WINDOW(hv->window)); + } +} + +void help_viewer_destroy(HelpViewer *hv) +{ + Shell *shell; + GSList *item; + + for (item = hv->back_stack; item; item = item->next) { + g_free(item->data); + } + + for (item = hv->forward_stack; item; item = item->next) { + g_free(item->data); + } + + g_slist_free(hv->back_stack); + g_slist_free(hv->forward_stack); + + g_free(hv->current_file); + g_free(hv->help_directory); + + shell = shell_get_main_shell(); + shell->help_viewer = NULL; +} + +static gboolean destroy_me(GtkWidget *widget, gpointer data) +{ + HelpViewer *hv = (HelpViewer *)data; + + help_viewer_destroy(hv); + + return FALSE; +} + +HelpViewer * +help_viewer_new (const gchar *help_dir, const gchar *help_file) +{ + Shell *shell; + HelpViewer *hv; + GtkWidget *help_viewer; + GtkWidget *vbox; + GtkWidget *hbox; + GtkWidget *toolbar1; + GtkIconSize tmp_toolbar_icon_size; + GtkWidget *btn_back; + GtkWidget *btn_forward; + GtkWidget *separatortoolitem1; + GtkWidget *toolbar2; + GtkWidget *toolitem3; +#if !GTK_CHECK_VERSION(2,16,0) + GtkWidget *label1; +#endif /* GTK_CHECK_VERSION(2,16,0) */ + GtkWidget *toolitem4; + GtkWidget *txt_search; + GtkWidget *scrolledhelp_viewer; + GtkWidget *markdown_textview; + GtkWidget *status_bar; + GtkWidget *btn_home; + GdkPixbuf *icon; + + shell = shell_get_main_shell(); + + help_viewer = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_widget_set_size_request(help_viewer, 300, 200); + gtk_window_set_default_size(GTK_WINDOW(help_viewer), 640, 480); + gtk_window_set_title(GTK_WINDOW(help_viewer), "Help Viewer"); + gtk_window_set_transient_for(GTK_WINDOW(help_viewer), GTK_WINDOW(shell->window)); + + icon = gtk_widget_render_icon(help_viewer, GTK_STOCK_HELP, + GTK_ICON_SIZE_DIALOG, + NULL); + gtk_window_set_icon(GTK_WINDOW(help_viewer), icon); + g_object_unref(icon); + + vbox = gtk_vbox_new (FALSE, 0); + gtk_widget_show (vbox); + gtk_container_add (GTK_CONTAINER (help_viewer), vbox); + + hbox = gtk_hbox_new (FALSE, 0); + gtk_widget_show (hbox); + gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0); + + toolbar1 = gtk_toolbar_new (); + gtk_widget_show (toolbar1); + gtk_box_pack_start (GTK_BOX (hbox), toolbar1, TRUE, TRUE, 0); + gtk_toolbar_set_style (GTK_TOOLBAR (toolbar1), GTK_TOOLBAR_BOTH_HORIZ); + tmp_toolbar_icon_size = gtk_toolbar_get_icon_size (GTK_TOOLBAR (toolbar1)); + + btn_back = (GtkWidget*) gtk_tool_button_new_from_stock ("gtk-go-back"); + gtk_widget_show (btn_back); + gtk_container_add (GTK_CONTAINER (toolbar1), btn_back); + gtk_tool_item_set_is_important (GTK_TOOL_ITEM (btn_back), TRUE); + gtk_widget_set_sensitive(btn_back, FALSE); + + btn_forward = (GtkWidget*) gtk_tool_button_new_from_stock ("gtk-go-forward"); + gtk_widget_show (btn_forward); + gtk_container_add (GTK_CONTAINER (toolbar1), btn_forward); + gtk_widget_set_sensitive(btn_forward, FALSE); + + separatortoolitem1 = (GtkWidget*) gtk_separator_tool_item_new (); + gtk_widget_show (separatortoolitem1); + gtk_container_add (GTK_CONTAINER (toolbar1), separatortoolitem1); + + btn_home = (GtkWidget*) gtk_tool_button_new_from_stock ("gtk-home"); + gtk_widget_show (btn_home); + gtk_container_add (GTK_CONTAINER (toolbar1), btn_home); + + toolbar2 = gtk_toolbar_new (); + gtk_widget_show (toolbar2); + gtk_box_pack_end (GTK_BOX (hbox), toolbar2, FALSE, TRUE, 0); + gtk_toolbar_set_style (GTK_TOOLBAR (toolbar2), GTK_TOOLBAR_BOTH_HORIZ); + gtk_toolbar_set_show_arrow (GTK_TOOLBAR (toolbar2), FALSE); + tmp_toolbar_icon_size = gtk_toolbar_get_icon_size (GTK_TOOLBAR (toolbar2)); + + toolitem3 = (GtkWidget*) gtk_tool_item_new (); + gtk_widget_show (toolitem3); + gtk_container_add (GTK_CONTAINER (toolbar2), toolitem3); + +#if !GTK_CHECK_VERSION(2,16,0) + label1 = gtk_label_new_with_mnemonic ("_Search:"); + gtk_widget_show (label1); + gtk_container_add (GTK_CONTAINER (toolitem3), label1); +#endif /* GTK_CHECK_VERSION(2,16,0) */ + + toolitem4 = (GtkWidget*) gtk_tool_item_new (); + gtk_widget_show (toolitem4); + gtk_container_add (GTK_CONTAINER (toolbar2), toolitem4); + + txt_search = gtk_entry_new (); + gtk_widget_show (txt_search); + gtk_container_add (GTK_CONTAINER (toolitem4), txt_search); + gtk_entry_set_invisible_char (GTK_ENTRY (txt_search), 9679); +#if GTK_CHECK_VERSION(2,16,0) + gtk_entry_set_icon_from_stock(GTK_ENTRY(txt_search), + GTK_ENTRY_ICON_SECONDARY, + GTK_STOCK_FIND); +#endif /* GTK_CHECK_VERSION(2,16,0) */ + + scrolledhelp_viewer = gtk_scrolled_window_new (NULL, NULL); + gtk_widget_show (scrolledhelp_viewer); + gtk_box_pack_start (GTK_BOX (vbox), scrolledhelp_viewer, TRUE, TRUE, 0); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledhelp_viewer), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + + markdown_textview = markdown_textview_new(); + markdown_textview_set_image_directory(MARKDOWN_TEXTVIEW(markdown_textview), help_dir); + gtk_widget_show (markdown_textview); + gtk_container_add (GTK_CONTAINER (scrolledhelp_viewer), markdown_textview); + + status_bar = gtk_statusbar_new (); + gtk_widget_show (status_bar); + gtk_box_pack_start (GTK_BOX (vbox), status_bar, FALSE, FALSE, 0); + + hv = g_new0(HelpViewer, 1); + hv->window = help_viewer; + hv->status_bar = status_bar; + hv->btn_back = btn_back; + hv->btn_forward = btn_forward; + hv->text_view = markdown_textview; + hv->text_search = txt_search; + hv->help_directory = g_strdup(help_dir); + hv->back_stack = NULL; + hv->forward_stack = NULL; + + g_signal_connect(markdown_textview, "link-clicked", G_CALLBACK(link_clicked), hv); + g_signal_connect(markdown_textview, "hovering-over-link", G_CALLBACK(hovering_over_link), hv); + g_signal_connect(markdown_textview, "hovering-over-text", G_CALLBACK(hovering_over_text), hv); + g_signal_connect(markdown_textview, "file-load-complete", G_CALLBACK(file_load_complete), hv); + + g_signal_connect(btn_back, "clicked", G_CALLBACK(back_clicked), hv); + g_signal_connect(btn_forward, "clicked", G_CALLBACK(forward_clicked), hv); + g_signal_connect(btn_home, "clicked", G_CALLBACK(home_clicked), hv); + + g_signal_connect(help_viewer, "delete-event", G_CALLBACK(destroy_me), hv); + g_signal_connect(txt_search, "activate", G_CALLBACK(activate), hv); + +#if GTK_CHECK_VERSION(2,16,0) + g_signal_connect(txt_search, "icon-press", G_CALLBACK(icon_press), hv); +#endif /* GTK_CHECK_VERSION(2,16,0) */ + + if (!markdown_textview_load_file(MARKDOWN_TEXTVIEW(markdown_textview), help_file ? help_file : "index.hlp")) { + GtkWidget *dialog; + + dialog = gtk_message_dialog_new(GTK_WINDOW(shell->window), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "Cannot open help file (%s).", + help_file ? help_file : "index.hlp"); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + + gtk_widget_destroy(hv->window); + g_free(hv); + + return NULL; + } + + gtk_widget_show_all(hv->window); + + return hv; +} + +#ifdef HELPVIEWER_TEST +int main(int argc, char **argv) +{ + HelpViewer *hv; + + gtk_init(&argc, &argv); + + hv = help_viewer_new("documentation", NULL); + + gtk_main(); +} +#endif /* HELPVIEWER_TEST */ diff --git a/hardinfo2/help-viewer/markdown-text-view.c b/hardinfo2/help-viewer/markdown-text-view.c new file mode 100644 index 00000000..6bfcc131 --- /dev/null +++ b/hardinfo2/help-viewer/markdown-text-view.c @@ -0,0 +1,637 @@ +/* + * Markdown Text View + * GtkTextView subclass that supports Markdown syntax + * + * Copyright (C) 2009 Leandro Pereira <leandro@hardinfo.org> + * Portions Copyright (C) 2007-2008 Richard Hughes <richard@hughsie.com> + * Portions Copyright (C) GTK+ Team (based on hypertext textview demo) + * + * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +#include <stdlib.h> +#include <gdk/gdkkeysyms.h> + +#include "markdown-text-view.h" +#include "config.h" + +static GdkCursor *hand_cursor = NULL; + +G_DEFINE_TYPE(MarkdownTextView, markdown_textview, GTK_TYPE_TEXT_VIEW); + +enum { + LINK_CLICKED, + HOVERING_OVER_LINK, + HOVERING_OVER_TEXT, + FILE_LOAD_COMPLETE, + LAST_SIGNAL +}; + +static guint markdown_textview_signals[LAST_SIGNAL] = { 0 }; + +GtkWidget *markdown_textview_new() +{ + return g_object_new(TYPE_MARKDOWN_TEXTVIEW, NULL); +} + +static void markdown_textview_class_init(MarkdownTextViewClass * klass) +{ + GObjectClass *object_class; + + if (!hand_cursor) { + hand_cursor = gdk_cursor_new(GDK_HAND2); + } + + object_class = G_OBJECT_CLASS(klass); + + markdown_textview_signals[LINK_CLICKED] = g_signal_new( + "link-clicked", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(MarkdownTextViewClass, link_clicked), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, + 1, + G_TYPE_STRING); + + markdown_textview_signals[HOVERING_OVER_LINK] = g_signal_new( + "hovering-over-link", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(MarkdownTextViewClass, hovering_over_link), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, + 1, + G_TYPE_STRING); + + markdown_textview_signals[HOVERING_OVER_TEXT] = g_signal_new( + "hovering-over-text", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(MarkdownTextViewClass, hovering_over_text), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + markdown_textview_signals[FILE_LOAD_COMPLETE] = g_signal_new( + "file-load-complete", + G_OBJECT_CLASS_TYPE(object_class), + G_SIGNAL_RUN_FIRST, + G_STRUCT_OFFSET(MarkdownTextViewClass, file_load_complete), + NULL, NULL, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, + 1, + G_TYPE_STRING); +} + +static void +gtk_text_buffer_insert_markup(GtkTextBuffer * buffer, GtkTextIter * iter, + const gchar * markup) +{ + PangoAttrIterator *paiter; + PangoAttrList *attrlist; + GtkTextMark *mark; + GError *error = NULL; + gchar *text; + + g_return_if_fail(GTK_IS_TEXT_BUFFER(buffer)); + g_return_if_fail(markup != NULL); + + if (*markup == '\000') + return; + + /* invalid */ + if (!pango_parse_markup(markup, -1, 0, &attrlist, &text, NULL, &error)) { + g_warning("Invalid markup string: %s", error->message); + g_error_free(error); + return; + } + + /* trivial, no markup */ + if (attrlist == NULL) { + gtk_text_buffer_insert(buffer, iter, text, -1); + g_free(text); + return; + } + + /* create mark with right gravity */ + mark = gtk_text_buffer_create_mark(buffer, NULL, iter, FALSE); + paiter = pango_attr_list_get_iterator(attrlist); + + do { + PangoAttribute *attr; + GtkTextTag *tag; + GtkTextTag *tag_para; + gint start, end; + + pango_attr_iterator_range(paiter, &start, &end); + + if (end == G_MAXINT) /* last chunk */ + end = start - 1; /* resulting in -1 to be passed to _insert */ + + tag = gtk_text_tag_new(NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_LANGUAGE))) + g_object_set(tag, "language", + pango_language_to_string(((PangoAttrLanguage *) + attr)->value), NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_FAMILY))) + g_object_set(tag, "family", ((PangoAttrString *) attr)->value, + NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_STYLE))) + g_object_set(tag, "style", ((PangoAttrInt *) attr)->value, + NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_WEIGHT))) + g_object_set(tag, "weight", ((PangoAttrInt *) attr)->value, + NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_VARIANT))) + g_object_set(tag, "variant", ((PangoAttrInt *) attr)->value, + NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_STRETCH))) + g_object_set(tag, "stretch", ((PangoAttrInt *) attr)->value, + NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_SIZE))) + g_object_set(tag, "size", ((PangoAttrInt *) attr)->value, + NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_FONT_DESC))) + g_object_set(tag, "font-desc", + ((PangoAttrFontDesc *) attr)->desc, NULL); + + if ((attr = + pango_attr_iterator_get(paiter, PANGO_ATTR_FOREGROUND))) { + GdkColor col = { 0, + ((PangoAttrColor *) attr)->color.red, + ((PangoAttrColor *) attr)->color.green, + ((PangoAttrColor *) attr)->color.blue + }; + + g_object_set(tag, "foreground-gdk", &col, NULL); + } + + if ((attr = + pango_attr_iterator_get(paiter, PANGO_ATTR_BACKGROUND))) { + GdkColor col = { 0, + ((PangoAttrColor *) attr)->color.red, + ((PangoAttrColor *) attr)->color.green, + ((PangoAttrColor *) attr)->color.blue + }; + + g_object_set(tag, "background-gdk", &col, NULL); + } + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_UNDERLINE))) + g_object_set(tag, "underline", ((PangoAttrInt *) attr)->value, + NULL); + + if ((attr = + pango_attr_iterator_get(paiter, PANGO_ATTR_STRIKETHROUGH))) + g_object_set(tag, "strikethrough", + (gboolean) (((PangoAttrInt *) attr)->value != 0), + NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_RISE))) + g_object_set(tag, "rise", ((PangoAttrInt *) attr)->value, + NULL); + + if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_SCALE))) + g_object_set(tag, "scale", ((PangoAttrFloat *) attr)->value, + NULL); + + gtk_text_tag_table_add(gtk_text_buffer_get_tag_table(buffer), tag); + + tag_para = + gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table + (buffer), "para"); + gtk_text_buffer_insert_with_tags(buffer, iter, text + start, + end - start, tag, tag_para, NULL); + + /* mark had right gravity, so it should be + * at the end of the inserted text now */ + gtk_text_buffer_get_iter_at_mark(buffer, iter, mark); + } while (pango_attr_iterator_next(paiter)); + + gtk_text_buffer_delete_mark(buffer, mark); + pango_attr_iterator_destroy(paiter); + pango_attr_list_unref(attrlist); + g_free(text); +} + + +static void +set_cursor_if_appropriate(MarkdownTextView * self, gint x, gint y) +{ + GSList *tags = NULL, *tagp = NULL; + GtkTextIter iter; + gboolean hovering = FALSE; + gchar *link_uri = NULL; + + gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(self), &iter, x, + y); + + tags = gtk_text_iter_get_tags(&iter); + for (tagp = tags; tagp != NULL; tagp = tagp->next) { + GtkTextTag *tag = tagp->data; + gint is_underline = 0; + gchar *lang = NULL; + + g_object_get(G_OBJECT(tag), + "underline", &is_underline, + "language", &lang, + NULL); + + if (is_underline == 1 && lang) { + link_uri = egg_markdown_get_link_uri(self->markdown, atoi(lang)); + g_free(lang); + hovering = TRUE; + break; + } + + g_free(lang); + } + + if (hovering != self->hovering_over_link) { + self->hovering_over_link = hovering; + + if (self->hovering_over_link) { + g_signal_emit(self, markdown_textview_signals[HOVERING_OVER_LINK], + 0, link_uri); + gdk_window_set_cursor(gtk_text_view_get_window + (GTK_TEXT_VIEW(self), + GTK_TEXT_WINDOW_TEXT), hand_cursor); + } else { + g_signal_emit(self, markdown_textview_signals[HOVERING_OVER_TEXT], 0); + gdk_window_set_cursor(gtk_text_view_get_window + (GTK_TEXT_VIEW(self), + GTK_TEXT_WINDOW_TEXT), NULL); + } + } + + if (link_uri) + g_free(link_uri); + + if (tags) + g_slist_free(tags); +} + +/* Update the cursor image if the pointer moved. + */ +static gboolean +motion_notify_event(GtkWidget * self, GdkEventMotion * event) +{ + gint x, y; + + gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(self), + GTK_TEXT_WINDOW_WIDGET, + event->x, event->y, &x, &y); + + set_cursor_if_appropriate(MARKDOWN_TEXTVIEW(self), x, y); + + gdk_window_get_pointer(self->window, NULL, NULL, NULL); + return FALSE; +} + +/* Also update the cursor image if the window becomes visible + * (e.g. when a window covering it got iconified). + */ +static gboolean +visibility_notify_event(GtkWidget * self, GdkEventVisibility * event) +{ + gint wx, wy, bx, by; + + gdk_window_get_pointer(self->window, &wx, &wy, NULL); + + gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(self), + GTK_TEXT_WINDOW_WIDGET, + wx, wy, &bx, &by); + + set_cursor_if_appropriate(MARKDOWN_TEXTVIEW(self), bx, by); + + return FALSE; +} + +static void +follow_if_link(GtkWidget * widget, GtkTextIter * iter) +{ + GSList *tags = NULL, *tagp = NULL; + MarkdownTextView *self = MARKDOWN_TEXTVIEW(widget); + + tags = gtk_text_iter_get_tags(iter); + for (tagp = tags; tagp != NULL; tagp = tagp->next) { + GtkTextTag *tag = tagp->data; + gint is_underline = 0; + gchar *lang = NULL; + + g_object_get(G_OBJECT(tag), + "underline", &is_underline, + "language", &lang, + NULL); + + if (is_underline == 1 && lang) { + gchar *link = egg_markdown_get_link_uri(self->markdown, atoi(lang)); + if (link) { + g_signal_emit(self, markdown_textview_signals[LINK_CLICKED], + 0, link); + g_free(link); + } + g_free(lang); + break; + } + + g_free(lang); + } + + if (tags) + g_slist_free(tags); +} + +static gboolean +key_press_event(GtkWidget * self, + GdkEventKey * event) +{ + GtkTextIter iter; + GtkTextBuffer *buffer; + + switch (event->keyval) { + case GDK_Return: + case GDK_KP_Enter: + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self)); + gtk_text_buffer_get_iter_at_mark(buffer, &iter, + gtk_text_buffer_get_insert + (buffer)); + follow_if_link(self, &iter); + break; + + default: + break; + } + + return FALSE; +} + +static gboolean +event_after(GtkWidget * self, GdkEvent * ev) +{ + GtkTextIter start, end, iter; + GtkTextBuffer *buffer; + GdkEventButton *event; + gint x, y; + + if (ev->type != GDK_BUTTON_RELEASE) + return FALSE; + + event = (GdkEventButton *) ev; + + if (event->button != 1) + return FALSE; + + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self)); + + /* we shouldn't follow a link if the user has selected something */ + gtk_text_buffer_get_selection_bounds(buffer, &start, &end); + if (gtk_text_iter_get_offset(&start) != gtk_text_iter_get_offset(&end)) + return FALSE; + + gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(self), + GTK_TEXT_WINDOW_WIDGET, + event->x, event->y, &x, &y); + + gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(self), &iter, x, + y); + + follow_if_link(self, &iter); + + return FALSE; +} + +void +markdown_textview_clear(MarkdownTextView * self) +{ + GtkTextBuffer *text_buffer; + + g_return_if_fail(IS_MARKDOWN_TEXTVIEW(self)); + + egg_markdown_clear(self->markdown); + + text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self)); + gtk_text_buffer_set_text(text_buffer, "\n", 1); +} + +static void +load_images(MarkdownTextView * self) +{ + GtkTextBuffer *buffer; + GtkTextIter iter; + GSList *tags, *tagp; + gchar *image_path; + + buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self)); + gtk_text_buffer_get_start_iter(buffer, &iter); + + do { + tags = gtk_text_iter_get_tags(&iter); + for (tagp = tags; tagp != NULL; tagp = tagp->next) { + GtkTextTag *tag = tagp->data; + gint is_underline = 0; + gchar *lang = NULL; + + g_object_get(G_OBJECT(tag), + "underline", &is_underline, + "language", &lang, + NULL); + + if (is_underline == 2 && lang) { + GdkPixbuf *pixbuf; + gchar *path; + + image_path = egg_markdown_get_link_uri(self->markdown, atoi(lang)); + path = g_build_filename(self->image_directory, image_path, NULL); + pixbuf = gdk_pixbuf_new_from_file(path, NULL); + if (pixbuf) { + GtkTextMark *mark; + GtkTextIter start; + + mark = gtk_text_buffer_create_mark(buffer, NULL, &iter, FALSE); + + gtk_text_buffer_get_iter_at_mark(buffer, &start, mark); + gtk_text_iter_forward_to_tag_toggle(&iter, tag); + gtk_text_buffer_delete(buffer, &start, &iter); + + gtk_text_buffer_insert_pixbuf(buffer, &iter, pixbuf); + + g_object_unref(pixbuf); + gtk_text_buffer_delete_mark(buffer, mark); + } + + g_free(image_path); + g_free(lang); + g_free(path); + break; + } + + g_free(lang); + } + + if (tags) + g_slist_free(tags); + } while (gtk_text_iter_forward_to_tag_toggle(&iter, NULL)); +} + +static gboolean +append_text(MarkdownTextView * self, + const gchar * text) +{ + GtkTextIter iter; + GtkTextBuffer *text_buffer; + gchar *line; + + g_return_val_if_fail(IS_MARKDOWN_TEXTVIEW(self), FALSE); + + text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self)); + gtk_text_buffer_get_end_iter(text_buffer, &iter); + + line = egg_markdown_parse(self->markdown, text); + if (line && *line) { + gtk_text_buffer_insert_markup(text_buffer, &iter, line); + gtk_text_buffer_insert(text_buffer, &iter, "\n", 1); + g_free(line); + + return TRUE; + } + + return FALSE; +} + +gboolean +markdown_textview_set_text(MarkdownTextView * self, + const gchar * text) +{ + gboolean result = TRUE; + gchar **lines; + gint line; + + g_return_val_if_fail(IS_MARKDOWN_TEXTVIEW(self), FALSE); + + markdown_textview_clear(self); + + lines = g_strsplit(text, "\n", 0); + for (line = 0; result && lines[line]; line++) { + result = append_text(self, (const gchar *)lines[line]); + } + g_strfreev(lines); + + load_images(self); + + return result; +} + +gboolean +markdown_textview_load_file(MarkdownTextView * self, + const gchar * file_name) +{ + FILE *text_file; + gchar *path; + + g_return_val_if_fail(IS_MARKDOWN_TEXTVIEW(self), FALSE); + + path = g_build_filename(self->image_directory, file_name, NULL); + + /* we do assume UTF-8 encoding */ + if ((text_file = fopen(path, "rb"))) { + GtkTextBuffer *text_buffer; + GtkTextIter iter; + gchar *line; + gchar buffer[EGG_MARKDOWN_MAX_LINE_LENGTH]; + + text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self)); + + gtk_text_buffer_set_text(text_buffer, "\n", 1); + gtk_text_buffer_get_start_iter(text_buffer, &iter); + + egg_markdown_clear(self->markdown); + + while (fgets(buffer, EGG_MARKDOWN_MAX_LINE_LENGTH, text_file)) { + line = egg_markdown_parse(self->markdown, buffer); + + if (line && *line) { + gtk_text_buffer_insert_markup(text_buffer, &iter, line); + gtk_text_buffer_insert(text_buffer, &iter, "\n", 1); + } + + g_free(line); + } + fclose(text_file); + + load_images(self); + + g_signal_emit(self, markdown_textview_signals[FILE_LOAD_COMPLETE], 0, file_name); + + g_free(path); + + return TRUE; + } + + g_free(path); + + return FALSE; +} + +void +markdown_textview_set_image_directory(MarkdownTextView * self, const gchar *directory) +{ + g_return_if_fail(IS_MARKDOWN_TEXTVIEW(self)); + + g_free(self->image_directory); + self->image_directory = g_strdup(directory); +} + +static void markdown_textview_init(MarkdownTextView * self) +{ + self->markdown = egg_markdown_new(); + self->image_directory = g_strdup("."); + + egg_markdown_set_output(self->markdown, EGG_MARKDOWN_OUTPUT_PANGO); + egg_markdown_set_escape(self->markdown, TRUE); + egg_markdown_set_autocode(self->markdown, TRUE); + egg_markdown_set_smart_quoting(self->markdown, TRUE); + + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(self), GTK_WRAP_WORD); + gtk_text_view_set_editable(GTK_TEXT_VIEW(self), FALSE); + gtk_text_view_set_left_margin(GTK_TEXT_VIEW(self), 10); + gtk_text_view_set_right_margin(GTK_TEXT_VIEW(self), 10); + gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(self), 3); + gtk_text_view_set_pixels_below_lines(GTK_TEXT_VIEW(self), 3); + + g_signal_connect(self, "event-after", + G_CALLBACK(event_after), NULL); + g_signal_connect(self, "key-press-event", + G_CALLBACK(key_press_event), NULL); + g_signal_connect(self, "motion-notify-event", + G_CALLBACK(motion_notify_event), NULL); + g_signal_connect(self, "visibility-notify-event", + G_CALLBACK(visibility_notify_event), NULL); +} + |