geany/plugins/export.c
Nick Treleaven db980f3f83 Add ui->set_statusbar() to the plugin API.
Make plugin function msgwin->status_add() not set the statusbar - but
ui->set_statusbar() can now be used to do both with the log argument.
After Geany 0.12 this is how the core versions of those functions
will work, so the status window can be set independently.


git-svn-id: https://geany.svn.sourceforge.net/svnroot/geany/trunk@1903 ea778897-0a13-0410-b9d1-a72fbfd435f5
2007-09-25 16:21:35 +00:00

735 lines
20 KiB
C

/*
* export.c - this file is part of Geany, a fast and lightweight IDE
*
* Copyright 2007 Enrico Tröger <enrico.troeger@uvena.de>
* Copyright 2007 Nick Treleaven <nick.treleaven@btinternet.com>
*
* 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.
*
* $Id: demoplugin.c 1749 2007-07-27 10:37:22Z ntrel $
*/
/* Export plugin. */
#include "geany.h"
#include "support.h"
#include "plugindata.h"
#include "editor.h"
#include "document.h"
#include "prefs.h"
#include "utils.h"
#include <ctype.h>
PluginFields *plugin_fields;
GeanyData *geany_data;
VERSION_CHECK(20)
PLUGIN_INFO(_("Export"), _("Exports the current file into different formats."), "0.1")
#define doc_array geany_data->doc_array
#define scintilla geany_data->sci
#define utils geany_data->utils
#define support geany_data->support
#define dialogs geany_data->dialogs
#define ui geany_data->ui
#define ROTATE_RGB(color) \
(((color) & 0xFF0000) >> 16) + ((color) & 0x00FF00) + (((color) & 0x0000FF) << 16)
#define TEMPLATE_HTML "\
<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n\
\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\n\
<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"en\">\n\
\n\
<head>\n\
<title>{export_filename}</title>\n\
<meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\" />\n\
<meta name=\"generator\" content=\"Geany " VERSION "\" />\n\
<meta name=\"date\" content=\"{export_date}\">\n\
<style type=\"text/css\">\n\
{export_styles}\n\
</style>\n\
</head>\n\
\n\
<body>\n\
<p>\n\
{export_content}\n\
</p>\n\
</body>\n\
</html>\n"
#define TEMPLATE_LATEX "\
% {export_filename} (LaTeX code generated by Geany " VERSION " on {export_date})\n\
\\documentclass[a4paper]{article}\n\
\\usepackage[a4paper,margin=2cm]{geometry}\n\
\\usepackage[utf8x]{inputenc}\n\
\\usepackage[T1]{fontenc}\n\
\\usepackage{color}\n\
\\setlength{\\parindent}{0em}\n\
\\setlength{\\parskip}{2ex plus1ex minus0.5ex}\n\
{export_styles}\n\
\\begin{document}\
\n\
\\ttfamily\n\
\\setlength{\\fboxrule}{0pt}\n\
\\setlength{\\fboxsep}{0pt}\n\
{export_content}\
\\end{document}\n"
enum
{
FORE = 0,
BACK,
BOLD,
ITALIC,
USED,
MAX_TYPES
};
enum
{
DATE_TYPE_DEFAULT,
DATE_TYPE_HTML
};
typedef void (*ExportFunc) (gint idx, const gchar *filename, gboolean use_zoom);
typedef struct
{
gint idx;
gboolean have_zoom_level_checkbox;
ExportFunc export_func;
} ExportInfo;
static void on_file_save_dialog_response(GtkDialog *dialog, gint response, gpointer user_data);
static void write_html_file(gint idx, const gchar *filename, gboolean use_zoom);
static void write_latex_file(gint idx, const gchar *filename, gboolean use_zoom);
/* converts a RGB colour into a LaTeX compatible representation, taken from SciTE */
static gchar* get_tex_rgb(gint rgb_colour)
{
//texcolor[rgb]{0,0.5,0}{....}
gdouble rf = (rgb_colour % 256) / 256.0;
gdouble gf = ((rgb_colour & - 16711936) / 256) / 256.0;
gdouble bf = ((rgb_colour & 0xff0000) / 65536) / 256.0;
gint r = (gint) (rf * 10 + 0.5);
gint g = (gint) (gf * 10 + 0.5);
gint b = (gint) (bf * 10 + 0.5);
return g_strdup_printf("%d.%d, %d.%d, %d.%d", r / 10, r % 10, g / 10, g % 10, b / 10, b % 10);
}
// convert a style number (0..127) into a string representation (aa, ab, .., ba, bb, .., zy, zz)
static gchar *get_tex_style(gint style)
{
static gchar buf[4];
int i = 0;
do
{
buf[i] = (style % 26) + 'a';
style /= 26;
i++;
} while (style > 0);
buf[i] = '\0';
return buf;
}
static void create_file_save_as_dialog(const gchar *extension, ExportFunc func,
gboolean show_zoom_level_checkbox)
{
gint idx;
GtkWidget *dialog;
GtkTooltips *tooltips;
ExportInfo *exi;
if (extension == NULL)
return;
idx = geany_data->document->get_cur_idx();
tooltips = GTK_TOOLTIPS(support->lookup_widget(geany_data->app->window, "tooltips"));
exi = g_new(ExportInfo, 1);
exi->idx = idx;
exi->export_func = func;
exi->have_zoom_level_checkbox = FALSE;
dialog = gtk_file_chooser_dialog_new(_("Export File"), GTK_WINDOW(geany_data->app->window),
GTK_FILE_CHOOSER_ACTION_SAVE, NULL, NULL);
gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
gtk_window_set_destroy_with_parent(GTK_WINDOW(dialog), TRUE);
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
gtk_window_set_type_hint(GTK_WINDOW(dialog), GDK_WINDOW_TYPE_HINT_DIALOG);
gtk_widget_set_name(dialog, "GeanyExportDialog");
gtk_dialog_add_buttons(GTK_DIALOG(dialog),
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
if (show_zoom_level_checkbox)
{
GtkWidget *vbox, *check_zoom_level;
vbox = gtk_vbox_new(FALSE, 0);
check_zoom_level = gtk_check_button_new_with_mnemonic(_("_Use current zoom level"));
gtk_tooltips_set_tip(tooltips, check_zoom_level,
_("Renders the font size of the document together with the current zoom level."), NULL);
gtk_box_pack_start(GTK_BOX(vbox), check_zoom_level, FALSE, FALSE, 0);
gtk_widget_show_all(vbox);
gtk_file_chooser_set_extra_widget(GTK_FILE_CHOOSER(dialog), vbox);
g_object_set_data_full(G_OBJECT(dialog), "check_zoom_level",
gtk_widget_ref(check_zoom_level), (GDestroyNotify) gtk_widget_unref);
exi->have_zoom_level_checkbox = TRUE;
}
g_signal_connect((gpointer) dialog, "delete_event",
G_CALLBACK(gtk_widget_hide_on_delete), NULL);
g_signal_connect((gpointer) dialog, "response",
G_CALLBACK(on_file_save_dialog_response), exi);
gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(geany_data->app->window));
// if the current document has a filename we use it as the default.
gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(dialog));
if (doc_list[idx].file_name != NULL)
{
gchar *base_name = g_path_get_basename(doc_list[idx].file_name);
gchar *short_name = utils->remove_ext_from_filename(base_name);
gchar *file_name = g_strconcat(short_name, extension, NULL);
gchar *locale_filename = utils->get_locale_from_utf8(doc_list[idx].file_name);
gchar *locale_dirname = g_path_get_dirname(locale_filename);
// set the current name to base_name.html which probably doesn't exist yet so
// gtk_file_chooser_set_filename() can't be used and we need
// gtk_file_chooser_set_current_folder() additionally
gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), locale_dirname);
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), file_name);
g_free(locale_filename);
g_free(short_name);
g_free(file_name);
g_free(base_name);
}
else
{
const gchar *default_open_path = geany_data->prefs->default_open_path;
gchar *fname = g_strconcat(GEANY_STRING_UNTITLED, extension, NULL);
gtk_file_chooser_unselect_all(GTK_FILE_CHOOSER(dialog));
gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), fname);
// use default startup directory(if set) if no files are open
if (NZV(default_open_path) && g_path_is_absolute(default_open_path))
{
gchar *locale_path = utils->get_locale_from_utf8(default_open_path);
gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), locale_path);
g_free(locale_path);
}
g_free(fname);
}
gtk_dialog_run(GTK_DIALOG(dialog));
}
static void on_menu_create_latex_activate(gint idx, const gchar *filename, gboolean use_zoom)
{
create_file_save_as_dialog(".tex", write_latex_file, FALSE);
}
static void on_menu_create_html_activate(gint idx, const gchar *filename, gboolean use_zoom)
{
create_file_save_as_dialog(".html", write_html_file, TRUE);
}
static void write_data(const gchar *filename, const gchar *data)
{
gint error_nr = utils->write_file(filename, data);
gchar *utf8_filename = utils->get_utf8_from_locale(filename);
if (error_nr == 0)
ui->set_statusbar(TRUE, _("Document successfully exported as '%s'."), utf8_filename);
else
ui->set_statusbar(TRUE, _("File '%s' could not be written (%s)."),
utf8_filename, g_strerror(error_nr));
g_free(utf8_filename);
}
static const gchar *get_date(gint type)
{
static gchar str[128];
gchar *format;
time_t t;
struct tm *tmp;
t = time(NULL);
tmp = localtime(&t);
if (tmp == NULL)
return "";
if (type == DATE_TYPE_HTML)
// needs testing
#ifdef _GNU_SOURCE
format = "%Y-%m-%dT%H:%M:%S%z";
#else
format = "%Y-%m-%dT%H:%M:%S";
#endif
else
format = "%c";
strftime(str, sizeof str, format, tmp);
return str;
}
static void on_file_save_dialog_response(GtkDialog *dialog, gint response, gpointer user_data)
{
ExportInfo *exi = user_data;
if (response == GTK_RESPONSE_ACCEPT && exi != NULL)
{
gchar *new_filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
gchar *utf8_filename;
gboolean use_zoom_level = FALSE;
if (exi->have_zoom_level_checkbox)
{
use_zoom_level = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(
support->lookup_widget(GTK_WIDGET(dialog), "check_zoom_level")));
}
utf8_filename = utils->get_utf8_from_locale(new_filename);
// check if file exists and ask whether to overwrite or not
if (g_file_test(new_filename, G_FILE_TEST_EXISTS))
{
if (dialogs->show_question(
_("The file '%s' already exists. Do you want to overwrite it?"),
utf8_filename) == FALSE)
return;
}
exi->export_func(exi->idx, new_filename, use_zoom_level);
g_free(utf8_filename);
g_free(new_filename);
}
g_free(exi);
gtk_widget_destroy(GTK_WIDGET(dialog));
}
static void write_latex_file(gint idx, const gchar *filename, gboolean use_zoom)
{
gint i, style = -1, old_style = 0, column = 0;
gchar c, c_next, *tmp;
// 0 - fore, 1 - back, 2 - bold, 3 - italic, 4 - font size, 5 - used(0/1)
gint styles[STYLE_MAX + 1][MAX_TYPES];
gboolean block_open = FALSE;
GString *body;
GString *cmds;
GString *latex;
// first read all styles from Scintilla
for (i = 0; i <= STYLE_MAX; i++)
{
styles[i][FORE] = scintilla->send_message(doc_list[idx].sci, SCI_STYLEGETFORE, i, 0);
styles[i][BACK] = scintilla->send_message(doc_list[idx].sci, SCI_STYLEGETBACK, i, 0);
styles[i][BOLD] = scintilla->send_message(doc_list[idx].sci, SCI_STYLEGETBOLD, i, 0);
styles[i][ITALIC] = scintilla->send_message(doc_list[idx].sci, SCI_STYLEGETITALIC, i, 0);
styles[i][USED] = 0;
}
// read the document and write the LaTeX code
body = g_string_new("");
for (i = 0; i < scintilla->get_length(doc_list[idx].sci); i++)
{
style = scintilla->get_style_at(doc_list[idx].sci, i);
c = scintilla->get_char_at(doc_list[idx].sci, i);
c_next = scintilla->get_char_at(doc_list[idx].sci, i + 1);
if (style != old_style || ! block_open)
{
old_style = style;
styles[style][USED] = 1;
if (block_open)
{
g_string_append(body, "}\n");
block_open = FALSE;
}
g_string_append_printf(body, "\\style%s{", get_tex_style(style));
block_open = TRUE;
}
// escape the current character if necessary else just add it
switch (c)
{
case '\r':
case '\n':
{
if (c == '\r' && c_next == '\n')
continue; // when using CR/LF skip CR and add the line break with LF
if (block_open)
{
g_string_append(body, "}");
block_open = FALSE;
}
g_string_append(body, " \\\\\n");
column = -1;
break;
}
case '\t':
{
gint tab_stop = geany_data->editor_prefs->tab_width -
(column % geany_data->editor_prefs->tab_width);
column += tab_stop - 1; // -1 because we add 1 at the end of the loop
g_string_append_printf(body, "\\hspace*{%dem}", tab_stop);
break;
}
case ' ':
{
if (c_next == ' ')
{
g_string_append(body, "{\\hspace*{1em}}");
i++; // skip the next character
}
else
g_string_append_c(body, ' ');
break;
}
case '{':
case '}':
case '_':
case '&':
case '$':
case '#':
case '%':
{
g_string_append_printf(body, "\\%c", c);
break;
}
case '\\':
{
g_string_append(body, "\\symbol{92}");
break;
}
case '~':
{
g_string_append(body, "\\symbol{126}");
break;
}
case '^':
{
g_string_append(body, "\\symbol{94}");
break;
}
/// TODO still don't work for "---" or "----"
case '-': // mask "--"
{
if (c_next == '-')
{
g_string_append(body, "-\\/-");
i++; // skip the next character
}
else
g_string_append_c(body, '-');
break;
}
case '<': // mask "<<"
{
if (c_next == '<')
{
g_string_append(body, "<\\/<");
i++; // skip the next character
}
else
g_string_append_c(body, '<');
break;
}
case '>': // mask ">>"
{
if (c_next == '>')
{
g_string_append(body, ">\\/>");
i++; // skip the next character
}
else
g_string_append_c(body, '>');
break;
}
default: g_string_append_c(body, c);
}
column++;
}
if (block_open)
{
g_string_append(body, "}\n");
block_open = FALSE;
}
// force writing of style 0 (used at least for line breaks)
styles[0][USED] = 1;
// write used styles in the header
cmds = g_string_new("");
for (i = 0; i <= STYLE_MAX; i++)
{
if (styles[i][USED])
{
g_string_append_printf(cmds,
"\\newcommand{\\style%s}[1]{\\noindent{", get_tex_style(i));
if (styles[i][BOLD])
g_string_append(cmds, "\\textbf{");
if (styles[i][ITALIC])
g_string_append(cmds, "\\textit{");
tmp = get_tex_rgb(styles[i][FORE]);
g_string_append_printf(cmds, "\\textcolor[rgb]{%s}{", tmp);
g_free(tmp);
tmp = get_tex_rgb(styles[i][BACK]);
g_string_append_printf(cmds, "\\fcolorbox[rgb]{0, 0, 0}{%s}{", tmp);
g_string_append(cmds, "#1}}");
g_free(tmp);
if (styles[i][BOLD])
g_string_append_c(cmds, '}');
if (styles[i][ITALIC])
g_string_append_c(cmds, '}');
g_string_append(cmds, "}}\n");
}
}
// write all
latex = g_string_new(TEMPLATE_LATEX);
utils->string_replace_all(latex, "{export_content}", body->str);
utils->string_replace_all(latex, "{export_styles}", cmds->str);
utils->string_replace_all(latex, "{export_date}", get_date(DATE_TYPE_DEFAULT));
if (doc_list[idx].file_name == NULL)
utils->string_replace_all(latex, "{export_filename}", GEANY_STRING_UNTITLED);
else
utils->string_replace_all(latex, "{export_filename}", doc_list[idx].file_name);
write_data(filename, latex->str);
g_string_free(body, TRUE);
g_string_free(cmds, TRUE);
g_string_free(latex, TRUE);
}
static void write_html_file(gint idx, const gchar *filename, gboolean use_zoom)
{
gint i, style = -1, old_style = 0, column = 0;
gchar c, c_next;
// 0 - fore, 1 - back, 2 - bold, 3 - italic, 4 - font size, 5 - used(0/1)
gint styles[STYLE_MAX + 1][MAX_TYPES];
gboolean span_open = FALSE;
const gchar *font_name;
gint font_size;
PangoFontDescription *font_desc;
GString *body;
GString *css;
GString *html;
// first read all styles from Scintilla
for (i = 0; i <= STYLE_MAX; i++)
{
styles[i][FORE] = ROTATE_RGB(scintilla->send_message(doc_list[idx].sci, SCI_STYLEGETFORE, i, 0));
styles[i][BACK] = ROTATE_RGB(scintilla->send_message(doc_list[idx].sci, SCI_STYLEGETBACK, i, 0));
styles[i][BOLD] = scintilla->send_message(doc_list[idx].sci, SCI_STYLEGETBOLD, i, 0);
styles[i][ITALIC] = scintilla->send_message(doc_list[idx].sci, SCI_STYLEGETITALIC, i, 0);
styles[i][USED] = 0;
}
// read Geany's font and font size
font_desc = pango_font_description_from_string(geany_data->prefs->editor_font);
font_name = pango_font_description_get_family(font_desc);
//font_size = pango_font_description_get_size(font_desc) / PANGO_SCALE;
// take the zoom level also into account
font_size = scintilla->send_message(doc_list[idx].sci, SCI_STYLEGETSIZE, 0, 0);
if (use_zoom)
font_size += scintilla->send_message(doc_list[idx].sci, SCI_GETZOOM, 0, 0);
// read the document and write the HTML body
body = g_string_new("");
for (i = 0; i < scintilla->get_length(doc_list[idx].sci); i++)
{
style = scintilla->get_style_at(doc_list[idx].sci, i);
c = scintilla->get_char_at(doc_list[idx].sci, i);
// scintilla->get_char_at() takes care of index boundaries and return 0 if i is too high
c_next = scintilla->get_char_at(doc_list[idx].sci, i + 1);
if ((style != old_style || ! span_open) && ! isspace(c))
{
old_style = style;
styles[style][USED] = 1;
if (span_open)
{
g_string_append(body, "</span>");
}
g_string_append_printf(body, "<span class=\"style_%d\">", style);
span_open = TRUE;
}
// escape the current character if necessary else just add it
switch (c)
{
case '\r':
case '\n':
{
if (c == '\r' && c_next == '\n')
continue; // when using CR/LF skip CR and add the line break with LF
if (span_open)
{
g_string_append(body, "</span>");
span_open = FALSE;
}
g_string_append(body, "<br />\n");
column = -1;
break;
}
case '\t':
{
gint j;
gint tab_stop = geany_data->editor_prefs->tab_width -
(column % geany_data->editor_prefs->tab_width);
column += tab_stop - 1; // -1 because we add 1 at the end of the loop
for (j = 0; j < tab_stop; j++)
{
g_string_append(body, "&nbsp;");
}
break;
}
case ' ':
{
g_string_append(body, "&nbsp;");
break;
}
case '<':
{
g_string_append(body, "&lt;");
break;
}
case '>':
{
g_string_append(body, "&gt;");
break;
}
case '&':
{
g_string_append(body, "&amp;");
break;
}
default: g_string_append_c(body, c);
}
column++;
}
if (span_open)
{
g_string_append(body, "</span>");
span_open = FALSE;
}
// write used styles in the header
css = g_string_new("");
g_string_append_printf(css,
"\tbody\n\t{\n\t\tfont-family: %s, monospace;\n\t\tfont-size: %dpt;\n\t}\n",
font_name, font_size);
for (i = 0; i <= STYLE_MAX; i++)
{
if (styles[i][USED])
{
g_string_append_printf(css,
"\t.style_%d\n\t{\n\t\tcolor: #%06x;\n\t\tbackground-color: #%06x;\n%s%s\t}\n",
i, styles[i][FORE], styles[i][BACK],
(styles[i][BOLD]) ? "\t\tfont-weight: bold;\n" : "",
(styles[i][ITALIC]) ? "\t\tfont-style: italic;\n" : "");
}
}
// write all
html = g_string_new(TEMPLATE_HTML);
utils->string_replace_all(html, "{export_date}", get_date(DATE_TYPE_HTML));
utils->string_replace_all(html, "{export_content}", body->str);
utils->string_replace_all(html, "{export_styles}", css->str);
if (doc_list[idx].file_name == NULL)
utils->string_replace_all(html, "{export_filename}", GEANY_STRING_UNTITLED);
else
utils->string_replace_all(html, "{export_filename}", doc_list[idx].file_name);
write_data(filename, html->str);
pango_font_description_free(font_desc);
g_string_free(body, TRUE);
g_string_free(css, TRUE);
g_string_free(html, TRUE);
}
void init(GeanyData *data)
{
GtkWidget *menu_export;
GtkWidget *menu_export_menu;
GtkWidget *menu_create_html;
GtkWidget *menu_create_latex;
menu_export = gtk_image_menu_item_new_with_mnemonic(_("_Export"));
gtk_container_add(GTK_CONTAINER(data->tools_menu), menu_export);
menu_export_menu = gtk_menu_new ();
gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_export), menu_export_menu);
// HTML
menu_create_html = gtk_menu_item_new_with_mnemonic(_("As HTML"));
gtk_container_add(GTK_CONTAINER (menu_export_menu), menu_create_html);
g_signal_connect((gpointer) menu_create_html, "activate",
G_CALLBACK(on_menu_create_html_activate), NULL);
// LaTeX
menu_create_latex = gtk_menu_item_new_with_mnemonic(_("As LaTeX"));
gtk_container_add(GTK_CONTAINER (menu_export_menu), menu_create_latex);
g_signal_connect((gpointer) menu_create_latex, "activate",
G_CALLBACK(on_menu_create_latex_activate), NULL);
gtk_widget_show_all(menu_export);
plugin_fields->menu_item = menu_export;
}
void cleanup()
{
gtk_widget_destroy(plugin_fields->menu_item);
}