cosmetix
[k8lowj.git] / src / tools.c
blob71e1172858d87bb40ee3de5f1923e4c9a7cee891
1 /* logjam - a GTK client for LiveJournal.
2 * Copyright (C) 2000-2003 Evan Martin <evan@livejournal.com>
3 */
4 #include "gtk-all.h"
5 #include "util-gtk.h"
7 #include <string.h>
9 #include <libxml/parser.h>
11 #include "tools.h"
12 #include "util.h"
15 typedef struct {
16 gunichar c;
17 const char *escaped;
18 } htmlescape;
20 static const htmlescape htmlescapes[] = {
21 {'<', "&lt;"},
22 {'>', "&gt;"},
23 {'&', "&amp;"},
24 /* FIXME: add more entities here?
25 * the above are all that are really necessary, though. */
26 {0, 0}
30 void tools_html_escape (GtkWindow *win, JamDoc *doc) {
31 GtkTextBuffer *buffer;
32 GtkTextIter start, end, pos, nextpos;
33 const htmlescape *esc;
34 gunichar c;
35 int len;
37 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; gtk_text_iter_in_range(&pos, &start, &end); gtk_text_iter_forward_char(&pos)) {
49 c = gtk_text_iter_get_char(&pos);
50 for (esc = htmlescapes; esc->c != 0; ++esc) if (esc->c == c) break;
51 /* if this character isn't in the to-escape list, continue. */
52 if (esc->c == 0) continue;
54 nextpos = pos;
55 gtk_text_iter_forward_char(&nextpos);
56 gtk_text_buffer_delete(buffer, &pos, &nextpos);
57 len = (int)strlen(esc->escaped);
58 gtk_text_buffer_insert(buffer, &pos, esc->escaped, len);
60 /* to counteract the pos++ in the for loop */
61 gtk_text_iter_backward_char(&pos);
63 /* changing the buffer invalidates this iterator */
64 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
67 gtk_text_buffer_move_mark_by_name(buffer, "insert", &end);
68 gtk_text_buffer_move_mark_by_name(buffer, "selection_bound", &end);
70 gtk_text_buffer_end_user_action(buffer);
74 void tools_remove_linebreaks (GtkWindow *win, JamDoc *doc) {
75 GtkTextBuffer *buffer;
76 GtkTextIter start, end, pos, nextpos;
77 gint nlrun = 0;
78 gboolean leading = TRUE;
80 buffer = jam_doc_get_text_buffer(doc);
82 if (!gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) {
83 /* no selection; give them some help. */
84 jam_messagebox(win, _("Linebreak Removal Help"),
85 _("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."));
86 return;
89 gtk_text_buffer_begin_user_action(buffer); /* atomic, in terms of undo */
91 for (pos = start; gtk_text_iter_in_range(&pos, &start, &end); gtk_text_iter_forward_char(&pos)) {
92 /* we remove linebreaks as we see them; but later we will
93 * make up for our deletions by inserting spaces or \n\ns
94 * as appropriate. */
95 if (gtk_text_iter_ends_line(&pos)) {
96 if (!leading) ++nlrun; /* leading linebreaks should just be removed */
98 nextpos = pos;
99 gtk_text_iter_forward_line(&nextpos);
100 gtk_text_buffer_delete(buffer, &pos, &nextpos);
102 gtk_text_iter_backward_char(&pos);
104 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
106 continue;
109 leading = FALSE;
111 /* make up for what we removed, according to how long a run it was. */
112 if (nlrun == 1) {
113 /* line separators turn into a space */
114 nlrun = 0;
115 gtk_text_buffer_insert(buffer, &pos, " ", 1);
116 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
117 } else if (nlrun > 1) {
118 /* paragraph breaks are normalized */
119 nlrun = 0;
120 gtk_text_buffer_insert(buffer, &pos, "\n\n", 2);
121 /* this is safe, since we've just added two characters. */
122 gtk_text_iter_forward_chars(&pos, 2);
123 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
127 gtk_text_buffer_move_mark_by_name(buffer, "insert", &end);
128 gtk_text_buffer_move_mark_by_name(buffer, "selection_bound", &end);
130 gtk_text_buffer_end_user_action(buffer);
134 void tools_insert_file (GtkWindow *win, JamDoc *doc) {
135 GtkWidget *filesel;
136 GtkWidget *hbox, *label, *combo;
137 GList *strings = NULL;
138 const char *localeenc;
140 filesel = gtk_file_chooser_dialog_new(_("Select File"), win,
141 GTK_FILE_CHOOSER_ACTION_OPEN,
142 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL);
143 label = gtk_label_new(NULL);
144 gtk_label_set_text_with_mnemonic(GTK_LABEL(label), _("File _encoding:"));
145 combo = gtk_combo_new();
147 /* if they're in a non-UTF-8 locale, default to the locale encoding. */
148 if (!g_get_charset(&localeenc)) strings = g_list_append(strings, (char *)localeenc);
149 /* what else should go in this list other than UTF-8? */
150 strings = g_list_append(strings, "UTF-8");
151 gtk_combo_set_popdown_strings(GTK_COMBO(combo), strings);
153 gtk_label_set_mnemonic_widget(GTK_LABEL(label), GTK_COMBO(combo)->entry);
155 hbox = gtk_hbox_new(FALSE, 12);
156 gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
157 gtk_box_pack_start(GTK_BOX(hbox), combo, TRUE, TRUE, 0);
159 gtk_box_pack_start(GTK_BOX(GTK_DIALOG(filesel)->vbox), hbox, FALSE, FALSE, 0);
160 gtk_widget_show_all(hbox);
162 while (gtk_dialog_run(GTK_DIALOG(filesel)) == GTK_RESPONSE_ACCEPT) {
163 const gchar *filename;
164 const gchar *encoding;
165 GError *err = NULL;
166 filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filesel));
167 encoding = gtk_entry_get_text(GTK_ENTRY(GTK_COMBO(combo)->entry));
168 if (!jam_doc_insert_file(doc, filename, encoding, &err)) {
169 jam_warning(GTK_WINDOW(filesel), _("Error loading file: %s"), err->message);
170 g_error_free(err);
171 } else {
172 break;
175 gtk_widget_destroy(filesel);
179 void tools_insert_command_output (GtkWindow *win, JamDoc *doc) {
180 /* FIXME: use combo with a history drop-down, save the history */
181 GtkWidget *cmd_dlg, *entry, *box;
183 cmd_dlg = gtk_dialog_new_with_buttons(_("Insert Command Output"),
184 win, GTK_DIALOG_MODAL, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OK, GTK_RESPONSE_OK, NULL);
185 gtk_window_set_transient_for(GTK_WINDOW(cmd_dlg), win);
187 entry = gtk_entry_new();
189 box = labelled_box_new(_("_Command:"), entry);
190 jam_dialog_set_contents(GTK_DIALOG(cmd_dlg), box);
191 gtk_widget_grab_focus(entry);
192 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
193 gtk_dialog_set_default_response(GTK_DIALOG(cmd_dlg), GTK_RESPONSE_OK);
195 while (gtk_dialog_run(GTK_DIALOG(cmd_dlg)) == GTK_RESPONSE_OK) {
196 const gchar *command;
197 GError *err = NULL;
198 command = gtk_entry_get_text(GTK_ENTRY(entry));
199 if (!jam_doc_insert_command_output(doc, command, NULL, &err, win) && (err)) {
200 jam_warning(GTK_WINDOW(cmd_dlg), _("Error getting command output: %s"), err->message);
201 if (err->code != 127) break;
202 g_error_free(err);
203 } else {
204 break;
207 gtk_widget_destroy(cmd_dlg);
211 /* it appears there's no way to attach a pointer to a libXML
212 * parser context, which means we need to use a global variable
213 * so we can save the error message it tries to print to stdout
214 * and display it in a dialog instead. as another hack, we need
215 * to print that text in a monospaced font because it highlights
216 * the invalid character position...
218 * the ideal solution would be for libxml to use GError to report
219 * errors and then I wouldn't need any of this.
221 static GString *xml_error_context_hack;
224 static __attribute__((format(printf,2,3))) void xmlErrFunc (void *ctx, const char *msg, ...) {
225 char *s;
226 va_list ap;
227 va_start(ap, msg);
228 s = g_strdup_vprintf(msg, ap);
229 g_string_append(xml_error_context_hack, s);
230 g_free(s);
231 va_end(ap);
235 static void xml_error_dialog (GtkWindow *parent, GString *errstr) {
236 GtkWidget *dlg, *box, *label;
238 dlg = gtk_dialog_new_with_buttons(_("XML Parse Error"), parent, GTK_DIALOG_MODAL, GTK_STOCK_OK, GTK_STOCK_CANCEL, NULL);
240 box = gtk_vbox_new(FALSE, 5);
242 gtk_box_pack_start(GTK_BOX(box), gtk_label_new(_("Error parsing XML:")), FALSE, FALSE, 0);
244 label = gtk_label_new(errstr->str);
245 gtk_label_set_selectable(GTK_LABEL(label), TRUE);
246 jam_widget_set_font(label, "monospace");
247 gtk_box_pack_start(GTK_BOX(box), label, TRUE, TRUE, 0);
249 jam_dialog_set_contents(GTK_DIALOG(dlg), box);
251 gtk_dialog_run(GTK_DIALOG(dlg));
252 gtk_widget_destroy(dlg);
256 static gboolean validate_xml (GtkWindow *parent, const char *text) {
257 xmlParserCtxtPtr ctxt = xmlCreateDocParserCtxt(BAD_CAST text);
258 xml_error_context_hack = g_string_new(NULL);
259 xmlSetGenericErrorFunc(ctxt, xmlErrFunc);
260 if (xmlParseDocument(ctxt) < 0) {
261 xml_error_dialog(parent, xml_error_context_hack);
262 xmlFreeParserCtxt(ctxt);
263 g_string_free(xml_error_context_hack, TRUE);
264 return FALSE;
266 initGenericErrorDefaultFunc(NULL);
267 xmlFreeParserCtxt(ctxt);
268 g_string_free(xml_error_context_hack, TRUE);
269 return TRUE;
273 void tools_validate_xml (GtkWindow *win, JamDoc *doc) {
274 GtkTextBuffer *buffer;
275 GtkTextIter start, end;
276 char *entry, *str;
278 buffer = jam_doc_get_text_buffer(doc);
280 /* default to the selection, but fall back to the entire entry. */
281 if (!gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) gtk_text_buffer_get_bounds(buffer, &start, &end);
283 entry = gtk_text_buffer_get_text(buffer, &start, &end, TRUE);
284 str = g_strdup_printf("<entry>%s</entry>", entry);
286 if (validate_xml(win, str)) jam_messagebox(win, _("XML Validation"), _("Document is well-formed XML."));
288 g_free(entry);
289 g_free(str);
293 void tools_ljcut (GtkWindow *win, JamDoc *doc) {
294 GtkTextBuffer *buffer;
295 GtkTextIter start, end;
296 GtkWidget *dlg, *vbox, *hbox, *label, *entry;
297 char *text;
299 dlg = gtk_dialog_new_with_buttons(_("LJ-Cut"), win, GTK_DIALOG_MODAL, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OK, GTK_RESPONSE_OK, NULL);
300 gtk_dialog_set_default_response(GTK_DIALOG(dlg), GTK_RESPONSE_OK);
302 vbox = gtk_vbox_new(FALSE, 5);
303 gtk_container_set_border_width(GTK_CONTAINER(vbox), 10);
305 entry = gtk_entry_new();
306 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
307 hbox = labelled_box_new(_("Cut c_aption:"), entry);
308 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
310 label = gtk_label_new(NULL);
311 gtk_label_set_markup(GTK_LABEL(label), _("<small>If left empty, the LiveJournal default " "will be used.</small>"));
312 gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
314 jam_dialog_set_contents(GTK_DIALOG(dlg), vbox);
316 if (gtk_dialog_run(GTK_DIALOG(dlg)) != GTK_RESPONSE_OK) {
317 gtk_widget_destroy(dlg);
318 return;
320 text = gtk_editable_get_chars(GTK_EDITABLE(entry), 0, -1);
321 gtk_widget_destroy(dlg);
322 if (text[0] == 0) {
323 g_free(text);
324 text = NULL;
326 xml_escape(&text);
328 buffer = jam_doc_get_text_buffer(doc);
330 gtk_text_buffer_begin_user_action(buffer); /* start undo action */
331 if (!gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) {
332 if (text) {
333 gtk_text_buffer_insert_at_cursor(buffer, "<lj-cut text=\"", -1);
334 gtk_text_buffer_insert_at_cursor(buffer, text, -1);
335 gtk_text_buffer_insert_at_cursor(buffer, "\">", -1);
336 } else {
337 gtk_text_buffer_insert_at_cursor(buffer, "<lj-cut>", -1);
339 } else {
340 if (text) {
341 gtk_text_buffer_insert(buffer, &start, "<lj-cut text=\"", -1);
342 gtk_text_buffer_insert(buffer, &start, text, -1);
343 gtk_text_buffer_insert(buffer, &start, "\">", -1);
344 } else {
345 gtk_text_buffer_insert(buffer, &start, "<lj-cut>", -1);
347 gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
348 gtk_text_buffer_insert(buffer, &end, "</lj-cut>", -1);
349 gtk_text_buffer_move_mark_by_name(buffer, "insert", &end);
350 gtk_text_buffer_move_mark_by_name(buffer, "selection_bound", &end);
352 g_free(text);
354 gtk_text_buffer_end_user_action(buffer);