| 1 | |
|---|
| 2 | |
|---|
| 3 | |
|---|
| 4 | |
|---|
| 5 | |
|---|
| 6 | |
|---|
| 7 | |
|---|
| 8 | |
|---|
| 9 | |
|---|
| 10 | |
|---|
| 11 | |
|---|
| 12 | |
|---|
| 13 | |
|---|
| 14 | |
|---|
| 15 | |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | |
|---|
| 19 | |
|---|
| 20 | """ |
|---|
| 21 | A gtk.TextView-based renderer for XHTML-IM, as described in: |
|---|
| 22 | http://www.jabber.org/jeps/jep-0071.html |
|---|
| 23 | |
|---|
| 24 | Starting with the version posted by Gustavo Carneiro, |
|---|
| 25 | I (Santiago Gala) am trying to make it more compatible |
|---|
| 26 | with the markup that docutils generate, and also more |
|---|
| 27 | modular. |
|---|
| 28 | |
|---|
| 29 | """ |
|---|
| 30 | |
|---|
| 31 | import gobject |
|---|
| 32 | import pango |
|---|
| 33 | import gtk |
|---|
| 34 | import xml.sax, xml.sax.handler |
|---|
| 35 | import re |
|---|
| 36 | import warnings |
|---|
| 37 | from cStringIO import StringIO |
|---|
| 38 | import socket |
|---|
| 39 | import time |
|---|
| 40 | import urllib2 |
|---|
| 41 | import operator |
|---|
| 42 | |
|---|
| 43 | if __name__ == '__main__': |
|---|
| 44 | from common import i18n |
|---|
| 45 | from common import gajim |
|---|
| 46 | |
|---|
| 47 | import tooltips |
|---|
| 48 | |
|---|
| 49 | |
|---|
| 50 | __all__ = ['HtmlTextView'] |
|---|
| 51 | |
|---|
| 52 | whitespace_rx = re.compile("\\s+") |
|---|
| 53 | allwhitespace_rx = re.compile("^\\s*$") |
|---|
| 54 | |
|---|
| 55 | |
|---|
| 56 | display_resolution = 0.3514598*(gtk.gdk.screen_height() / |
|---|
| 57 | float(gtk.gdk.screen_height_mm())) |
|---|
| 58 | |
|---|
| 59 | |
|---|
| 60 | classes = { |
|---|
| 61 | |
|---|
| 62 | 'problematic':';color: red', |
|---|
| 63 | } |
|---|
| 64 | |
|---|
| 65 | |
|---|
| 66 | element_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 | |
|---|
| 79 | element_styles['dfn'] = element_styles['em'] |
|---|
| 80 | element_styles['var'] = element_styles['em'] |
|---|
| 81 | |
|---|
| 82 | element_styles['tt'] = element_styles['kbd'] |
|---|
| 83 | element_styles['i'] = element_styles['em'] |
|---|
| 84 | element_styles['b'] = element_styles['strong'] |
|---|
| 85 | |
|---|
| 86 | """ |
|---|
| 87 | ========== |
|---|
| 88 | JEP-0071 |
|---|
| 89 | ========== |
|---|
| 90 | |
|---|
| 91 | This Integration Set includes a subset of the modules defined for |
|---|
| 92 | XHTML 1.0 but does not redefine any existing modules, nor |
|---|
| 93 | does it define any new modules. Specifically, it includes the |
|---|
| 94 | following 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 | |
|---|
| 120 | Therefore 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 | |
|---|
| 133 | XHTML-IM also uses the following Attribute Groups: |
|---|
| 134 | |
|---|
| 135 | Core.extra.attrib |
|---|
| 136 | TBD |
|---|
| 137 | I18n.extra.attrib |
|---|
| 138 | TBD |
|---|
| 139 | Common.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 | |
|---|
| 158 | BLOCK_HEAD = set(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', )) |
|---|
| 159 | BLOCK_PHRASAL = set(( 'address', 'blockquote', 'pre', )) |
|---|
| 160 | BLOCK_PRES = set(( 'hr', )) |
|---|
| 161 | BLOCK_STRUCT = set(( 'div', 'p', )) |
|---|
| 162 | BLOCK_HACKS = set(( 'table', 'tr' )) |
|---|
| 163 | BLOCK = BLOCK_HEAD.union(BLOCK_PHRASAL).union(BLOCK_STRUCT).union(BLOCK_PRES).union(BLOCK_HACKS) |
|---|
| 164 | |
|---|
| 165 | INLINE_PHRASAL = set('abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var'.split(', ')) |
|---|
| 166 | INLINE_PRES = set('b, i, u, tt'.split(', ')) |
|---|
| 167 | INLINE_STRUCT = set('br, span'.split(', ')) |
|---|
| 168 | INLINE = INLINE_PHRASAL.union(INLINE_PRES).union(INLINE_STRUCT) |
|---|
| 169 | |
|---|
| 170 | LIST_ELEMS = set( 'dl, ol, ul'.split(', ')) |
|---|
| 171 | |
|---|
| 172 | for 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 | |
|---|
| 181 | def build_patterns(view, config, interface): |
|---|
| 182 | |
|---|
| 183 | |
|---|
| 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 | |
|---|
| 188 | emoticons_pattern = '' |
|---|
| 189 | try: |
|---|
| 190 | if config.get('emoticons_theme'): |
|---|
| 191 | |
|---|
| 192 | |
|---|
| 193 | |
|---|
| 194 | |
|---|
| 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: |
|---|
| 201 | emoticon_escaped = re.escape(emoticon) |
|---|
| 202 | emoticons_pattern += emoticon_escaped + '|' |
|---|
| 203 | if (emoticon_length != len(emoticon)): |
|---|
| 204 | |
|---|
| 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 | |
|---|
| 211 | |
|---|
| 212 | |
|---|
| 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 | |
|---|
| 221 | |
|---|
| 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 | |
|---|
| 226 | def _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 | |
|---|
| 234 | def style_iter(style): |
|---|
| 235 | return (map(lambda x:x.strip(),item.split(':', 1)) for item in style.split(';') if len(item.strip())) |
|---|
| 236 | |
|---|
| 237 | |
|---|
| 238 | class 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 = [] |
|---|
| 253 | self.list_counters = [] |
|---|
| 254 | |
|---|
| 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 | |
|---|
| 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 | |
|---|
| 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 | |
|---|
| 328 | |
|---|
| 329 | |
|---|
| 330 | |
|---|
| 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 | |
|---|
| 344 | val = sign*max(minl,min(abs(val*display_resolution),maxl)) |
|---|
| 345 | if value.endswith('pt'): |
|---|
| 346 | callback(val*display_resolution, *args) |
|---|
| 347 | |
|---|
| 348 | elif value.endswith('em'): |
|---|
| 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'): |
|---|
| 354 | |
|---|
| 355 | |
|---|
| 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'): |
|---|
| 361 | callback(val, *args) |
|---|
| 362 | |
|---|
| 363 | else: |
|---|
| 364 | try: |
|---|
| 365 | |
|---|
| 366 | val = int(value) |
|---|
| 367 | sign = cmp(val,0) |
|---|
| 368 | |
|---|
| 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 | |
|---|
| 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 | |
|---|
| 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 | |
|---|
| 427 | |
|---|
| 428 | def _parse_style_margin_left(self, tag, value): |
|---|
| 429 | |
|---|
| 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 | |
|---|
| 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 | |
|---|
| 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 | |
|---|
| 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_ |
|---|
| 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 | |
|---|
| 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 | |
|---|
| 568 | try: |
|---|
| 569 | f.fp._sock.fp._sock.settimeout(0.5) |
|---|
| 570 | except: |
|---|
| 571 | pass |
|---|
| 572 | |
|---|
| 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 | |
|---|
| 609 | |
|---|
| 610 | |
|---|
| 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 | |
|---|
| 618 | w = attrs.get('width') |
|---|
| 619 | h = attrs.get('height') |
|---|
| 620 | |
|---|
| 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() |
|---|
| 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 |
|---|
| 742 | |
|---|
| 743 | possible_emot_ascii_caps = special_text.upper() |
|---|
| 744 | if self.textview.config.get('emoticons_theme') and \ |
|---|
| 745 | possible_emot_ascii_caps in self.textview.interface.emoticons.keys(): |
|---|
| 746 | |
|---|
| 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 | |
|---|
| 753 | self.textview.add_child_at_anchor(img, anchor) |
|---|
| 754 | else: |
|---|
| 755 | |
|---|
| 756 | if special_text.startswith('/'): |
|---|
| 757 | self.startElement('i', {}) |
|---|
| 758 | elif special_text.startswith('_'): |
|---|
| 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('_'): |
|---|
| 764 | self.endElement('u') |
|---|
| 765 | if special_text.startswith('/'): |
|---|
| 766 | |
|---|