root/tags/gajim-0.11.4/src/htmltextview.py

Revision 8948, 34.9 kB (checked in by asterix, 10 months ago)

destroy htmltextview tooltip when we destroy a chat control. fixes #3545

Line 
1### Copyright (C) 2005 Gustavo J. A. M. Carneiro
2### Copyright (C) 2006 Santiago Gala
3###
4### This library is free software; you can redistribute it and/or
5### modify it under the terms of the GNU Lesser General Public
6### License as published by the Free Software Foundation; either
7### version 2 of the License, or (at your option) any later version.
8###
9### This library is distributed in the hope that it will be useful,
10### but WITHOUT ANY WARRANTY; without even the implied warranty of
11### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12### Lesser General Public License for more details.
13###
14### You should have received a copy of the GNU Lesser General Public
15### License along with this library; if not, write to the
16### Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17### Boston, MA 02111-1307, USA.
18
19
20"""
21A gtk.TextView-based renderer for XHTML-IM, as described in:
22  http://www.jabber.org/jeps/jep-0071.html
23
24Starting with the version posted by Gustavo Carneiro,
25I (Santiago Gala) am trying to make it more compatible
26with the markup that docutils generate, and also more
27modular.
28
29"""
30
31import gobject
32import pango
33import gtk
34import xml.sax, xml.sax.handler
35import re
36import warnings
37from cStringIO import StringIO
38import socket
39import time
40import urllib2
41import operator
42
43if __name__ == '__main__':
44        from common import i18n
45from common import gajim
46
47import tooltips
48
49
50__all__ = ['HtmlTextView']
51
52whitespace_rx = re.compile("\\s+")
53allwhitespace_rx = re.compile("^\\s*$")
54
55# pixels = points * display_resolution
56display_resolution = 0.3514598*(gtk.gdk.screen_height() /
57                                        float(gtk.gdk.screen_height_mm()))
58
59# embryo of CSS classes
60classes = {
61        #'system-message':';display: none',
62        'problematic':';color: red',
63}
64
65# styles for elements
66element_styles = {
67                'u'                     : ';text-decoration: underline',
68                'em'            : ';font-style: oblique',
69                'cite'          : '; background-color:rgb(170,190,250); font-style: oblique',
70                'li'            : '; margin-left: 1em; margin-right: 10%',
71                'strong'        : ';font-weight: bold',
72                'pre'           : '; background-color:rgb(190,190,190); font-family: monospace; white-space: pre; margin-left: 1em; margin-right: 10%',
73                'kbd'           : ';background-color:rgb(210,210,210);font-family: monospace',
74                'blockquote': '; background-color:rgb(170,190,250); margin-left: 2em; margin-right: 10%',
75                'dt'            : ';font-weight: bold; font-style: oblique',
76                'dd'            : ';margin-left: 2em; font-style: oblique'
77}
78# no difference for the moment
79element_styles['dfn'] = element_styles['em']
80element_styles['var'] = element_styles['em']
81# deprecated, legacy, presentational
82element_styles['tt']  = element_styles['kbd']
83element_styles['i']   = element_styles['em']
84element_styles['b']   = element_styles['strong']
85
86"""
87==========
88  JEP-0071
89==========
90
91This Integration Set includes a subset of the modules defined for
92XHTML 1.0 but does not redefine any existing modules, nor
93does it define any new modules. Specifically, it includes the
94following modules only:
95
96- Structure
97- Text
98 
99  * Block
100   
101    phrasal
102       addr, blockquote, pre
103    Struc
104       div,p
105    Heading
106       h1, h2, h3, h4, h5, h6
107   
108  * Inline
109   
110    phrasal
111       abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
112    structural
113       br, span
114 
115- Hypertext (a)
116- List (ul, ol, dl)
117- Image (img)
118- Style Attribute
119     
120Therefore XHTML-IM uses the following content models:
121
122  Block.mix
123            Block-like elements, e.g., paragraphs
124  Flow.mix
125            Any block or inline elements
126  Inline.mix
127            Character-level elements
128  InlineNoAnchor.class
129                        Anchor element
130  InlinePre.mix
131            Pre element
132
133XHTML-IM also uses the following Attribute Groups:
134
135Core.extra.attrib
136        TBD
137I18n.extra.attrib
138        TBD
139Common.extra
140        style
141
142
143...
144#block level:
145#Heading    h
146#           ( pres           = h1 | h2 | h3 | h4 | h5 | h6 )
147#Block      ( phrasal        = address | blockquote | pre )
148#NOT           ( presentational = hr )
149#           ( structural     = div | p )
150#other:     section
151#Inline     ( phrasal        = abbr | acronym | cite | code | dfn | em | kbd | q | samp | strong | var )
152#NOT        ( presentational =  b  | big | i | small | sub | sup | tt )
153#           ( structural     =  br | span )
154#Param/Legacy    param, font, basefont, center, s, strike, u, dir, menu, isindex
155#
156"""
157
158BLOCK_HEAD = set(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', ))
159BLOCK_PHRASAL = set(( 'address', 'blockquote', 'pre', ))
160BLOCK_PRES = set(( 'hr', )) #not in xhtml-im
161BLOCK_STRUCT = set(( 'div', 'p', ))
162BLOCK_HACKS = set(( 'table', 'tr' )) # at the very least, they will start line ;)
163BLOCK = BLOCK_HEAD.union(BLOCK_PHRASAL).union(BLOCK_STRUCT).union(BLOCK_PRES).union(BLOCK_HACKS)
164
165INLINE_PHRASAL = set('abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var'.split(', '))
166INLINE_PRES = set('b, i, u, tt'.split(', ')) #not in xhtml-im
167INLINE_STRUCT = set('br, span'.split(', '))
168INLINE = INLINE_PHRASAL.union(INLINE_PRES).union(INLINE_STRUCT)
169
170LIST_ELEMS = set( 'dl, ol, ul'.split(', '))
171
172for name in BLOCK_HEAD:
173        num = eval(name[1])
174        size = (num-1) // 2
175        weigth = (num - 1) % 2
176        element_styles[name] = '; font-size: %s; %s' % ( ('large', 'medium', 'small')[size],
177                                                                                                        ('font-weight: bold', 'font-style: oblique')[weigth],
178                                                                                          )
179
180
181def build_patterns(view, config, interface):
182        # extra, rst does not mark _underline_ or /it/ up
183        # actually <b>, <i> or <u> are not in the JEP-0071, but are seen in the wild
184        basic_pattern = r'(?<!\w|\<|/|:)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\w|/|:)|'\
185                                        r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)'
186        view.basic_pattern_re = re.compile(basic_pattern)
187        # emoticons
188        emoticons_pattern = ''
189        try:
190                if config.get('emoticons_theme'):
191                        # When an emoticon is bordered by an alpha-numeric character it is NOT
192                        # expanded.  e.g., foo:) NO, foo :) YES, (brb) NO, (:)) YES, etc.
193                        # We still allow multiple emoticons side-by-side like :P:P:P
194                        # sort keys by length so :qwe emot is checked before :q
195                        keys = interface.emoticons.keys()
196                        keys.sort(interface.on_emoticon_sort)
197                        emoticons_pattern_prematch = ''
198                        emoticons_pattern_postmatch = ''
199                        emoticon_length = 0
200                        for emoticon in keys: # travel thru emoticons list
201                                emoticon_escaped = re.escape(emoticon) # espace regexp metachars
202                                emoticons_pattern += emoticon_escaped + '|'# | means or in regexp
203                                if (emoticon_length != len(emoticon)):
204                                        # Build up expressions to match emoticons next to other emoticons
205                                        emoticons_pattern_prematch  = emoticons_pattern_prematch[:-1]  + ')|(?<='
206                                        emoticons_pattern_postmatch = emoticons_pattern_postmatch[:-1] + ')|(?='
207                                        emoticon_length = len(emoticon)
208                                emoticons_pattern_prematch += emoticon_escaped  + '|'
209                                emoticons_pattern_postmatch += emoticon_escaped + '|'
210                        # We match from our list of emoticons, but they must either have
211                        # whitespace, or another emoticon next to it to match successfully
212                        # [\w.] alphanumeric and dot (for not matching 8) in (2.8))
213                        emoticons_pattern = '|' + \
214                        '(?:(?<![\w.]' + emoticons_pattern_prematch[:-1]   + '))' + \
215                        '(?:'       + emoticons_pattern[:-1]            + ')'  + \
216                        '(?:(?![\w.]'  + emoticons_pattern_postmatch[:-1]  + '))'
217        except:
218                pass
219
220        # because emoticons match later (in the string) they need to be after
221        # basic matches that may occur earlier
222        emot_and_basic_pattern = basic_pattern + emoticons_pattern
223        view.emot_and_basic_re = re.compile(emot_and_basic_pattern, re.IGNORECASE)
224
225
226def _parse_css_color(color):
227        '''_parse_css_color(css_color) -> gtk.gdk.Color'''
228        if color.startswith("rgb(") and color.endswith(')'):
229                r, g, b = [int(c)*257 for c in color[4:-1].split(',')]
230                return gtk.gdk.Color(r, g, b)
231        else:
232                return gtk.gdk.color_parse(color)
233
234def style_iter(style):
235        return (map(lambda x:x.strip(),item.split(':', 1)) for item in style.split(';') if len(item.strip()))
236       
237
238class HtmlHandler(xml.sax.handler.ContentHandler):
239        """A handler to display html to a gtk textview.
240
241        It keeps a stack of "style spans" (start/end element pairs)
242        and a stack of list counters, for nested lists.
243        """
244        def __init__(self, textview, startiter):
245                xml.sax.handler.ContentHandler.__init__(self)
246                self.textbuf = textview.get_buffer()
247                self.textview = textview
248                self.iter = startiter
249                self.text = ''
250                self.starting=True
251                self.preserve = False
252                self.styles = [] # a gtk.TextTag or None, for each span level
253                self.list_counters = [] # stack (top at head) of list
254                                                                # counters, or None for unordered list
255
256        def _parse_style_color(self, tag, value):
257                color = _parse_css_color(value)
258                tag.set_property("foreground-gdk", color)
259
260        def _parse_style_background_color(self, tag, value):
261                color = _parse_css_color(value)
262                tag.set_property("background-gdk", color)
263                if gtk.gtk_version >= (2, 8):
264                        tag.set_property("paragraph-background-gdk", color)
265
266
267        if gtk.gtk_version >= (2, 8, 5) or gobject.pygtk_version >= (2, 8, 1):
268
269                def _get_current_attributes(self):
270                        attrs = self.textview.get_default_attributes()
271                        self.iter.backward_char()
272                        self.iter.get_attributes(attrs)
273                        self.iter.forward_char()
274                        return attrs
275               
276        else:
277               
278                # Workaround http://bugzilla.gnome.org/show_bug.cgi?id=317455
279                def _get_current_style_attr(self, propname, comb_oper=None):
280                        tags = [tag for tag in self.styles if tag is not None]
281                        tags.reverse()
282                        is_set_name = propname + "-set"
283                        value = None
284                        for tag in tags:
285                                if tag.get_property(is_set_name):
286                                        if value is None:
287                                                value = tag.get_property(propname)
288                                                if comb_oper is None:
289                                                        return value
290                                        else:
291                                                value = comb_oper(value, tag.get_property(propname))
292                        return value
293
294                class _FakeAttrs(object):
295                        __slots__ = ("font", "font_scale")
296
297                def _get_current_attributes(self):
298                        attrs = self._FakeAttrs()
299                        attrs.font_scale = self._get_current_style_attr("scale",
300                                                                                                                        operator.mul)
301                        if attrs.font_scale is None:
302                                attrs.font_scale = 1.0
303                        attrs.font = self._get_current_style_attr("font-desc")
304                        if attrs.font is None:
305                                attrs.font = self.textview.style.font_desc
306                        return attrs
307
308
309        def __parse_length_frac_size_allocate(self, textview, allocation,
310                                                                                  frac, callback, args):
311                callback(allocation.width*frac, *args)
312
313        def _parse_length(self, value, font_relative, block_relative, minl, maxl, callback, *args):
314                '''Parse/calc length, converting to pixels, calls callback(length, *args)
315                when the length is first computed or changes'''
316                if value.endswith('%'):
317                        val = float(value[:-1])
318                        sign = cmp(val,0)
319                        # limits: 1% to 500%
320                        val = sign*max(1,min(abs(val),500))
321                        frac = val/100
322                        if font_relative:
323                                attrs = self._get_current_attributes()
324                                font_size = attrs.font.get_size() / pango.SCALE
325                                callback(frac*display_resolution*font_size, *args)
326                        elif block_relative:
327                                # CSS says "Percentage values: refer to width of the closest
328                                #           block-level ancestor"
329                                # This is difficult/impossible to implement, so we use
330                                # textview width instead; a reasonable approximation..
331                                alloc = self.textview.get_allocation()
332                                self.__parse_length_frac_size_allocate(self.textview, alloc,
333                                                                                                           frac, callback, args)
334                                self.textview.connect("size-allocate",
335                                                                          self.__parse_length_frac_size_allocate,
336                                                                          frac, callback, args)
337                        else:
338                                callback(frac, *args)
339                        return
340
341                val = float(value[:-2])
342                sign = cmp(val,0)
343                # validate length
344                val = sign*max(minl,min(abs(val*display_resolution),maxl))
345                if value.endswith('pt'): # points
346                        callback(val*display_resolution, *args)
347
348                elif value.endswith('em'): # ems, the width of the element's font
349                        attrs = self._get_current_attributes()
350                        font_size = attrs.font.get_size() / pango.SCALE
351                        callback(val*display_resolution*font_size, *args)
352
353                elif value.endswith('ex'): # x-height, ~ the height of the letter 'x'
354                        # FIXME: figure out how to calculate this correctly
355                        #        for now 'em' size is used as approximation
356                        attrs = self._get_current_attributes()
357                        font_size = attrs.font.get_size() / pango.SCALE
358                        callback(val*display_resolution*font_size, *args)
359
360                elif value.endswith('px'): # pixels
361                        callback(val, *args)
362
363                else:
364                        try:
365                                # TODO: isn't "no units" interpreted as pixels?
366                                val = int(value)
367                                sign = cmp(val,0)
368                                # validate length
369                                val = sign*max(minl,min(abs(val),maxl))
370                                callback(val, *args)
371                        except:
372                                warnings.warn('Unable to parse length value "%s"' % value)
373
374        def __parse_font_size_cb(length, tag):
375                tag.set_property("size-points", length/display_resolution)
376        __parse_font_size_cb = staticmethod(__parse_font_size_cb)
377
378        def _parse_style_display(self, tag, value):
379                if value == 'none':
380                        tag.set_property('invisible','true')
381                # FIXME: display: block, inline
382
383        def _parse_style_font_size(self, tag, value):
384                try:
385                        scale = {
386                                "xx-small": pango.SCALE_XX_SMALL,
387                                "x-small": pango.SCALE_X_SMALL,
388                                "small": pango.SCALE_SMALL,
389                                "medium": pango.SCALE_MEDIUM,
390                                "large": pango.SCALE_LARGE,
391                                "x-large": pango.SCALE_X_LARGE,
392                                "xx-large": pango.SCALE_XX_LARGE,
393                                } [value]
394                except KeyError:
395                        pass
396                else:
397                        attrs = self._get_current_attributes()
398                        tag.set_property("scale", scale / attrs.font_scale)
399                        return
400                if value == 'smaller':
401                        tag.set_property("scale", pango.SCALE_SMALL)
402                        return
403                if value == 'larger':
404                        tag.set_property("scale", pango.SCALE_LARGE)
405                        return
406                # font relative (5 ~ 4pt, 110 ~ 72pt)
407                self._parse_length(value, True, False, 5, 110, self.__parse_font_size_cb, tag)
408
409        def _parse_style_font_style(self, tag, value):
410                try:
411                        style = {
412                                "normal": pango.STYLE_NORMAL,
413                                "italic": pango.STYLE_ITALIC,
414                                "oblique": pango.STYLE_OBLIQUE,
415                                } [value]
416                except KeyError:
417                        warnings.warn("unknown font-style %s" % value)
418                else:
419                        tag.set_property("style", style)
420
421        def __frac_length_tag_cb(self,length, tag, propname):
422                styles = self._get_style_tags()
423                if styles:
424                        length += styles[-1].get_property(propname)
425                tag.set_property(propname, length)
426        #__frac_length_tag_cb = staticmethod(__frac_length_tag_cb)
427               
428        def _parse_style_margin_left(self, tag, value):
429                # block relative
430                self._parse_length(value, False, True, 1, 1000, self.__frac_length_tag_cb,
431                                                   tag, "left-margin")
432
433        def _parse_style_margin_right(self, tag, value):
434                # block relative
435                self._parse_length(value, False, True, 1, 1000, self.__frac_length_tag_cb,
436                                                   tag, "right-margin")
437
438        def _parse_style_font_weight(self, tag, value):
439                # TODO: missing 'bolder' and 'lighter'
440                try:
441                        weight = {
442                                '100': pango.WEIGHT_ULTRALIGHT,
443                                '200': pango.WEIGHT_ULTRALIGHT,
444                                '300': pango.WEIGHT_LIGHT,
445                                '400': pango.WEIGHT_NORMAL,
446                                '500': pango.WEIGHT_NORMAL,
447                                '600': pango.WEIGHT_BOLD,
448                                '700': pango.WEIGHT_BOLD,
449                                '800': pango.WEIGHT_ULTRABOLD,
450                                '900': pango.WEIGHT_HEAVY,
451                                'normal': pango.WEIGHT_NORMAL,
452                                'bold': pango.WEIGHT_BOLD,
453                                } [value]
454                except KeyError:
455                        warnings.warn("unknown font-style %s" % value)
456                else:
457                        tag.set_property("weight", weight)
458
459        def _parse_style_font_family(self, tag, value):
460                tag.set_property("family", value)
461
462        def _parse_style_text_align(self, tag, value):
463                try:
464                        align = {
465                                'left': gtk.JUSTIFY_LEFT,
466                                'right': gtk.JUSTIFY_RIGHT,
467                                'center': gtk.JUSTIFY_CENTER,
468                                'justify': gtk.JUSTIFY_FILL,
469                                } [value]
470                except KeyError:
471                        warnings.warn("Invalid text-align:%s requested" % value)
472                else:
473                        tag.set_property("justification", align)
474       
475        def _parse_style_text_decoration(self, tag, value):
476                if value == "none":
477                        tag.set_property("underline", pango.UNDERLINE_NONE)
478                        tag.set_property("strikethrough", False)
479                elif value == "underline":
480                        tag.set_property("underline", pango.UNDERLINE_SINGLE)
481                        tag.set_property("strikethrough", False)
482                elif value == "overline":
483                        warnings.warn("text-decoration:overline not implemented")
484                        tag.set_property("underline", pango.UNDERLINE_NONE)
485                        tag.set_property("strikethrough", False)
486                elif value == "line-through":
487                        tag.set_property("underline", pango.UNDERLINE_NONE)
488                        tag.set_property("strikethrough", True)
489                elif value == "blink":
490                        warnings.warn("text-decoration:blink not implemented")
491                else:
492                        warnings.warn("text-decoration:%s not implemented" % value)
493       
494        def _parse_style_white_space(self, tag, value):
495                if value == 'pre':
496                        tag.set_property("wrap_mode", gtk.WRAP_NONE)
497                elif value == 'normal':
498                        tag.set_property("wrap_mode", gtk.WRAP_WORD)
499                elif value == 'nowrap':
500                        tag.set_property("wrap_mode", gtk.WRAP_NONE)
501
502        def __length_tag_cb(self, value, tag, propname):
503                try:
504                        tag.set_property(propname, value)
505                except:
506                        gajim.log.warn( "Error with prop: " + propname + " for tag: " + str(tag))
507               
508
509        def _parse_style_width(self, tag, value):
510                if value == 'auto':
511                        return
512                self._parse_length(value, False, False, 1, 1000, self.__length_tag_cb,
513                                                   tag, "width")
514        def _parse_style_height(self, tag, value):
515                if value == 'auto':
516                        return
517                self._parse_length(value, False, False, 1, 1000, self.__length_tag_cb,
518                                                   tag, "height")
519         
520       
521        # build a dictionary mapping styles to methods, for greater speed
522        __style_methods = dict()
523        for style in ['background-color', 'color', 'font-family', 'font-size',
524                                  'font-style', 'font-weight', 'margin-left', 'margin-right',
525                                  'text-align', 'text-decoration', 'white-space', 'display',
526                                  'width', 'height' ]:
527                try:
528                        method = locals()["_parse_style_%s" % style.replace('-', '_')]
529                except KeyError:
530                        warnings.warn("Style attribute '%s' not yet implemented" % style)
531                else:
532                        __style_methods[style] = method
533        del style
534        # --
535
536        def _get_style_tags(self):
537                return [tag for tag in self.styles if tag is not None]
538
539        def _create_url(self, href, title, type_, id_):
540                '''Process a url tag.
541                '''
542                tag = self.textbuf.create_tag(id_)
543                if href and href[0] != '#':
544                        tag.href = href
545                        tag.type_ = type_ # to be used by the URL handler
546                        tag.connect('event', self.textview.html_hyperlink_handler, 'url', href)
547                        tag.set_property('foreground', '#0000ff')
548                        tag.set_property('underline', pango.UNDERLINE_SINGLE)
549                        tag.is_anchor = True
550                if title:
551                        tag.title = title
552                return tag
553
554        def _process_img(self, attrs):
555                '''Process a img tag.
556                '''
557                try:
558                        # Wait maximum 1s for connection
559                        socket.setdefaulttimeout(1)
560                        try:
561                                f = urllib2.urlopen(attrs['src'])
562                        except Exception, ex:
563                                gajim.log.debug(str('Error loading image %s ' % attrs['src'] + ex))
564                                pixbuf = None
565                                alt = attrs.get('alt', 'Broken image')
566                        else:
567                                # Wait 0.1s between each byte
568                                try:
569                                        f.fp._sock.fp._sock.settimeout(0.5)
570                                except:
571                                        pass 
572                        # Max image size = 2 MB (to try to prevent DoS)
573                        mem = ''
574                        deadline = time.time() + 3
575                        while True:
576                                if time.time() > deadline:
577                                        gajim.log.debug(str('Timeout loading image %s ' % \
578                                                attrs['src'] + ex))
579                                        mem = ''
580                                        alt = attrs.get('alt', '')
581                                        if alt:
582                                                alt += '\n'
583                                        alt += _('Timeout loading image')
584                                        break
585                                try:
586                                        temp = f.read(100)
587                                except socket.timeout, ex:
588                                        gajim.log.debug('Timeout loading image %s ' % attrs['src'] + \
589                                                str(ex))
590                                        mem = ''
591                                        alt = attrs.get('alt', '')
592                                        if alt:
593                                                alt += '\n'
594                                        alt += _('Timeout loading image')
595                                        break
596                                if temp:
597                                        mem += temp
598                                else:
599                                        break
600                                if len(mem) > 2*1024*1024:
601                                        alt = attrs.get('alt', '')
602                                        if alt:
603                                                alt += '\n'
604                                        alt += _('Image is too big')
605                                        break
606                        pixbuf = None
607                        if mem:
608                                # Caveat: GdkPixbuf is known not to be safe to load
609                                # images from network... this program is now potentially
610                                # hackable ;)
611                                loader = gtk.gdk.PixbufLoader()
612                                dims = [0,0]
613                                def height_cb(length):
614                                        dims[1] = length
615                                def width_cb(length):
616                                        dims[0] = length
617                                # process width and height attributes
618                                w = attrs.get('width')
619                                h = attrs.get('height')
620                                # override with width and height styles
621                                for attr, val in style_iter(attrs.get('style', '')):
622                                        if attr == 'width':
623                                                w = val
624                                        elif attr == 'height':
625                                                h = val
626                                if w:
627                                        self._parse_length(w, False, False, 1, 1000, width_cb)
628                                if h:
629                                        self._parse_length(h, False, False, 1, 1000, height_cb)
630                                def set_size(pixbuf, w, h, dims):
631                                        '''FIXME: floats should be relative to the whole
632                                        textview, and resize with it. This needs new
633                                        pifbufs for every resize, gtk.gdk.Pixbuf.scale_simple
634                                        or similar.
635                                        '''
636                                        if type(dims[0]) == float:
637                                                dims[0] = int(dims[0]*w)
638                                        elif not dims[0]:
639                                                dims[0] = w
640                                        if type(dims[1]) == float:
641                                                dims[1] = int(dims[1]*h)
642                                        if not dims[1]:
643                                                dims[1] = h
644                                        loader.set_size(*dims)
645                                if w or h:
646                                        loader.connect('size-prepared', set_size, dims)
647                                loader.write(mem)
648                                loader.close()
649                                pixbuf = loader.get_pixbuf()
650                                alt = attrs.get('alt', '')
651                        if pixbuf is not None:
652                                tags = self._get_style_tags()
653                                if tags:
654                                        tmpmark = self.textbuf.create_mark(None, self.iter, True)
655                                self.textbuf.insert_pixbuf(self.iter, pixbuf)
656                                self.starting = False
657                                if tags:
658                                        start = self.textbuf.get_iter_at_mark(tmpmark)
659                                        for tag in tags:
660                                                self.textbuf.apply_tag(tag, start, self.iter)
661                                        self.textbuf.delete_mark(tmpmark)
662                        else:
663                                self._insert_text('[IMG: %s]' % alt)
664                except Exception, ex:
665                        gajim.log.error('Error loading image ' + str(ex))
666                        pixbuf = None
667                        alt = attrs.get('alt', 'Broken image')
668                        try:
669                                loader.close()
670                        except:
671                                pass
672                return pixbuf
673
674        def _begin_span(self, style, tag=None, id_=None):
675                if style is None:
676                        self.styles.append(tag)
677                        return None
678                if tag is None:
679                        if id_:
680                                tag = self.textbuf.create_tag(id_)
681                        else:
682                                tag = self.textbuf.create_tag() # we create anonymous tag
683                for attr, val in style_iter(style):
684                        attr = attr.lower()
685                        val = val
686                        try:
687                                method = self.__style_methods[attr]
688                        except KeyError:
689                                warnings.warn("Style attribute '%s' requested "
690                                                          "but not yet implemented" % attr)
691                        else:
692                                method(self, tag, val)
693                self.styles.append(tag)
694
695        def _end_span(self):
696                self.styles.pop()
697
698        def _jump_line(self):
699                self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol')
700                self.starting = True
701
702        def _insert_text(self, text):
703                if self.starting and text != '\n':
704                        self.starting = (text[-1] == '\n')
705                tags = self._get_style_tags()
706                if tags:
707                        self.textbuf.insert_with_tags(self.iter, text, *tags)
708                else:
709                        self.textbuf.insert(self.iter, text)
710
711        def _starts_line(self):
712                return self.starting or self.iter.starts_line()
713               
714        def _flush_text(self):
715                if not self.text: return
716                text, self.text = self.text, ''
717                if not self.preserve:
718                        text = text.replace('\n', ' ')
719                        self.handle_specials(whitespace_rx.sub(' ', text))
720                else:
721                        self._insert_text(text.strip("\n"))
722
723        def _anchor_event(self, tag, textview, event, iter, href, type_):
724                if event.type == gtk.gdk.BUTTON_PRESS:
725                        self.textview.emit("url-clicked", href, type_)
726                        return True
727                return False
728
729        def handle_specials(self, text):
730                index = 0
731                se = self.textview.config.get('show_ascii_formatting_chars')
732                if self.textview.config.get('emoticons_theme'):
733                        iterator = self.textview.emot_and_basic_re.finditer(text)
734                else:
735                        iterator = self.textview.basic_pattern_re.finditer(text)
736                for match in iterator:
737                        start, end = match.span()
738                        special_text = text[start:end]
739                        if start != 0:
740                                self._insert_text(text[index:start])
741                        index = end # update index
742                        #emoticons
743                        possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
744                        if self.textview.config.get('emoticons_theme') and \
745                                        possible_emot_ascii_caps in self.textview.interface.emoticons.keys():
746                                #it's an emoticon
747                                emot_ascii = possible_emot_ascii_caps
748                                anchor = self.textbuf.create_child_anchor(self.iter)
749                                img = gtk.Image()
750                                img.set_from_file(self.textview.interface.emoticons[emot_ascii])
751                                img.show()
752                                # TODO: add alt/tooltip with the special_text (a11y)
753                                self.textview.add_child_at_anchor(img, anchor)
754                        else:
755                                # now print it
756                                if special_text.startswith('/'): # it's explicit italics
757                                        self.startElement('i', {})
758                                elif special_text.startswith('_'): # it's explicit underline
759                                        self.startElement("u", {})
760                                if se: self._insert_text(special_text[0])
761                                self.handle_specials(special_text[1:-1])
762                                if se: self._insert_text(special_text[0])
763                                if special_text.startswith('_'): # it's explicit underline
764                                        self.endElement('u')
765                                if special_text.startswith('/'): # it's explicit italics