| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | ## This file is part of Gajim. |
|---|
| 3 | ## |
|---|
| 4 | ## Gajim is free software; you can redistribute it and/or modify |
|---|
| 5 | ## it under the terms of the GNU General Public License as published |
|---|
| 6 | ## by the Free Software Foundation; version 3 only. |
|---|
| 7 | ## |
|---|
| 8 | ## Gajim is distributed in the hope that it will be useful, |
|---|
| 9 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 10 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 11 | ## GNU General Public License for more details. |
|---|
| 12 | ## |
|---|
| 13 | ## You should have received a copy of the GNU General Public License |
|---|
| 14 | ## along with Gajim. If not, see <http://www.gnu.org/licenses/>. |
|---|
| 15 | ## |
|---|
| 16 | |
|---|
| 17 | ''' |
|---|
| 18 | LaTeX Plugin. |
|---|
| 19 | |
|---|
| 20 | based on Acronym Plugin by Mateusz Biliński |
|---|
| 21 | :author: Yves Fischer <yvesf@xapek.org> |
|---|
| 22 | :since: 17th June 2008 |
|---|
| 23 | :copyright: Copyright (2008) Yves Fischer |
|---|
| 24 | :license: GPL |
|---|
| 25 | ''' |
|---|
| 26 | |
|---|
| 27 | |
|---|
| 28 | from threading import Thread |
|---|
| 29 | from random import random |
|---|
| 30 | import sys,gtk,gobject |
|---|
| 31 | |
|---|
| 32 | gtk.gdk.threads_init() ##for gtk.gdk.thread_[enter|leave]() |
|---|
| 33 | |
|---|
| 34 | |
|---|
| 35 | #following constants are taken from Pidgin Latex Plugin |
|---|
| 36 | def latex_template(code): |
|---|
| 37 | return """\\documentclass[12pt]{article} |
|---|
| 38 | \\usepackage[dvips]{graphicx} |
|---|
| 39 | \\usepackage{amsmath} |
|---|
| 40 | \\usepackage{amssymb} |
|---|
| 41 | \\pagestyle{empty} |
|---|
| 42 | \\begin{document} |
|---|
| 43 | \\begin{gather*} |
|---|
| 44 | %s |
|---|
| 45 | \\end{gather*} |
|---|
| 46 | \\end{document}""" % (code) |
|---|
| 47 | |
|---|
| 48 | |
|---|
| 49 | #used in pidgin-latex, necessary in gajim? |
|---|
| 50 | """ |
|---|
| 51 | FILTER_AND="&" |
|---|
| 52 | FILTER_LT= "<" |
|---|
| 53 | FILTER_GT=">" |
|---|
| 54 | FILTER_BR="<br>" |
|---|
| 55 | """ |
|---|
| 56 | |
|---|
| 57 | """ |
|---|
| 58 | Yes, this is simply a copy/paste of KopeteTex blacklist. |
|---|
| 59 | But too bad in LaTeX and system security to verify all |
|---|
| 60 | of this |
|---|
| 61 | """ |
|---|
| 62 | BLACKLIST=[ |
|---|
| 63 | "\def", |
|---|
| 64 | "\\let", |
|---|
| 65 | "\\futurelet", |
|---|
| 66 | "\\newcommand", |
|---|
| 67 | "\\renewcomment", |
|---|
| 68 | "\\else", |
|---|
| 69 | "\\fi", |
|---|
| 70 | "\\write", |
|---|
| 71 | "\\input", |
|---|
| 72 | "\\include", |
|---|
| 73 | "\\chardef", |
|---|
| 74 | "\\catcode", |
|---|
| 75 | "\\makeatletter", |
|---|
| 76 | "\\noexpand", |
|---|
| 77 | "\\toksdef", |
|---|
| 78 | "\\every", |
|---|
| 79 | "\\errhelp", |
|---|
| 80 | "\\errorstopmode", |
|---|
| 81 | "\\scrollmode", |
|---|
| 82 | "\\nonstopmode", |
|---|
| 83 | "\\batchmode", |
|---|
| 84 | "\\read", |
|---|
| 85 | "\\csname", |
|---|
| 86 | "\\newhelp", |
|---|
| 87 | "\\relax", |
|---|
| 88 | "\\afterground", |
|---|
| 89 | "\\afterassignment", |
|---|
| 90 | "\\expandafter", |
|---|
| 91 | "\\noexpand", |
|---|
| 92 | "\\special", |
|---|
| 93 | "\\command", |
|---|
| 94 | "\\loop", |
|---|
| 95 | "\\repeat", |
|---|
| 96 | "\\toks", |
|---|
| 97 | "\\output", |
|---|
| 98 | "\\line", |
|---|
| 99 | "\\mathcode", |
|---|
| 100 | "\\name", |
|---|
| 101 | "\\item", |
|---|
| 102 | "\\section", |
|---|
| 103 | "\\mbox", |
|---|
| 104 | "\\DeclareRobustCommand" |
|---|
| 105 | ] |
|---|
| 106 | |
|---|
| 107 | |
|---|
| 108 | |
|---|
| 109 | |
|---|
| 110 | """ |
|---|
| 111 | |
|---|
| 112 | """ |
|---|
| 113 | class LatexRenderer(Thread): |
|---|
| 114 | def __init__(self, iter_start, iter_end, buffer, widget): |
|---|
| 115 | Thread.__init__(self) |
|---|
| 116 | |
|---|
| 117 | self.code = iter_start.get_text(iter_end) |
|---|
| 118 | self.mark_name = "LatexRendererMark%d" % (int(random() * 100000)) |
|---|
| 119 | self.mark = buffer.create_mark(self.mark_name, iter_start, True) |
|---|
| 120 | |
|---|
| 121 | self.buffer = buffer |
|---|
| 122 | self.widget = widget |
|---|
| 123 | |
|---|
| 124 | #delete code and show message "processing" |
|---|
| 125 | self.buffer.delete(iter_start, iter_end) |
|---|
| 126 | #iter_start.forward_char() |
|---|
| 127 | self.buffer.insert(iter_start, "Processing LaTeX") |
|---|
| 128 | |
|---|
| 129 | self.start() #start background processing |
|---|
| 130 | |
|---|
| 131 | def run(self): |
|---|
| 132 | gtk.gdk.threads_enter() #its nearly non-sense using this in a full thread |
|---|
| 133 | #should be fine-grained later |
|---|
| 134 | try: |
|---|
| 135 | if self.check_code(): |
|---|
| 136 | self.show_image() |
|---|
| 137 | else: |
|---|
| 138 | self.show_error("There are bad commands!") |
|---|
| 139 | except: |
|---|
| 140 | pass |
|---|
| 141 | finally: |
|---|
| 142 | gtk.gdk.threads_leave() |
|---|
| 143 | self.buffer.delete_mark(self.mark) |
|---|
| 144 | |
|---|
| 145 | def show_error(self, message): |
|---|
| 146 | iter_mark = self.buffer.get_iter_at_mark(self.mark) |
|---|
| 147 | iter_end = iter_mark.copy().forward_search("Processing LaTeX", gtk.TEXT_SEARCH_TEXT_ONLY)[1] |
|---|
| 148 | self.buffer.delete(iter_mark, iter_end) |
|---|
| 149 | |
|---|
| 150 | |
|---|
| 151 | pixbuf = self.widget.render_icon(gtk.STOCK_STOP, gtk.ICON_SIZE_BUTTON) |
|---|
| 152 | self.buffer.insert_pixbuf(iter_end, pixbuf) |
|---|
| 153 | self.buffer.insert(iter_end, message) |
|---|
| 154 | |
|---|
| 155 | def show_image(self): |
|---|
| 156 | from os import system |
|---|
| 157 | fn = "/tmp/gajim_latex-" + str(int(random() * 100000)) |
|---|
| 158 | |
|---|
| 159 | f = open(fn+".tex", 'w') |
|---|
| 160 | f.write(latex_template(self.code.split("$$")[1])) |
|---|
| 161 | f.close() |
|---|
| 162 | |
|---|
| 163 | system("latex --interaction=nonstopmode -output-directory=/tmp -output-format=dvi %s.tex" % fn) |
|---|
| 164 | |
|---|
| 165 | system("dvipng -T tight -x 1200 -z 9 -bg transparent -o %s.png %s.dvi" % (fn, fn)) |
|---|
| 166 | |
|---|
| 167 | try: |
|---|
| 168 | pixbuf = gtk.gdk.pixbuf_new_from_file(fn + ".png") |
|---|
| 169 | iter_mark = self.buffer.get_iter_at_mark(self.mark) |
|---|
| 170 | iter_end = iter_mark.copy().forward_search("Processing LaTeX", gtk.TEXT_SEARCH_TEXT_ONLY)[1] |
|---|
| 171 | self.buffer.delete(iter_mark, iter_end) |
|---|
| 172 | |
|---|
| 173 | self.buffer.insert_pixbuf(iter_end, pixbuf) |
|---|
| 174 | except gobject.GError: |
|---|
| 175 | self.show_error("Cant open %s.png, something around invoking latex goes wrong" % fn) |
|---|
| 176 | finally: |
|---|
| 177 | system("rm -vf %s.png %s.log %s.aux %s.tex" % (fn, fn, fn, fn)) |
|---|
| 178 | |
|---|
| 179 | def check_code(self): |
|---|
| 180 | for bad_cmd in BLACKLIST: |
|---|
| 181 | if self.code.find(bad_cmd) != -1: |
|---|
| 182 | print "Found bad command %s" % bad_cmd |
|---|
| 183 | return False |
|---|
| 184 | return True |
|---|
| 185 | |
|---|
| 186 | |
|---|
| 187 | |
|---|
| 188 | class LatexPluginConfiguration(gtk.Window): |
|---|
| 189 | def __init__(self): |
|---|
| 190 | gtk.Window.__init__(self) |
|---|
| 191 | self.set_title("Latex Plugin Configuration") |
|---|
| 192 | |
|---|
| 193 | self.pane = gtk.VBox() |
|---|
| 194 | self.add(self.pane) |
|---|
| 195 | |
|---|
| 196 | self.btn_test = gtk.Button("Test Latex Configuration") |
|---|
| 197 | self.btn_test.connect("clicked", self.start_test) |
|---|
| 198 | self.pane.pack_start(self.btn_test,expand=False) |
|---|
| 199 | |
|---|
| 200 | self.lbl_messages = gtk.Label("Results:\n") |
|---|
| 201 | self.lbl_messages.set_line_wrap(True) |
|---|
| 202 | self.pane.pack_start(self.lbl_messages) |
|---|
| 203 | |
|---|
| 204 | def log(self, message): |
|---|
| 205 | self.lbl_messages.set_text(self.lbl_messages.get_text() + "\n" + message) |
|---|
| 206 | |
|---|
| 207 | |
|---|
| 208 | """ |
|---|
| 209 | performs very simple checks (check if executable is in PATH) |
|---|
| 210 | """ |
|---|
| 211 | def start_test(self,widget): |
|---|
| 212 | log = self.log |
|---|
| 213 | from os import system |
|---|
| 214 | log("Start Test") |
|---|
| 215 | log("Test Latex Binary") |
|---|
| 216 | ret = system("latex -version") |
|---|
| 217 | if ret != 0: |
|---|
| 218 | log("No LaTeX binary found in PATH") |
|---|
| 219 | else: |
|---|
| 220 | log("OK") |
|---|
| 221 | log("Test dvipng") |
|---|
| 222 | ret = system("dvipng --version") |
|---|
| 223 | if ret != 0: |
|---|
| 224 | log("No dvipng binary found in PATH") |
|---|
| 225 | else: |
|---|
| 226 | log("OK") |
|---|
| 227 | |
|---|
| 228 | def run(self,parent): |
|---|
| 229 | self.present() |
|---|
| 230 | self.show_all() |
|---|
| 231 | |
|---|
| 232 | |
|---|
| 233 | from plugins import GajimPlugin |
|---|
| 234 | from plugins.helpers import log, log_calls |
|---|
| 235 | |
|---|
| 236 | |
|---|
| 237 | class LatexPlugin(GajimPlugin): |
|---|
| 238 | name = u'Latex Plugin' |
|---|
| 239 | short_name = u'latex' |
|---|
| 240 | version = u'0.1' |
|---|
| 241 | description = u'''Invoke Latex to render $$foobar$$ sourrounded Latex equations. Needs latex and dvipng''' |
|---|
| 242 | authors = [u'Yves Fischer <yvesf@xapek.org>'] |
|---|
| 243 | homepage = u'http://xapek.org' |
|---|
| 244 | |
|---|
| 245 | |
|---|
| 246 | def init(self): |
|---|
| 247 | print "init LatexPlugin" |
|---|
| 248 | self.config_dialog = LatexPluginConfiguration() |
|---|
| 249 | |
|---|
| 250 | self.gui_extension_points = { |
|---|
| 251 | 'chat_control_base' : (self.connect_with_chat_control_base, |
|---|
| 252 | self.disconnect_from_chat_control_base) |
|---|
| 253 | } |
|---|
| 254 | |
|---|
| 255 | """ |
|---|
| 256 | start rendering if clicked on a link |
|---|
| 257 | """ |
|---|
| 258 | @log_calls('LatexPlugin') |
|---|
| 259 | def textview_event_after(self, tag, widget, event, iter): |
|---|
| 260 | if tag.get_property("name") != "latex" or event.type != gtk.gdk.BUTTON_PRESS: |
|---|
| 261 | return |
|---|
| 262 | dollar_start, iter_start = iter.backward_search("$$", gtk.TEXT_SEARCH_TEXT_ONLY) |
|---|
| 263 | iter_end, dollar_end = iter.forward_search("$$", gtk.TEXT_SEARCH_TEXT_ONLY) |
|---|
| 264 | #code = iter_start.get_text(iter_end) |
|---|
| 265 | #widget.get_buffer().delete(dollar_start, dollar_end) |
|---|
| 266 | #pixbuf = widget.render_icon(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON) |
|---|
| 267 | #widget.get_buffer().insert_pixbuf(dollar_start, pixbuf) |
|---|
| 268 | LatexRenderer(dollar_start, dollar_end, widget.get_buffer(), widget) |
|---|
| 269 | |
|---|
| 270 | """ |
|---|
| 271 | called when conversation text widget changes |
|---|
| 272 | """ |
|---|
| 273 | @log_calls('AcronymsExpanderPlugin') |
|---|
| 274 | def textbuffer_live_latex_expander(self, tb): |
|---|
| 275 | def split_list(list): |
|---|
| 276 | newlist = [] |
|---|
| 277 | for i in range(0,len(list)-1,2): |
|---|
| 278 | newlist.append( [ list[i], list[i+1], ] ) |
|---|
| 279 | return newlist |
|---|
| 280 | |
|---|
| 281 | assert isinstance(tb,gtk.TextBuffer) |
|---|
| 282 | start_iter = tb.get_start_iter() |
|---|
| 283 | points = [] |
|---|
| 284 | tuple_found = start_iter.forward_search("$$", gtk.TEXT_SEARCH_TEXT_ONLY) |
|---|
| 285 | while tuple_found != None: |
|---|
| 286 | points.append(tuple_found) |
|---|
| 287 | tuple_found = tuple_found[1].forward_search("$$", gtk.TEXT_SEARCH_TEXT_ONLY) |
|---|
| 288 | |
|---|
| 289 | for pair in split_list(points): |
|---|
| 290 | tb.apply_tag_by_name("latex", pair[0][1], pair[1][0]) |
|---|
| 291 | |
|---|
| 292 | @log_calls('AcronymsExpanderPlugin') |
|---|
| 293 | def connect_with_chat_control_base(self, chat_control): |
|---|
| 294 | d = {} |
|---|
| 295 | tv = chat_control.conv_textview.tv |
|---|
| 296 | tb = tv.get_buffer() |
|---|
| 297 | |
|---|
| 298 | self.latex_tag = gtk.TextTag("latex") |
|---|
| 299 | self.latex_tag.set_property('foreground', "blue") |
|---|
| 300 | self.latex_tag.set_property('underline', "single") |
|---|
| 301 | d['tag_id'] = self.latex_tag.connect('event', self.textview_event_after) |
|---|
| 302 | tb.get_tag_table().add(self.latex_tag) |
|---|
| 303 | |
|---|
| 304 | d['h_id'] = tb.connect('changed', self.textbuffer_live_latex_expander) |
|---|
| 305 | chat_control.latexs_expander_plugin_data = d |
|---|
| 306 | |
|---|
| 307 | return True |
|---|
| 308 | |
|---|
| 309 | @log_calls('AcronymsExpanderPlugin') |
|---|
| 310 | def disconnect_from_chat_control_base(self, chat_control): |
|---|
| 311 | d = chat_control.latexs_expander_plugin_data |
|---|
| 312 | #tv = chat_control.msg_textview |
|---|
| 313 | tv = chat_control.conv_textview.tv |
|---|
| 314 | |
|---|
| 315 | tv.get_buffer().disconnect(d['h_id']) |
|---|
| 316 | self.latex_tag.disconnect(d['tag_id']) |
|---|