1 /*
2 * LaTeX.c
3 * pidgin-latex plugin
4 *
5 * This a plugin for Pidgin to display LaTeX formula in conversation
6 *
7 * PLEASE, send any comment, bug report, etc. to the trackers at sourceforge.net
8 *
9 * Copyright (C) 2006-2007 Benjamin Moll (qjuh@users.sourceforge.net)
10 * some portions : Copyright (C) 2004-2006 Nicolas Schoonbroodt (nicolas@ffsa.be)
11 * Copyright (C) 2004-2006 GRIm@ (thegrima@altern.org).
12 * Copyright (C) 2004-2006 Eric Betts (bettse@onid.orst.edu).
13 * Windows port : Copyright (C) 2005-2006 Nicolai Stange (nic-stange@t-online.de)
14 * Other portions heavily inspired and copied from gaim sources
15 * Copyright (C) 1998-2007 Pidgin developers pidgin.im
16 *
17 * This program is free software; you can redistribute it and/or modify
18 * it under the terms of the GNU General Public License as published by
19 * the Free Software Foundation; This document is under the scope of
20 * the version 2 of the License, or any later version.
21 *
22 * This program is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * You should have received a copy of the GNU General Public License
28 * along with this program (see COPYING); if not, write to the Free Software
29 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
30 *
31 */
32 #include "LaTeX.h"
33
34 #include <stdio.h>
35 #include <string.h>
36 #include <unistd.h>
37
38 #ifndef MAX
39 #define MAX(a,b) ( (a) > (b) ? (a ): (b))
40 #endif
41
42 #ifdef _WIN32
43 #include <windows.h>
44 #endif
45
46 static void
open_log(PurpleConversation * conv)47 open_log(PurpleConversation *conv)
48 {
49 conv->logs = g_list_append(NULL, purple_log_new(conv->type == PURPLE_CONV_TYPE_CHAT ? PURPLE_LOG_CHAT :
50 PURPLE_LOG_IM, conv->name, conv->account,
51 conv, time(NULL), NULL));
52 }
53
54 #ifdef _WIN32
win32_purple_notify_error(char * prep)55 void win32_purple_notify_error(char *prep)
56 {
57 char *errmsg=NULL;
58 char *finalmsg=NULL;
59 if(!FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (void*)&errmsg, 0, NULL))
60 {
61 purple_notify_error(NULL, "LaTeX" ,"Can't display error message.", NULL);
62 return;
63 }
64
65 if(prep)
66 {
67 finalmsg=malloc((strlen(errmsg)+ strlen(prep) + 3)*sizeof(char));
68 if(!finalmsg)
69 {
70 purple_notify_error(NULL, "LaTeX", "Can't display error message.", NULL);
71 LocalFree(errmsg); /* we can't do anything more for you */
72 return;
73 }
74 strcpy(finalmsg, prep);
75 strcat(finalmsg, ": ");
76 strcat(finalmsg, errmsg);
77 LocalFree(errmsg);
78 }
79 else
80 {
81 finalmsg = malloc((strlen(errmsg)+1)*sizeof(char));
82 if(!finalmsg)
83 {
84 purple_notify_error(NULL, "LaTeX", "Can't display error message.", NULL);
85 LocalFree(errmsg); /* we can't do anything more for you */
86 return;
87 }
88 strcpy(finalmsg, errmsg);
89 LocalFree(errmsg);
90 }
91 purple_notify_error(NULL, "LaTeX", finalmsg, NULL);
92 free(finalmsg);
93 return;
94 }
95 #endif
96
getdirname(const char const * file)97 static char* getdirname(const char const *file)
98 {
99 char *s=NULL;
100 char *r=NULL;
101 s=strrchr(file, G_DIR_SEPARATOR);
102 if(!s) /* just a pure filename without dir? */
103 {
104 /* Here is no standard-, but GNU-bahaviour of getcwd assumed.
105 Note that msdn.microsoft.com defines the same as GNU.
106 */
107 return getcwd(NULL,0);
108 }
109
110 s+=1; /* get the G_DIR_SEPARATOR at the end of directory-string */
111 r=malloc(s-file+sizeof(char));
112 if(r)
113 {
114 memcpy(r,file, s-file);
115 r[(s-file)/sizeof(char)]='\0';
116 }
117 return r;
118 }
119
getfilename(const char const * file)120 static char* getfilename(const char const *file)
121 {
122 char *s=NULL;
123 char *r=NULL;
124 s=strrchr(file,G_DIR_SEPARATOR);
125 if(!s)
126 {
127 r=malloc((strlen(file)+1)*sizeof(char));
128 strcpy(r,file);
129 return r;
130 }
131
132 s+=1;
133 r=malloc((strlen(file)+1)*sizeof(char)+file-s);
134 if(r)
135 {
136 memcpy(r,s,strlen(file)*sizeof(char)+file-s);
137 r[strlen(file)+(file-s)/sizeof(char)]='\0';
138 }
139 return r;
140 }
141
searchPATH(const char const * file)142 char* searchPATH(const char const *file)
143 {
144 char *cmd=NULL;
145 #ifdef _WIN32
146 DWORD sz=0;
147 DWORD sz2=0;
148
149 sz=SearchPath(NULL, file, ".exe", 0, cmd, NULL);
150 cmd = malloc((sz+1)*sizeof(TCHAR));
151 if(cmd)
152 {
153 sz2=SearchPath(NULL, file, ".exe", sz+1, cmd, NULL);
154 if(!sz2)
155 {
156 free(cmd);
157 cmd=NULL;
158 }
159 }
160 #else
161 cmd=malloc((strlen(file)+1)*sizeof(char));
162 if(cmd)
163 strcpy(cmd, file);
164 #endif
165
166 return cmd;
167 }
168
169 #ifdef _WIN32 /* we could take system(), too, but this opens ugly console-windows within IM-Session*/
execute(char * cmd,char * opts[],int copts)170 static DWORD execute(char *cmd, char *opts[], int copts)
171 {
172 int i=0;
173 int len=strlen(cmd) + 4;
174 char *params=NULL;
175 DWORD exitcode=0;
176 for(i=0; i<copts; ++i)
177 len+=(strlen(opts[i]))*sizeof(char);
178
179 params=malloc(len);
180 if(!params)
181 return -1;
182
183 strcpy(params, "\"");
184 strcat(params, cmd);
185 strcat(params, "\" ");
186
187 for(i=0; i<copts; ++i)
188 strcat(params, opts[i]);
189
190 STARTUPINFO sup;
191 PROCESS_INFORMATION pi;
192 ZeroMemory( &sup, sizeof(sup) );
193 sup.cb = sizeof(sup);
194 ZeroMemory( &pi, sizeof(pi) );
195
196 if(!CreateProcess(NULL, params, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &sup, &pi))
197 {
198 win32_purple_notify_error(cmd);
199 free(params);
200 return -1;
201 }
202
203 free(params);
204 if(WAIT_OBJECT_0!=WaitForSingleObjectEx(pi.hProcess, INFINITE, FALSE))
205 {
206 win32_purple_notify_error(cmd);
207 CloseHandle(pi.hProcess);
208 CloseHandle(pi.hThread);
209 return -1;
210 }
211
212 if(!GetExitCodeThread(pi.hThread, &exitcode))
213 {
214 win32_purple_notify_error(cmd);
215 return -1;
216 }
217 CloseHandle(pi.hProcess);
218 CloseHandle(pi.hThread);
219 return exitcode;
220 }
221 #else /* not windows, take system() */
execute(char * cmd,char * opts[],int copts)222 static int execute(char *cmd, char *opts[], int copts)
223 {
224 int i=0;
225 int exitcode=-1;
226 int len=strlen(cmd) + 4;
227 char *params=NULL;
228
229 for(i=0; i<copts; ++i)
230 len+=(strlen(opts[i]))*sizeof(char);
231
232 params=malloc(len);
233 if(!params)
234 return -1;
235
236 strcpy(params, "\"");
237 strcat(params, cmd);
238 strcat(params, "\" ");
239
240 for(i=0; i<copts; ++i)
241 strcat(params, opts[i]);
242
243 exitcode=system(params);
244 free(params);
245 return exitcode;
246 }
247 #endif
248
get_latex_cmd()249 static char* get_latex_cmd()
250 {
251 return searchPATH("latex");
252 }
253
get_dvips_cmd()254 static char* get_dvips_cmd()
255 {
256 return searchPATH("dvips");
257 }
258
get_convert_cmd()259 static char* get_convert_cmd()
260 {
261 #ifdef _WIN32
262 /*open registry*/
263 DWORD type;
264 DWORD cbData;
265 char *pData=NULL;
266 HKEY hk=NULL;
267 RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SOFTWARE\\ImageMagick\\Current",0,KEY_READ,&hk);
268 RegQueryValueEx(hk, "BinPath", NULL, &type, NULL, &cbData);
269 cbData=cbData + (1+12)*sizeof(char);
270 pData=malloc(cbData);
271 if(pData && RegQueryValueEx(hk, "BinPath", NULL, &type, pData, &cbData)==ERROR_SUCCESS)
272 {
273 CloseHandle(hk);
274 strcat(pData,"\\convert.exe");
275 return pData;
276 }
277
278 CloseHandle(hk);
279 free(pData);
280 return searchPATH("convert");
281 #else
282 return searchPATH("convert");
283 #endif
284 }
285
is_blacklisted(char * message)286 static gboolean is_blacklisted(char *message)
287 {
288 char *not_secure[NB_BLACKLIST] = BLACKLIST;
289 int i;
290 for (i = 0 ; i < NB_BLACKLIST ; i++)
291 {
292 char *begin_not_secure = malloc((strlen(not_secure[i])+9)*sizeof(char));
293 strcpy(begin_not_secure,"\\begin{");
294 strcat(begin_not_secure,not_secure[i]+0x01);
295 strcat(begin_not_secure,"}");
296 if (strstr(message, not_secure[i]) != NULL || strstr(message, begin_not_secure)) return TRUE;
297 }
298 return FALSE;
299 }
300
latex_to_image(char * latex,char ** file_tex,char ** file_dvi,char ** file_ps,char ** file_png)301 static gboolean latex_to_image(char *latex, char **file_tex, char **file_dvi, char **file_ps, char **file_png)
302 {
303 FILE * texfile;
304 char *file_tmp = NULL;
305 char *tmpdir = NULL;
306 char *cmdlatex = NULL;
307 char *cmddvips = NULL;
308 char *cmdconvert = NULL;
309
310 /* the following is new and related to temporary-filename-generation */
311 texfile = purple_mkstemp(&file_tmp,TRUE);
312 *file_tex=malloc((strlen(file_tmp)+5)*sizeof(char));
313 *file_dvi=malloc((strlen(file_tmp)+5)*sizeof(char));
314 *file_ps=malloc((strlen(file_tmp)+4)*sizeof(char));
315 *file_png=malloc((strlen(file_tmp)+5)*sizeof(char));
316 if(!(file_tmp && *file_tex && *file_dvi && *file_ps && *file_png))
317 {
318 fclose(texfile);
319 unlink(file_tmp);
320 free(file_tmp);
321 free(*file_tex);
322 free(*file_dvi);
323 free(*file_ps);
324 free(*file_png);
325 *file_tex=*file_dvi=*file_ps=*file_png=NULL;
326 return FALSE;
327 }
328 strcpy(*file_tex, file_tmp);
329 strcat(*file_tex, ".tex");
330 strcpy(*file_dvi, file_tmp);
331 strcat(*file_dvi, ".dvi");
332 strcpy(*file_ps, file_tmp);
333 strcat(*file_ps, ".ps");
334 strcpy(*file_png, file_tmp);
335 strcat(*file_png, ".jpg");
336 free(file_tmp);
337 fclose(texfile);
338
339 if (! (texfile = fopen(*file_tex, "w"))) return FALSE;
340 fprintf(texfile, HEADER HEADER_MATH "%s" FOOTER_MATH FOOTER, latex);
341 fclose (texfile);
342
343 tmpdir=getdirname(*file_tex);
344 /* generate commands, also new */
345 char *latexopts[5]={"--interaction=nonstopmode", " ", "\"", *file_tex, "\""};
346 char *dvipsopts[8]={"-E", " ", "-o", " \"", *file_ps, "\" \"", *file_dvi, "\""};
347 char *convertopts[5]={"\"", *file_ps, "\" \"", *file_png, "\""};
348 cmdlatex=get_latex_cmd();
349 cmddvips=get_dvips_cmd();
350 cmdconvert=get_convert_cmd();
351
352 if (!tmpdir || chdir(tmpdir) || !cmdlatex || !cmddvips || !cmdconvert)
353 {
354 free(*file_dvi);
355 free(*file_ps);
356 free(*file_png);
357 *file_dvi=*file_ps=*file_png=NULL;
358 free(cmdlatex);
359 free(cmddvips);
360 free(cmdconvert);
361 return FALSE;
362 }
363
364 free(tmpdir);
365 if((execute(cmdlatex, latexopts, 5) || execute(cmddvips, dvipsopts, 8) || execute(cmdconvert, convertopts, 5))) return FALSE;
366
367 free(cmdlatex);
368 free(cmddvips);
369 free(cmdconvert);
370
371 return TRUE;
372 }
373
analyse(PurpleConversation * conv,char ** tmp2,char * startdelim,char * enddelim,gboolean remote)374 static gboolean analyse(PurpleConversation *conv, char **tmp2, char *startdelim, char *enddelim, gboolean remote)
375 {
376 int pos1, pos2, idimg;
377 char *ptr1, *ptr2;
378 char *file_tex=NULL;
379 char *file_dvi=NULL;
380 char *file_ps=NULL;
381 char *file_png=NULL;
382
383 ptr1 = strstr(*tmp2, startdelim);
384 while(ptr1 != NULL)
385 {
386 char *tex, *message, *filter, *idstring;
387 gchar *name, *buf, *filedata;
388 size_t size;
389 GError *error = NULL;
390
391 pos1 = strlen(*tmp2) - strlen(ptr1);
392
393 // Have to ignore the first 2 char ("$$") --> & [2]
394 ptr2 = strstr(&ptr1[strlen(startdelim)], enddelim);
395 if (ptr2 == NULL)
396 { return FALSE; }
397
398 pos2 = strlen(*tmp2) - strlen(ptr2) + strlen(enddelim);
399
400 if ((tex = malloc(pos2 - pos1 - strlen(enddelim) - strlen(startdelim) + 1)) == NULL)
401 {
402 // TODO: Report the error
403 return FALSE;
404 }
405
406 strncpy(tex, &ptr1[strlen(startdelim)], pos2 - pos1 - strlen(startdelim)-strlen(enddelim));
407 tex[pos2-pos1-strlen(startdelim)-strlen(enddelim)] = '\0';
408
409 // Pidgin transform & to & and I make the inverse transformation
410 while ( (filter = strstr(tex, FILTER_AND) ) != NULL)
411 {
412 strcpy(&tex[strlen(tex) - strlen(filter) + 1], &filter[5]);
413 }
414 // <br> filter
415 while ( (filter = strstr(tex, FILTER_BR) ) != NULL)
416 {
417 strcpy(&tex[strlen(tex) - strlen(filter)], &filter[4]);
418 }
419
420 // Creates the image in file_png
421 if (!latex_to_image(tex, &file_tex, &file_dvi, &file_ps, &file_png)) { free(tex); return FALSE; };
422 free(tex);
423
424 // loading image
425 if (!g_file_get_contents(file_png, &filedata, &size, &error))
426 {
427 purple_notify_error(NULL, "LaTeX", error->message, NULL);
428 g_error_free(error);
429 return FALSE;
430 }
431
432 unlink(file_tex);
433 file_tex[strlen(file_tex)-4]='\0';
434
435 strcat(file_tex, ".aux");
436 unlink(file_tex);
437 file_tex[strlen(file_tex)-4]='\0';
438 strcat(file_tex, ".log");
439 unlink(file_tex);
440 unlink(file_dvi);
441 unlink(file_ps);
442 unlink(file_png);
443
444 free(file_tex);
445 free(file_dvi);
446 free(file_ps);
447 free(file_png);
448
449
450 name = "pidginTeX.jpg";
451
452 idimg = purple_imgstore_add_with_id(filedata, MAX(1024,size), name);
453 filedata = NULL;
454
455 if (idimg == 0)
456 {
457 buf = g_strdup_printf("Failed to store image.");
458 purple_notify_error(NULL,"LaTeX", buf, NULL);
459 g_free(buf);
460 return FALSE;
461 }
462
463 idstring = malloc(10);
464 sprintf(idstring, "%d", idimg);
465
466 // making new message
467 if ((message = malloc (strlen(*tmp2) - pos2 + pos1 + strlen(idstring) + strlen(IMG_BEGIN) + strlen(IMG_END) + 1)) == NULL)
468 {
469 purple_notify_error(NULL,"LaTeX", "couldn't make the message.", NULL);
470 return FALSE;
471 }
472
473 if (pos1 > 0)
474 {
475 strncpy(message, *tmp2, pos1);
476 message[pos1] = '\0';
477 strcat(message, IMG_BEGIN);
478 } else {
479 strcpy(message, IMG_BEGIN);
480 }
481 strcat(message, idstring);
482 strcat(message, IMG_END);
483
484 free(idstring);
485
486 if (pos2 < strlen(*tmp2))
487 { strcat(message, &ptr2[strlen(enddelim)]); }
488
489 free(*tmp2);
490 if ((*tmp2 = malloc(strlen(message)+1)) == NULL)
491 {
492 purple_notify_error(NULL,"LaTeX", "couldn't split the message.", NULL);
493 return FALSE;
494 }
495
496 strcpy(*tmp2, message);
497 free(message);
498
499 ptr1 = strstr(*tmp2, startdelim);
500 }
501 return TRUE;
502 }
503
pidgin_latex_write(PurpleConversation * conv,char * nom,char * message,PurpleMessageFlags messFlag,char * original)504 static gboolean pidgin_latex_write(PurpleConversation *conv, char *nom, char *message, PurpleMessageFlags messFlag, char *original)
505 {
506 gboolean logflag;
507
508 // writing log
509 logflag = purple_conversation_is_logging(conv);
510
511 if (logflag)
512 {
513 GList *log;
514
515 if (conv->logs == NULL)
516 open_log(conv);
517
518 log = conv->logs;
519 while (log != NULL) {
520 purple_log_write((PurpleLog *)log->data, messFlag, nom, time(NULL), original);
521 log = log->next;
522 }
523 purple_conversation_set_logging(conv,FALSE);
524 }
525
526 purple_conv_im_write(PURPLE_CONV_IM(conv), nom, message, messFlag, time(NULL));
527
528 if (logflag)
529 purple_conversation_set_logging(conv,TRUE);
530
531 return FALSE;
532 }
533
message_send(PurpleAccount * account,const char * who,char ** buffer,PurpleConversation * conv,PurpleMessageFlags flags)534 static gboolean message_send(PurpleAccount *account, const char *who, char **buffer, PurpleConversation *conv, PurpleMessageFlags flags)
535 {
536 char *tmp2;
537 int t[10], i = 0;//, j;
538
539 // if nothing to do
540 if (/*strstr(*buffer, BEG) == NULL &&*/ strstr(*buffer,KOPETE_TEX) == NULL)
541 {
542 return FALSE;
543 };
544
545 if (is_blacklisted(*buffer)) return FALSE;
546
547 if((tmp2 = malloc(strlen(*buffer)+1)) == NULL)
548 {
549 // TODO: Notify Error
550 return FALSE;
551 }
552
553 strcpy(tmp2,*buffer);
554
555 if (analyse(conv, &tmp2, KOPETE_TEX, KOPETE_TEX, FALSE) )
556 {
557 char *name2;
558
559 // TODO : THIS IS VERY VERY UGLY
560 // TODO : MODIFY TO MINIMIZE CALL
561 // finding name of sender
562 if (purple_account_get_alias(account) != NULL)
563 {
564 name2 = malloc(strlen(purple_account_get_alias(account))+1);
565 strcpy(name2, purple_account_get_alias(account));
566 }
567 else if (purple_account_get_username(account) != NULL)
568 {
569 name2 = malloc(strlen(purple_account_get_username(account))+1);
570 strcpy(name2,purple_account_get_username(account));
571 } else {
572 free(tmp2);
573 return FALSE;
574 }
575
576 pidgin_latex_write(conv, name2, tmp2, PURPLE_MESSAGE_SEND, *buffer);
577
578 free(tmp2);
579 free(name2);
580
581 return TRUE;
582 }
583
584 free(tmp2);
585
586 return FALSE;
587 }
588
message_recv(PurpleAccount * account,char ** sender,char ** buffer,PurpleConversation * conv,PurpleMessageFlags * flags)589 static gboolean message_recv(PurpleAccount *account, char **sender, char **buffer, PurpleConversation *conv, PurpleMessageFlags *flags)
590 {
591
592 // if no $$ -> nothing to do
593 if (strstr(*buffer, BEG) == NULL && strstr(*buffer, KOPETE_TEX) == NULL)
594 {
595 return FALSE;
596 };
597
598 if (is_blacklisted(*buffer)) return FALSE;
599
600 if (conv==NULL) conv = purple_conversation_new(PURPLE_CONV_TYPE_IM,account,*sender);
601 if (purple_conversation_get_im_data(conv) != NULL)
602 {
603 char *tmp2;
604 int t[10], i = 0;//, j;
605
606 if ((tmp2 = malloc(strlen(*buffer)+1)) == NULL)
607 {
608 // TODO: Report the error
609 return FALSE;
610 }
611
612 strcpy(tmp2,*buffer);
613
614 if (analyse(conv, &tmp2, KOPETE_TEX, KOPETE_TEX, TRUE));
615 {
616 pidgin_latex_write(conv, *sender, tmp2, PURPLE_MESSAGE_RECV, *buffer);
617
618 free(tmp2); tmp2 = NULL;
619 free(*buffer);
620 *buffer = NULL;
621 return TRUE;
622 }
623
624 free(tmp2);
625 return FALSE;
626 }
627
628 return FALSE;
629 }
630
631
plugin_load(PurplePlugin * plugin)632 static gboolean plugin_load(PurplePlugin *plugin)
633 {
634 void *conv_handle = purple_conversations_get_handle();
635
636 purple_signal_connect(conv_handle, "writing-im-msg",
637 plugin, PURPLE_CALLBACK(message_send), NULL);
638
639 purple_signal_connect_priority(conv_handle, "receiving-im-msg",
640 plugin, PURPLE_CALLBACK(message_recv), NULL, -9999);
641 purple_debug(PURPLE_DEBUG_INFO, "LaTeX", "LaTeX loaded\n");
642
643 return TRUE;
644 }
645
plugin_unload(PurplePlugin * plugin)646 static gboolean plugin_unload(PurplePlugin * plugin)
647 {
648 void *conv_handle = purple_conversations_get_handle();
649
650 purple_signal_disconnect(conv_handle, "writing-im-msg", plugin, PURPLE_CALLBACK(message_send));
651 purple_signal_disconnect(conv_handle, "receiving-im-msg", plugin, PURPLE_CALLBACK(message_recv));
652
653 return TRUE;
654 }
655
656
657 static PurplePluginInfo info =
658 {
659 PURPLE_PLUGIN_MAGIC,
660 PURPLE_MAJOR_VERSION,
661 PURPLE_MINOR_VERSION,
662 PURPLE_PLUGIN_STANDARD, /**< type */
663 NULL, /**< ui_requirement */
664 0, /**< flags */
665 NULL, /**< dependencies */
666 PURPLE_PRIORITY_DEFAULT, /**< priority */
667
668 LATEX_PLUGIN_ID, /**< id */
669 "LaTeX", /**< name */
670 "1.0", /**< version */
671 /** summary */
672 "To display LaTeX formula into Pidgin conversation.",
673 /** description */
674 "Put LaTeX-code between $$ ... $$ markup to have it displayed as Picture in your conversation.\nRemember that your contact needs an similar plugin or else he will just see the pure LaTeX-code\nYou must have LaTeX and ImageMagick installed (in your PATH)",
675 "Benjamin Moll <qjuh@users.sourceforge.net>\nNicolas Schoonbroodt <nicolas@ffsa.be>\nNicolai Stange <nic-stange@t-online.de>", /**< author */
676 WEBSITE, /**< homepage */
677 plugin_load, /**< load */
678 plugin_unload, /**< unload */
679 NULL, /**< destroy */
680 NULL, /**< ui_info */
681 NULL, /**< extra_info */
682 NULL,
683 NULL,
684 NULL,
685 NULL,
686 NULL,
687 NULL
688 };
689
690
691
692 static void
init_plugin(PurplePlugin * plugin)693 init_plugin(PurplePlugin *plugin)
694 {
695 }
696
697 PURPLE_INIT_PLUGIN(LaTeX, init_plugin, info)
698