| 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 | """ |
|---|
| 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 | from common import gajim |
|---|
| 44 | #from common import i18n |
|---|
| 45 | |
|---|
| 46 | |
|---|
| 47 | import tooltips |
|---|
| 48 | |
|---|
| 49 | |
|---|
| 50 | __all__ = ['HtmlTextView'] |
|---|
| 51 | |
|---|
| 52 | whitespace_rx = re.compile("\\s+") |
|---|
| 53 | allwhitespace_rx = re.compile("^\\s*$") |
|---|
| 54 | |
|---|
| 55 | # pixels = points * display_resolution |
|---|
| 56 | display_resolution = 0.3514598*(gtk.gdk.screen_height() / |
|---|
| 57 | float(gtk.gdk.screen_height_mm())) |
|---|
| 58 | |
|---|
| 59 | #embryo of CSS classes |
|---|
| 60 | classes = { |
|---|
| 61 | #'system-message':';display: none', |
|---|
| 62 | 'problematic':';color: red', |
|---|
| 63 | } |
|---|
| 64 | |
|---|
| 65 | #styles for elemens |
|---|
| 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 | # no difference for the moment |
|---|
| 79 | element_styles['dfn'] = element_styles['em'] |
|---|
| 80 | element_styles['var'] = element_styles['em'] |
|---|
| 81 | # deprecated, legacy, presentational |
|---|
| 82 | element_styles['tt'] = element_styles['kbd'] |
|---|
| 83 | element_styles['i'] = element_styles['em'] |
|---|
| 84 | element_styles['b'] = element_styles['strong'] |
|---|
| 85 | |
|---|
| 86 | class_styles = { |
|---|
| 87 | } |
|---|
| 88 | |
|---|
| 89 | """ |
|---|
| 90 | ========== |
|---|
| 91 | JEP-0071 |
|---|
| 92 | ========== |
|---|
| 93 | |
|---|
| 94 | This Integration Set includes a subset of the modules defined for |
|---|
| 95 | XHTML 1.0 but does not redefine any existing modules, nor |
|---|
| 96 | does it define any new modules. Specifically, it includes the |
|---|
| 97 | following modules only: |
|---|
| 98 | |
|---|
| 99 | - Structure |
|---|
| 100 | - Text |
|---|
| 101 | |
|---|
| 102 | * Block |
|---|
| 103 | |
|---|
| 104 | phrasal |
|---|
| 105 | addr, blockquote, pre |
|---|
| 106 | Struc |
|---|
| 107 | div,p |
|---|
| 108 | Heading |
|---|
| 109 | h1, h2, h3, h4, h5, h6 |
|---|
| 110 | |
|---|
| 111 | * Inline |
|---|
| 112 | |
|---|
| 113 | phrasal |
|---|
| 114 | abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var |
|---|
| 115 | structural |
|---|
| 116 | br, span |
|---|
| 117 | |
|---|
| 118 | - Hypertext (a) |
|---|
| 119 | - List (ul, ol, dl) |
|---|
| 120 | - Image (img) |
|---|
| 121 | - Style Attribute |
|---|
| 122 | |
|---|
| 123 | Therefore XHTML-IM uses the following content models: |
|---|
| 124 | |
|---|
| 125 | Block.mix |
|---|
| 126 | Block-like elements, e.g., paragraphs |
|---|
| 127 | Flow.mix |
|---|
| 128 | Any block or inline elements |
|---|
| 129 | Inline.mix |
|---|
| 130 | Character-level elements |
|---|
| 131 | InlineNoAnchor.class |
|---|
| 132 | Anchor element |
|---|
| 133 | InlinePre.mix |
|---|
| 134 | Pre element |
|---|
| 135 | |
|---|
| 136 | XHTML-IM also uses the following Attribute Groups: |
|---|
| 137 | |
|---|
| 138 | Core.extra.attrib |
|---|
| 139 | TBD |
|---|
| 140 | I18n.extra.attrib |
|---|
| 141 | TBD |
|---|
| 142 | Common.extra |
|---|
| 143 | style |
|---|
| 144 | |
|---|
| 145 | |
|---|
| 146 | ... |
|---|
| 147 | #block level: |
|---|
| 148 | #Heading h |
|---|
| 149 | # ( pres = h1 | h2 | h3 | h4 | h5 | h6 ) |
|---|
| 150 | #Block ( phrasal = address | blockquote | pre ) |
|---|
| 151 | #NOT ( presentational = hr ) |
|---|
| 152 | # ( structural = div | p ) |
|---|
| 153 | #other: section |
|---|
| 154 | #Inline ( phrasal = abbr | acronym | cite | code | dfn | em | kbd | q | samp | strong | var ) |
|---|
| 155 | #NOT ( presentational = b | big | i | small | sub | sup | tt ) |
|---|
| 156 | # ( structural = br | span ) |
|---|
| 157 | #Param/Legacy param, font, basefont, center, s, strike, u, dir, menu, isindex |
|---|
| 158 | # |
|---|
| 159 | """ |
|---|
| 160 | |
|---|
| 161 | BLOCK_HEAD = set(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', )) |
|---|
| 162 | BLOCK_PHRASAL = set(( 'address', 'blockquote', 'pre', )) |
|---|
| 163 | BLOCK_PRES = set(( 'hr', )) #not in xhtml-im |
|---|
| 164 | BLOCK_STRUCT = set(( 'div', 'p', )) |
|---|
| 165 | BLOCK_HACKS = set(( 'table', 'tr' )) # at the very least, they will start line ;) |
|---|
| 166 | BLOCK = BLOCK_HEAD.union(BLOCK_PHRASAL).union(BLOCK_STRUCT).union(BLOCK_PRES).union(BLOCK_HACKS) |
|---|
| 167 | |
|---|
| 168 | INLINE_PHRASAL = set('abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var'.split(', ')) |
|---|
| 169 | INLINE_PRES = set('b, i, u, tt'.split(', ')) #not in xhtml-im |
|---|
| 170 | INLINE_STRUCT = set('br, span'.split(', ')) |
|---|
| 171 | INLINE = INLINE_PHRASAL.union(INLINE_PRES).union(INLINE_STRUCT) |
|---|
| 172 | |
|---|
| 173 | LIST_ELEMS = set( 'dl, ol, ul'.split(', ')) |
|---|
| 174 | |
|---|
| 175 | for name in BLOCK_HEAD: |
|---|
| 176 | num = eval(name[1]) |
|---|
| 177 | size = (num-1) // 2 |
|---|
| 178 | weigth = (num - 1) % 2 |
|---|
| 179 | element_styles[name] = '; font-size: %s; %s' % ( ('large', 'medium', 'small')[size], |
|---|
| 180 | ('font-weight: bold', 'font-style: oblique')[weigth], |
|---|
| 181 | ) |
|---|
| 182 | |
|---|
| 183 | |
|---|
| 184 | def build_patterns(view, config, interface): |
|---|
| 185 | #extra, rst does not mark _underline_ or /it/ up |
|---|
| 186 | #actually <b>, <i> or <u> are not in the JEP-0071, but are seen in the wild |
|---|
| 187 | basic_pattern = r'(?<!\w|\<|/|:)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\w|/|:)|'\ |
|---|
| 188 | r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)' |
|---|
| 189 | view.basic_pattern_re = re.compile(basic_pattern) |
|---|
| 190 | #TODO: emoticons |
|---|
| 191 | emoticons_pattern = '' |
|---|
| 192 | if config.get('emoticons_theme'): |
|---|
| 193 | # When an emoticon is bordered by an alpha-numeric character it is NOT |
|---|
| 194 | # expanded. e.g., foo:) NO, foo :) YES, (brb) NO, (:)) YES, etc. |
|---|
| 195 | # We still allow multiple emoticons side-by-side like :P:P:P |
|---|
| 196 | # sort keys by length so :qwe emot is checked before :q |
|---|
| 197 | keys = interface.emoticons.keys() |
|---|
| 198 | keys.sort(interface.on_emoticon_sort) |
|---|
| 199 | emoticons_pattern_prematch = '' |
|---|
| 200 | emoticons_pattern_postmatch = '' |
|---|
| 201 | emoticon_length = 0 |
|---|
| 202 | for emoticon in keys: # travel thru emoticons list |
|---|
| 203 | emoticon_escaped = re.escape(emoticon) # espace regexp metachars |
|---|
| 204 | emoticons_pattern += emoticon_escaped + '|'# | means or in regexp |
|---|
| 205 | if (emoticon_length != len(emoticon)): |
|---|
| 206 | # Build up expressions to match emoticons next to other emoticons |
|---|
| 207 | emoticons_pattern_prematch = emoticons_pattern_prematch[:-1] + ')|(?<=' |
|---|
| 208 | emoticons_pattern_postmatch = emoticons_pattern_postmatch[:-1] + ')|(?=' |
|---|
| 209 | emoticon_length = len(emoticon) |
|---|
| 210 | emoticons_pattern_prematch += emoticon_escaped + '|' |
|---|
| 211 | emoticons_pattern_postmatch += emoticon_escaped + '|' |
|---|
| 212 | # We match from our list of emoticons, but they must either have |
|---|
| 213 | # whitespace, or another emoticon next to it to match successfully |
|---|
| 214 | # [\w.] alphanumeric and dot (for not matching 8) in (2.8)) |
|---|
| 215 | emoticons_pattern = '|' + \ |
|---|
| 216 | '(?:(?<![\w.]' + emoticons_pattern_prematch[:-1] + '))' + \ |
|---|
| 217 | '(?:' + emoticons_pattern[:-1] + ')' + \ |
|---|
| 218 | '(?:(?![\w.]' + emoticons_pattern_postmatch[:-1] + '))' |
|---|
| 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 | |
|---|
| 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 | |
|---|
| 235 | class HtmlHandler(xml.sax.handler.ContentHandler): |
|---|
| 236 | |
|---|
| 237 | def __init__(self, textview, startiter): |
|---|
| 238 | xml.sax.handler.ContentHandler.__init__(self) |
|---|
| 239 | self.textbuf = textview.get_buffer() |
|---|
| 240 | self.textview = textview |
|---|
| 241 | self.iter = startiter |
|---|
| 242 | self.text = '' |
|---|
| 243 | self.starting=True |
|---|
| 244 | self.preserve = False |
|---|
| 245 | self.styles = [] # a gtk.TextTag or None, for each span level |
|---|
| 246 | self.list_counters = [] # stack (top at head) of list |
|---|
| 247 | # counters, or None for unordered list |
|---|
| 248 | |
|---|
| 249 | def _parse_style_color(self, tag, value): |
|---|
| 250 | color = _parse_css_color(value) |
|---|
| 251 | tag.set_property("foreground-gdk", color) |
|---|
| 252 | |
|---|
| 253 | def _parse_style_background_color(self, tag, value): |
|---|
| 254 | color = _parse_css_color(value) |
|---|
| 255 | tag.set_property("background-gdk", color) |
|---|
| 256 | if gtk.gtk_version >= (2, 8): |
|---|
| 257 | tag.set_property("paragraph-background-gdk", color) |
|---|
| 258 | |
|---|
| 259 | |
|---|
| 260 | if gtk.gtk_version >= (2, 8, 5) or gobject.pygtk_version >= (2, 8, 1): |
|---|
| 261 | |
|---|
| 262 | def _get_current_attributes(self): |
|---|
| 263 | attrs = self.textview.get_default_attributes() |
|---|
| 264 | self.iter.backward_char() |
|---|
| 265 | self.iter.get_attributes(attrs) |
|---|
| 266 | self.iter.forward_char() |
|---|
| 267 | return attrs |
|---|
| 268 | |
|---|
| 269 | else: |
|---|
| 270 | |
|---|
| 271 | # Workaround http://bugzilla.gnome.org/show_bug.cgi?id=317455 |
|---|
| 272 | def _get_current_style_attr(self, propname, comb_oper=None): |
|---|
| 273 | tags = [tag for tag in self.styles if tag is not None] |
|---|
| 274 | tags.reverse() |
|---|
| 275 | is_set_name = propname + "-set" |
|---|
| 276 | value = None |
|---|
| 277 | for tag in tags: |
|---|
| 278 | if tag.get_property(is_set_name): |
|---|
| 279 | if value is None: |
|---|
| 280 | value = tag.get_property(propname) |
|---|
| 281 | if comb_oper is None: |
|---|
| 282 | return value |
|---|
| 283 | else: |
|---|
| 284 | value = comb_oper(value, tag.get_property(propname)) |
|---|
| 285 | return value |
|---|
| 286 | |
|---|
| 287 | class _FakeAttrs(object): |
|---|
| 288 | __slots__ = ("font", "font_scale") |
|---|
| 289 | |
|---|
| 290 | def _get_current_attributes(self): |
|---|
| 291 | attrs = self._FakeAttrs() |
|---|
| 292 | attrs.font_scale = self._get_current_style_attr("scale", |
|---|
| 293 | operator.mul) |
|---|
| 294 | if attrs.font_scale is None: |
|---|
| 295 | attrs.font_scale = 1.0 |
|---|
| 296 | attrs.font = self._get_current_style_attr("font-desc") |
|---|
| 297 | if attrs.font is None: |
|---|
| 298 | attrs.font = self.textview.style.font_desc |
|---|
| 299 | return attrs |
|---|
| 300 | |
|---|
| 301 | |
|---|
| 302 | def __parse_length_frac_size_allocate(self, textview, allocation, |
|---|
| 303 | frac, callback, args): |
|---|
| 304 | callback(allocation.width*frac, *args) |
|---|
| 305 | |
|---|
| 306 | def _parse_length(self, value, font_relative, callback, *args): |
|---|
| 307 | '''Parse/calc length, converting to pixels, calls callback(length, *args) |
|---|
| 308 | when the length is first computed or changes''' |
|---|
| 309 | if value.endswith('%'): |
|---|
| 310 | frac = float(value[:-1])/100 |
|---|
| 311 | if font_relative: |
|---|
| 312 | attrs = self._get_current_attributes() |
|---|
| 313 | font_size = attrs.font.get_size() / pango.SCALE |
|---|
| 314 | callback(frac*display_resolution*font_size, *args) |
|---|
| 315 | else: |
|---|
| 316 | # CSS says "Percentage values: refer to width of the closest |
|---|
| 317 | # block-level ancestor" |
|---|
| 318 | # This is difficult/impossible to implement, so we use |
|---|
| 319 | # textview width instead; a reasonable approximation.. |
|---|
| 320 | alloc = self.textview.get_allocation() |
|---|
| 321 | self.__parse_length_frac_size_allocate(self.textview, alloc, |
|---|
| 322 | frac, callback, args) |
|---|
| 323 | self.textview.connect("size-allocate", |
|---|
| 324 | self.__parse_length_frac_size_allocate, |
|---|
| 325 | frac, callback, args) |
|---|
| 326 | |
|---|
| 327 | elif value.endswith('pt'): # points |
|---|
| 328 | callback(float(value[:-2])*display_resolution, *args) |
|---|
| 329 | |
|---|
| 330 | elif value.endswith('em'): # ems, the height of the element's font |
|---|
| 331 | attrs = self._get_current_attributes() |
|---|
| 332 | font_size = attrs.font.get_size() / pango.SCALE |
|---|
| 333 | callback(float(value[:-2])*display_resolution*font_size, *args) |
|---|
| 334 | |
|---|
| 335 | elif value.endswith('ex'): # x-height, ~ the height of the letter 'x' |
|---|
| 336 | # FIXME: figure out how to calculate this correctly |
|---|
| 337 | # for now 'em' size is used as approximation |
|---|
| 338 | attrs = self._get_current_attributes() |
|---|
| 339 | font_size = attrs.font.get_size() / pango.SCALE |
|---|
| 340 | callback(float(value[:-2])*display_resolution*font_size, *args) |
|---|
| 341 | |
|---|
| 342 | elif value.endswith('px'): # pixels |
|---|
| 343 | callback(int(value[:-2]), *args) |
|---|
| 344 | |
|---|
| 345 | else: |
|---|
| 346 | warnings.warn("Unable to parse length value '%s'" % value) |
|---|
| 347 | |
|---|
| 348 | def __parse_font_size_cb(length, tag): |
|---|
| 349 | tag.set_property("size-points", length/display_resolution) |
|---|
| 350 | __parse_font_size_cb = staticmethod(__parse_font_size_cb) |
|---|
| 351 | |
|---|
| 352 | def _parse_style_display(self, tag, value): |
|---|
| 353 | if value == 'none': |
|---|
| 354 | tag.set_property('invisible','true') |
|---|
| 355 | #Fixme: display: block, inline |
|---|
| 356 | |
|---|
| 357 | def _parse_style_font_size(self, tag, value): |
|---|
| 358 | try: |
|---|
| 359 | scale = { |
|---|
| 360 | "xx-small": pango.SCALE_XX_SMALL, |
|---|
| 361 | "x-small": pango.SCALE_X_SMALL, |
|---|
| 362 | "small": pango.SCALE_SMALL, |
|---|
| 363 | "medium": pango.SCALE_MEDIUM, |
|---|
| 364 | "large": pango.SCALE_LARGE, |
|---|
| 365 | "x-large": pango.SCALE_X_LARGE, |
|---|
| 366 | "xx-large": pango.SCALE_XX_LARGE, |
|---|
| 367 | } [value] |
|---|
| 368 | except KeyError: |
|---|
| 369 | pass |
|---|
| 370 | else: |
|---|
| 371 | attrs = self._get_current_attributes() |
|---|
| 372 | tag.set_property("scale", scale / attrs.font_scale) |
|---|
| 373 | return |
|---|
| 374 | if value == 'smaller': |
|---|
| 375 | tag.set_property("scale", pango.SCALE_SMALL) |
|---|
| 376 | return |
|---|
| 377 | if value == 'larger': |
|---|
| 378 | tag.set_property("scale", pango.SCALE_LARGE) |
|---|
| 379 | return |
|---|
| 380 | self._parse_length(value, True, self.__parse_font_size_cb, tag) |
|---|
| 381 | |
|---|
| 382 | def _parse_style_font_style(self, tag, value): |
|---|
| 383 | try: |
|---|
| 384 | style = { |
|---|
| 385 | "normal": pango.STYLE_NORMAL, |
|---|
| 386 | "italic": pango.STYLE_ITALIC, |
|---|
| 387 | "oblique": pango.STYLE_OBLIQUE, |
|---|
| 388 | } [value] |
|---|
| 389 | except KeyError: |
|---|
| 390 | warnings.warn("unknown font-style %s" % value) |
|---|
| 391 | else: |
|---|
| 392 | tag.set_property("style", style) |
|---|
| 393 | |
|---|
| 394 | def __frac_length_tag_cb(self,length, tag, propname): |
|---|
| 395 | styles = self._get_style_tags() |
|---|
| 396 | if styles: |
|---|
| 397 | length += styles[-1].get_property(propname) |
|---|
| 398 | tag.set_property(propname, length) |
|---|
| 399 | #__frac_length_tag_cb = staticmethod(__frac_length_tag_cb) |
|---|
| 400 | |
|---|
| 401 | def _parse_style_margin_left(self, tag, value): |
|---|
| 402 | self._parse_length(value, False, self.__frac_length_tag_cb, |
|---|
| 403 | tag, "left-margin") |
|---|
| 404 | |
|---|
| 405 | def _parse_style_margin_right(self, tag, value): |
|---|
| 406 | self._parse_length(value, False, self.__frac_length_tag_cb, |
|---|
| 407 | tag, "right-margin") |
|---|
| 408 | |
|---|
| 409 | def _parse_style_font_weight(self, tag, value): |
|---|
| 410 | # TODO: missing 'bolder' and 'lighter' |
|---|
| 411 | try: |
|---|
| 412 | weight = { |
|---|
| 413 | '100': pango.WEIGHT_ULTRALIGHT, |
|---|
| 414 | '200': pango.WEIGHT_ULTRALIGHT, |
|---|
| 415 | '300': pango.WEIGHT_LIGHT, |
|---|
| 416 | '400': pango.WEIGHT_NORMAL, |
|---|
| 417 | '500': pango.WEIGHT_NORMAL, |
|---|
| 418 | '600': pango.WEIGHT_BOLD, |
|---|
| 419 | '700': pango.WEIGHT_BOLD, |
|---|
| 420 | '800': pango.WEIGHT_ULTRABOLD, |
|---|
| 421 | '900': pango.WEIGHT_HEAVY, |
|---|
| 422 | 'normal': pango.WEIGHT_NORMAL, |
|---|
| 423 | 'bold': pango.WEIGHT_BOLD, |
|---|
| 424 | } [value] |
|---|
| 425 | except KeyError: |
|---|
| 426 | warnings.warn("unknown font-style %s" % value) |
|---|
| 427 | else: |
|---|
| 428 | tag.set_property("weight", weight) |
|---|
| 429 | |
|---|
| 430 | def _parse_style_font_family(self, tag, value): |
|---|
| 431 | tag.set_property("family", value) |
|---|
| 432 | |
|---|
| 433 | def _parse_style_text_align(self, tag, value): |
|---|
| 434 | try: |
|---|
| 435 | align = { |
|---|
| 436 | 'left': gtk.JUSTIFY_LEFT, |
|---|
| 437 | 'right': gtk.JUSTIFY_RIGHT, |
|---|
| 438 | 'center': gtk.JUSTIFY_CENTER, |
|---|
| 439 | 'justify': gtk.JUSTIFY_FILL, |
|---|
| 440 | } [value] |
|---|
| 441 | except KeyError: |
|---|
| 442 | warnings.warn("Invalid text-align:%s requested" % value) |
|---|
| 443 | else: |
|---|
| 444 | tag.set_property("justification", align) |
|---|
| 445 | |
|---|
| 446 | def _parse_style_text_decoration(self, tag, value): |
|---|
| 447 | if value == "none": |
|---|
| 448 | tag.set_property("underline", pango.UNDERLINE_NONE) |
|---|
| 449 | tag.set_property("strikethrough", False) |
|---|
| 450 | elif value == "underline": |
|---|
| 451 | tag.set_property("underline", pango.UNDERLINE_SINGLE) |
|---|
| 452 | tag.set_property("strikethrough", False) |
|---|
| 453 | elif value == "overline": |
|---|
| 454 | warnings.warn("text-decoration:overline not implemented") |
|---|
| 455 | tag.set_property("underline", pango.UNDERLINE_NONE) |
|---|
| 456 | tag.set_property("strikethrough", False) |
|---|
| 457 | elif value == "line-through": |
|---|
| 458 | tag.set_property("underline", pango.UNDERLINE_NONE) |
|---|
| 459 | tag.set_property("strikethrough", True) |
|---|
| 460 | elif value == "blink": |
|---|
| 461 | warnings.warn("text-decoration:blink not implemented") |
|---|
| 462 | else: |
|---|
| 463 | warnings.warn("text-decoration:%s not implemented" % value) |
|---|
| 464 | |
|---|
| 465 | def _parse_style_white_space(self, tag, value): |
|---|
| 466 | if value == 'pre': |
|---|
| 467 | tag.set_property("wrap_mode", gtk.WRAP_NONE) |
|---|
| 468 | elif value == 'normal': |
|---|
| 469 | tag.set_property("wrap_mode", gtk.WRAP_WORD) |
|---|
| 470 | elif value == 'nowrap': |
|---|
| 471 | tag.set_property("wrap_mode", gtk.WRAP_NONE) |
|---|
| 472 | |
|---|
| 473 | |
|---|
| 474 | # build a dictionary mapping styles to methods, for greater speed |
|---|
| 475 | __style_methods = dict() |
|---|
| 476 | for style in ["background-color", "color", "font-family", "font-size", |
|---|
| 477 | "font-style", "font-weight", "margin-left", "margin-right", |
|---|
| 478 | "text-align", "text-decoration", "white-space", 'display' ]: |
|---|
| 479 | try: |
|---|
| 480 | method = locals()["_parse_style_%s" % style.replace('-', '_')] |
|---|
| 481 | except KeyError: |
|---|
| 482 | warnings.warn("Style attribute '%s' not yet implemented" % style) |
|---|
| 483 | else: |
|---|
| 484 | __style_methods[style] = method |
|---|
| 485 | del style |
|---|
| 486 | # -- |
|---|
| 487 | |
|---|
| 488 | def _get_style_tags(self): |
|---|
| 489 | return [tag for tag in self.styles if tag is not None] |
|---|
| 490 | |
|---|
| 491 | def _create_url(self, href, title, type_, id_): |
|---|
| 492 | tag = self.textbuf.create_tag(id_) |
|---|
| 493 | if href and href[0] != '#': |
|---|
| 494 | tag.href = href |
|---|
| 495 | tag.type_ = type_ # to be used by the URL handler |
|---|
| 496 | tag.connect('event', self.textview.html_hyperlink_handler, 'url', href) |
|---|
| 497 | tag.set_property('foreground', '#0000ff') |
|---|
| 498 | tag.set_property('underline', pango.UNDERLINE_SINGLE) |
|---|
| 499 | tag.is_anchor = True |
|---|
| 500 | if title: |
|---|
| 501 | tag.title = title |
|---|
| 502 | return tag |
|---|
| 503 | |
|---|
| 504 | |
|---|
| 505 | def _begin_span(self, style, tag=None, id_=None): |
|---|
| 506 | if style is None: |
|---|
| 507 | self.styles.append(tag) |
|---|
| 508 | return None |
|---|
| 509 | if tag is None: |
|---|
| 510 | if id_: |
|---|
| 511 | tag = self.textbuf.create_tag(id_) |
|---|
| 512 | else: |
|---|
| 513 | tag = self.textbuf.create_tag() # we create anonymous tag |
|---|
| 514 | for attr, val in [item.split(':', 1) for item in style.split(';') if len(item.strip())]: |
|---|
| 515 | attr = attr.strip().lower() |
|---|
| 516 | val = val.strip() |
|---|
| 517 | try: |
|---|
| 518 | method = self.__style_methods[attr] |
|---|
| 519 | except KeyError: |
|---|
| 520 | warnings.warn("Style attribute '%s' requested " |
|---|
| 521 | "but not yet implemented" % attr) |
|---|
| 522 | else: |
|---|
| 523 | method(self, tag, val) |
|---|
| 524 | self.styles.append(tag) |
|---|
| 525 | |
|---|
| 526 | def _end_span(self): |
|---|
| 527 | self.styles.pop() |
|---|
| 528 | |
|---|
| 529 | def _jump_line(self): |
|---|
| 530 | self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol') |
|---|
| 531 | self.starting = True |
|---|
| 532 | |
|---|
| 533 | def _insert_text(self, text): |
|---|
| 534 | if self.starting and text != '\n': |
|---|
| 535 | self.starting = (text[-1] == '\n') |
|---|
| 536 | tags = self._get_style_tags() |
|---|
| 537 | if tags: |
|---|
| 538 | self.textbuf.insert_with_tags(self.iter, text, *tags) |
|---|
| 539 | else: |
|---|
| 540 | self.textbuf.insert(self.iter, text) |
|---|
| 541 | |
|---|
| 542 | def _starts_line(self): |
|---|
| 543 | return self.starting or self.iter.starts_line() |
|---|
| 544 | |
|---|
| 545 | def _flush_text(self): |
|---|
| 546 | if not self.text: return |
|---|
| 547 | text, self.text = self.text, '' |
|---|
| 548 | if not self.preserve: |
|---|
| 549 | text = text.replace('\n', ' ') |
|---|
| 550 | self.handle_specials(whitespace_rx.sub(' ', text)) |
|---|
| 551 | else: |
|---|
| 552 | self._insert_text(text.strip("\n")) |
|---|
| 553 | |
|---|
| 554 | def _anchor_event(self, tag, textview, event, iter, href, type_): |
|---|
| 555 | if event.type == gtk.gdk.BUTTON_PRESS: |
|---|
| 556 | self.textview.emit("url-clicked", href, type_) |
|---|
| 557 | return True |
|---|
| 558 | return False |
|---|
| 559 | |
|---|
| 560 | def handle_specials(self, text): |
|---|
| 561 | index = 0 |
|---|
| 562 | se = self.textview.config.get('show_ascii_formatting_chars') |
|---|
| 563 | if self.textview.config.get('emoticons_theme'): |
|---|
| 564 | iterator = self.textview.emot_and_basic_re.finditer(text) |
|---|
| 565 | else: |
|---|
| 566 | iterator = self.textview.basic_pattern_re.finditer(text) |
|---|
| 567 | for match in iterator: |
|---|
| 568 | start, end = match.span() |
|---|
| 569 | special_text = text[start:end] |
|---|
| 570 | if start != 0: |
|---|
| 571 | self._insert_text(text[index:start]) |
|---|
| 572 | index = end # update index |
|---|
| 573 | #emoticons |
|---|
| 574 | possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS |
|---|
| 575 | if self.textview.config.get('emoticons_theme') and \ |
|---|
| 576 | possible_emot_ascii_caps in self.textview.interface.emoticons.keys(): |
|---|
| 577 | #it's an emoticon |
|---|
| 578 | emot_ascii = possible_emot_ascii_caps |
|---|
| 579 | anchor = self.textbuf.create_child_anchor(self.iter) |
|---|
| 580 | img = gtk.Image() |
|---|
| 581 | img.set_from_file(self.textview.interface.emoticons[emot_ascii]) |
|---|
| 582 | img.show() |
|---|
| 583 | # TODO: add alt/tooltip with the special_text (a11y) |
|---|
| 584 | self.textview.add_child_at_anchor(img, anchor) |
|---|
| 585 | else: |
|---|
| 586 | # now print it |
|---|
| 587 | if special_text.startswith('/'): # it's explicit italics |
|---|
| 588 | self.startElement('i', {}) |
|---|
| 589 | elif special_text.startswith('_'): # it's explicit underline |
|---|
| 590 | self.startElement("u", {}) |
|---|
| 591 | if se: self._insert_text(special_text[0]) |
|---|
| 592 | self.handle_specials(special_text[1:-1]) |
|---|
| 593 | if se: self._insert_text(special_text[0]) |
|---|
| 594 | if special_text.startswith('_'): # it's explicit underline |
|---|
| 595 | self.endElement('u') |
|---|
| 596 | if special_text.startswith('/'): # it's explicit italics |
|---|
| 597 | self.endElement('i') |
|---|
| 598 | if index < len(text): |
|---|
| 599 | self._insert_text(text[index:]) |
|---|
| 600 | |
|---|
| 601 | def characters(self, content): |
|---|
| 602 | if self.preserve: |
|---|
| 603 | self.text += content |
|---|
| 604 | return |
|---|
| 605 | if allwhitespace_rx.match(content) is not None and self._starts_line(): |
|---|
| 606 | return |
|---|
| 607 | self.text += content |
|---|
| 608 | self.starting = False |
|---|
| 609 | |
|---|
| 610 | |
|---|
| 611 | def startElement(self, name, attrs): |
|---|
| 612 | self._flush_text() |
|---|
| 613 | klass = [i for i in attrs.get('class',' ').split(' ') if i] |
|---|
| 614 | style = attrs.get('style','') |
|---|
| 615 | #Add styles defined for classes |
|---|
| 616 | #TODO: priority between class and style elements? |
|---|
| 617 | for k in klass: |
|---|
| 618 | if k in classes: |
|---|
| 619 | style += classes[k] |
|---|
| 620 | |
|---|
| 621 | tag = None |
|---|
| 622 | #FIXME: if we want to use id, it needs to be unique across |
|---|
| 623 | # the whole textview, so we need to add something like the |
|---|
| 624 | # message-id to it. |
|---|
| 625 | #id_ = attrs.get('id',None) |
|---|
| 626 | id_ = None |
|---|
| 627 | |
|---|