progress.*: cosmetix
[k8lowj.git] / src / tools.c
blob1bc28e092a21c424b1ce48d88cdee1cc55e2269b
1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2003 Evan Martin <evan@livejournal.com>
4 * vim: tabstop=4 shiftwidth=4 noexpandtab :
5 */
7 #include "gtk-all.h"
8 #include <libxml/parser.h>
9 #include <string.h>
11 #include "tools.h"
12 #include "util-gtk.h"
13 #include "util.h"
15 typedef struct {
16 gunichar c;
17 char *escaped;
18 } htmlescape;
19 htmlescape htmlescapes[] = {
20 { '<', "&lt;" },
21 { '>', "&gt;" },
22 { '&', "&amp;" },
23 /* FIXME: add more entities here?
24 * the above are all that are really necessary, though. */
25 { 0, 0 }
28 void
29 tools_html_escape(GtkWindow *win, JamDoc *doc) {
30 GtkTextBuffer *buffer;
31 GtkTextIter start, end, pos, nextpos;
32 htmlescape *esc;
33 gunichar c;
34 int len;
36 buffer = jam_doc_get_text_buffer(doc);
38 if (!gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) {
39 /* no selection; give them some help. */
40 jam_messagebox(win, _("HTML Escape Help"),
41 _("Some characters (such as '<' and '&') are reserved in HTML, and may not display correctly in your post.\n"
42 "To properly escape these characters, first select a block of text and try this command again.\n"));
43 return;
46 gtk_text_buffer_begin_user_action(buffer); /* atomic, in terms of undo */
48 for (pos = start;
49 gtk_text_iter_in_range(&pos, &start, &end);
50 gtk_text_iter_forward_char(&pos)) {
51 c = gtk_text_iter_get_char(&pos);
52 for (esc = htmlescapes; esc->c != 0; esc++) {
53 if (esc->c == c) break;
55 /* if this character isn't in the to-escape list, continue. */
56 if (esc->c == 0) continue;
58 nextpos = pos;
59 gtk_text_iter_forward_char(&nextpos);
60 gtk_text_buffer_delete(buffer, &pos, &nextpos);
61 len = (int)strlen(esc->escaped);
62 gtk_text_buffer_insert(buffer, &pos, esc->escaped, len);
64 /* to counteract the pos++ in the for loop */
65 gtk_text_iter_backward_char(&pos);
67 /* changing the buffer invalidates this iterator */
68 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
71 gtk_text_buffer_move_mark_by_name(buffer, "insert", &end);
72 gtk_text_buffer_move_mark_by_name(buffer, "selection_bound", &end);
74 gtk_text_buffer_end_user_action(buffer);
77 void
78 tools_remove_linebreaks(GtkWindow *win, JamDoc *doc) {
79 GtkTextBuffer *buffer;
80 GtkTextIter start, end, pos, nextpos;
81 gint nlrun = 0;
82 gboolean leading = TRUE;
84 buffer = jam_doc_get_text_buffer(doc);
86 if (!gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) {
87 /* no selection; give them some help. */
88 jam_messagebox(win, _("Linebreak Removal Help"),
89 _("When you paste text into LogJam, linebreaks from the original are preserved, and will normally show up in your post on the server. You can have LogJam remove extraneous linebreaks from your entry, but you must make a specific selection of the text you wish this to happen on first.\n\nNote that you can also use the \"don't autoformat\" option on your post."));
90 return;
93 gtk_text_buffer_begin_user_action(buffer); /* atomic, in terms of undo */
95 for (pos = start;
96 gtk_text_iter_in_range(&pos, &start, &end);
97 gtk_text_iter_forward_char(&pos)) {
99 /* we remove linebreaks as we see them; but later we will
100 * make up for our deletions by inserting spaces or \n\ns
101 * as appropriate. */
102 if (gtk_text_iter_ends_line(&pos)) {
103 if (!leading) /* leading linebreaks should just be removed */
104 nlrun++;
105 nextpos = pos;
106 gtk_text_iter_forward_line(&nextpos);
107 gtk_text_buffer_delete(buffer, &pos, &nextpos);
109 gtk_text_iter_backward_char(&pos);
111 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
113 continue;
116 leading = FALSE;
118 /* make up for what we removed, according to how long a run
119 * it was. */
120 if (nlrun == 1) { /* line separators turn into a space */
121 nlrun = 0;
122 gtk_text_buffer_insert(buffer, &pos, " ", 1);
123 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
124 } else if (nlrun > 1) { /* paragraph breaks are normalized */
125 nlrun = 0;
126 gtk_text_buffer_insert(buffer, &pos, "\n\n", 2);
128 /* this is safe, since we've just added two characters. */
129 gtk_text_iter_forward_chars(&pos, 2);
131 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
135 gtk_text_buffer_move_mark_by_name(buffer, "insert", &end);
136 gtk_text_buffer_move_mark_by_name(buffer, "selection_bound", &end);
138 gtk_text_buffer_end_user_action(buffer);
141 void
142 tools_insert_file(GtkWindow *win, JamDoc *doc) {
143 GtkWidget *filesel;
144 GtkWidget *hbox, *label, *combo;
145 GList *strings = NULL;
146 const char *localeenc;
148 filesel = gtk_file_chooser_dialog_new(_("Select File"), win,
149 GTK_FILE_CHOOSER_ACTION_OPEN,
150 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
151 GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
152 NULL);
153 label = gtk_label_new(NULL);
154 gtk_label_set_text_with_mnemonic(GTK_LABEL(label),
155 _("File _encoding:"));
156 combo = gtk_combo_new();
158 /* if they're in a non-UTF-8 locale, default to the locale encoding. */
159 if (!g_get_charset(&localeenc))
160 strings = g_list_append(strings, (char*)localeenc);
161 /* what else should go in this list other than UTF-8? */
162 strings = g_list_append(strings, "UTF-8");
163 gtk_combo_set_popdown_strings(GTK_COMBO(combo), strings);
165 gtk_label_set_mnemonic_widget(GTK_LABEL(label), GTK_COMBO(combo)->entry);
167 hbox = gtk_hbox_new(FALSE, 12);
168 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
169 gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
171 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(filesel)->vbox),
172 hbox, FALSE, FALSE, 0);
173 gtk_widget_show_all(hbox);
175 while (gtk_dialog_run(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) {
176 const gchar *filename;
177 const gchar *encoding;
178 GError *err = NULL;
180 filename = gtk_file_chooser_get_filename(
181 GTK_FILE_CHOOSER(filesel));
182 encoding = gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(combo)->entry));
184 if (!jam_doc_insert_file(doc, filename, encoding, &err)) {
185 jam_warning(GTK_WINDOW(filesel),
186 _("Error loading file: %s"), err->message);
187 g_error_free(err);
188 } else {
189 break;
192 gtk_widget_destroy(filesel);
195 void
196 tools_insert_command_output(GtkWindow *win, JamDoc *doc) {
197 /* FIXME: use combo with a history drop-down, save the history */
198 GtkWidget *cmd_dlg, *entry, *box;
200 cmd_dlg = gtk_dialog_new_with_buttons(_("Insert Command Output"),
201 win, GTK_DIALOG_MODAL,
202 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
203 GTK_STOCK_OK, GTK_RESPONSE_OK,
204 NULL);
205 gtk_window_set_transient_for(GTK_WINDOW(cmd_dlg), win);
207 entry = gtk_entry_new();
209 box = labelled_box_new(_("_Command:"), entry);
210 jam_dialog_set_contents(GTK_DIALOG(cmd_dlg), box);
211 gtk_widget_grab_focus(entry);
212 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
213 gtk_dialog_set_default_response(GTK_DIALOG(cmd_dlg), GTK_RESPONSE_OK);
215 while (gtk_dialog_run(GTK_DIALOG(cmd_dlg)) == GTK_RESPONSE_OK) {
216 const gchar *command;
217 GError *err = NULL;
218 command = gtk_entry_get_text(GTK_ENTRY(entry));
219 if (!jam_doc_insert_command_output(doc, command, NULL, &err, win) && (err)) {
220 jam_warning(GTK_WINDOW(cmd_dlg),
221 _("Error getting command output: %s"),
222 err->message);
223 if (err->code != 127)
224 break;
225 g_error_free(err);
226 } else {
227 break;
230 gtk_widget_destroy(cmd_dlg);
233 /* it appears there's no way to attach a pointer to a libXML
234 * parser context, which means we need to use a global variable
235 * so we can save the error message it tries to print to stdout
236 * and display it in a dialog instead. as another hack, we need
237 * to print that text in a monospaced font because it highlights
238 * the invalid character position...
240 * the ideal solution would be for libxml to use GError to report
241 * errors and then I wouldn't need any of this.
243 static GString *xml_error_context_hack;
244 void
245 xmlErrFunc(void *ctx, const char *msg, ...) {
246 char *s;
247 va_list ap;
248 va_start(ap, msg);
249 s = g_strdup_vprintf(msg, ap);
250 g_string_append(xml_error_context_hack, s);
251 g_free(s);
252 va_end(ap);
254 static void
255 xml_error_dialog(GtkWindow *parent, GString *errstr) {
256 GtkWidget *dlg, *box, *label;
258 dlg = gtk_dialog_new_with_buttons(_("XML Parse Error"),
259 parent, GTK_DIALOG_MODAL,
260 GTK_STOCK_OK, GTK_STOCK_CANCEL, NULL);
262 box = gtk_vbox_new(FALSE, 5);
264 gtk_box_pack_start(GTK_BOX(box),
265 gtk_label_new(_("Error parsing XML:")),
266 FALSE, FALSE, 0);
268 label = gtk_label_new(errstr->str);
269 gtk_label_set_selectable(GTK_LABEL(label), TRUE);
270 jam_widget_set_font(label, "monospace");
271 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0);
273 jam_dialog_set_contents(GTK_DIALOG(dlg), box);
275 gtk_dialog_run(GTK_DIALOG(dlg));
276 gtk_widget_destroy(dlg);
279 static gboolean
280 validate_xml(GtkWindow *parent, char *text) {
281 xmlParserCtxtPtr ctxt = xmlCreateDocParserCtxt(BAD_CAST text);
283 xml_error_context_hack = g_string_new(NULL);
284 xmlSetGenericErrorFunc(ctxt, xmlErrFunc);
285 if (xmlParseDocument(ctxt) < 0) {
286 xml_error_dialog(parent, xml_error_context_hack);
287 xmlFreeParserCtxt(ctxt);
288 g_string_free(xml_error_context_hack, TRUE);
289 return FALSE;
291 initGenericErrorDefaultFunc(NULL);
292 xmlFreeParserCtxt(ctxt);
293 g_string_free(xml_error_context_hack, TRUE);
294 return TRUE;
297 void
298 tools_validate_xml(GtkWindow *win, JamDoc *doc) {
299 GtkTextBuffer *buffer;
300 GtkTextIter start, end;
301 char *entry, *str;
303 buffer = jam_doc_get_text_buffer(doc);
305 /* default to the selection, but fall back to the entire entry. */
306 if (!gtk_text_buffer_get_selection_bounds(buffer, &start, &end))
307 gtk_text_buffer_get_bounds(buffer, &start, &end);
309 entry = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
310 str = g_strdup_printf("<entry>%s</entry>", entry);
312 if (validate_xml(win, str)) {
313 jam_messagebox(win, _("XML Validation"),
314 _("Document is well-formed XML."));
317 g_free(entry);
318 g_free(str);
321 void
322 tools_ljcut(GtkWindow *win, JamDoc *doc) {
323 GtkTextBuffer *buffer;
324 GtkTextIter start, end;
325 GtkWidget *dlg, *vbox, *hbox, *label, *entry;
326 char *text;
328 dlg = gtk_dialog_new_with_buttons(_("LJ-Cut"), win,
329 GTK_DIALOG_MODAL,
330 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
331 GTK_STOCK_OK, GTK_RESPONSE_OK,
332 NULL);
333 gtk_dialog_set_default_response(GTK_DIALOG(dlg), GTK_RESPONSE_OK);
335 vbox = gtk_vbox_new(FALSE, 5);
336 gtk_container_set_border_width(GTK_CONTAINER(vbox), 10);
338 entry = gtk_entry_new();
339 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
340 hbox = labelled_box_new(_("Cut c_aption:"), entry);
341 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
343 label = gtk_label_new(NULL);
344 gtk_label_set_markup(GTK_LABEL(label),
345 _("<small>If left empty, the LiveJournal default "
346 "will be used.</small>"));
347 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
349 jam_dialog_set_contents(GTK_DIALOG(dlg), vbox);
351 if (gtk_dialog_run(GTK_DIALOG(dlg)) != GTK_RESPONSE_OK) {
352 gtk_widget_destroy(dlg);
353 return;
355 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
356 gtk_widget_destroy(dlg);
357 if (text[0] == 0) {
358 g_free(text);
359 text = NULL;
361 xml_escape(&text);
363 buffer = jam_doc_get_text_buffer(doc);
365 gtk_text_buffer_begin_user_action(buffer); /* start undo action */
366 if (!gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) {
367 if (text) {
368 gtk_text_buffer_insert_at_cursor(buffer, "<lj-cut text=\"", -1);
369 gtk_text_buffer_insert_at_cursor(buffer, text, -1);
370 gtk_text_buffer_insert_at_cursor(buffer, "\">", -1);
371 } else {
372 gtk_text_buffer_insert_at_cursor(buffer, "<lj-cut>", -1);
374 } else {
375 if (text) {
376 gtk_text_buffer_insert(buffer, &start, "<lj-cut text=\"", -1);
377 gtk_text_buffer_insert(buffer, &start, text, -1);
378 gtk_text_buffer_insert(buffer, &start, "\">", -1);
379 } else {
380 gtk_text_buffer_insert(buffer, &start, "<lj-cut>", -1);
382 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
383 gtk_text_buffer_insert(buffer, &end, "</lj-cut>", -1);
384 gtk_text_buffer_move_mark_by_name(buffer, "insert", &end);
385 gtk_text_buffer_move_mark_by_name(buffer, "selection_bound", &end);
387 g_free(text);
389 gtk_text_buffer_end_user_action(buffer);