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