Changeset 8665
- Timestamp:
- 08/31/07 10:43:27 (15 months ago)
- Location:
- branches/gajim_0.11.1/src
- Files:
-
- 2 modified
-
common/rst_xhtml_generator.py (modified) (3 diffs)
-
htmltextview.py (modified) (25 diffs)
Legend:
- Unmodified
- Added
- Removed
-
branches/gajim_0.11.1/src/common/rst_xhtml_generator.py
r7940 r8665 22 22 from docutils.parsers.rst.roles import set_classes 23 23 except: 24 print "Requires docutils 0.4 for set_classes to be available" 24 25 def create_xhtml(text): 25 26 return None 26 27 else: 27 def jep_reference_role(role, rawtext, text, lineno, inliner, 28 options={}, content=[]): 29 '''Role to make handy references to Jabber Enhancement Proposals (JEP). 28 def pos_int_validator(text): 29 """Validates that text can be evaluated as a positive integer.""" 30 result = int(text) 31 if result < 0: 32 raise ValueError("Error: value '%(text)s' " 33 "must be a positive integer") 34 return result 30 35 31 Use as :JEP:`71` (or jep, or jep-reference). 32 Modeled after the sample in docutils documentation. 36 def generate_uri_role( role_name, aliases, 37 anchor_text, base_url, 38 interpret_url, validator): 39 '''Creates and register a uri based "interpreted role". 40 41 Those are similar to the RFC, and PEP ones, and take 42 role_name: 43 name that will be registered 44 aliases: 45 list of alternate names 46 anchor_text: 47 text that will be used, together with the role 48 base_url: 49 base url for the link 50 interpret_url: 51 this, modulo the validated text, will be added to it 52 validator: 53 should return the validated text, or raise ValueError 33 54 ''' 55 def uri_reference_role(role, rawtext, text, lineno, inliner, 56 options={}, content=[]): 57 try: 58 valid_text = validator(text) 59 except ValueError, e: 60 msg = inliner.reporter.error( e.message % dict(text=text), line=lineno) 61 prb = inliner.problematic(rawtext, rawtext, msg) 62 return [prb], [msg] 63 ref = base_url + interpret_url % valid_text 64 set_classes(options) 65 node = nodes.reference(rawtext, anchor_text + utils.unescape(text), refuri=ref, 66 **options) 67 return [node], [] 34 68 35 jep_base_url = 'http://www.jabber.org/jeps/' 36 jep_url = 'jep-%04d.html' 37 try: 38 jepnum = int(text) 39 if jepnum <= 0: 40 raise ValueError 41 except ValueError: 42 msg = inliner.reporter.error( 43 'JEP number must be a number greater than or equal to 1; ' 44 '"%s" is invalid.' % text, line=lineno) 45 prb = inliner.problematic(rawtext, rawtext, msg) 46 return [prb], [msg] 47 ref = jep_base_url + jep_url % jepnum 48 set_classes(options) 49 node = nodes.reference(rawtext, 'JEP ' + utils.unescape(text), refuri=ref, 50 **options) 51 return [node], [] 69 uri_reference_role.__doc__ = """Role to make handy references to URIs. 52 70 53 roles.register_canonical_role('jep-reference', jep_reference_role) 54 from docutils.parsers.rst.languages.en import roles 55 roles['jep-reference'] = 'jep-reference' 56 roles['jep'] = 'jep-reference' 71 Use as :%(role_name)s:`71` (or any of %(aliases)s). 72 It will use %(base_url)s+%(interpret_url)s 73 validator should throw a ValueError, containing optionally 74 a %%(text)s format, if the interpreted text is not valid. 75 """ % locals() 76 roles.register_canonical_role(role_name, uri_reference_role) 77 from docutils.parsers.rst.languages import en 78 en.roles[role_name] = role_name 79 for alias in aliases: 80 en.roles[alias] = role_name 81 82 generate_uri_role('xep-reference', ('jep', 'xep'), 83 'XEP #', 'http://www.xmpp.org/extensions/', 'xep-%04d.html', 84 pos_int_validator) 85 generate_uri_role('gajim-ticket-reference', ('ticket','gtrack'), 86 'Gajim Ticket #', 'http://trac.gajim.org/ticket/', '%d', 87 pos_int_validator) 57 88 58 89 class HTMLGenerator: … … 109 140 110 141 if __name__ == '__main__': 111 print Generator.create_xhtml('''142 print "test 1\n", Generator.create_xhtml(""" 112 143 test:: 113 144 … … 119 150 this `` should trigger`` should trigger the problem. 120 151 121 ''')122 print Generator.create_xhtml('''152 """) 153 print "test 2\n", Generator.create_xhtml(""" 123 154 *test1 124 155 125 156 test2_ 126 ''') 157 """) 158 print "test 3\n", Generator.create_xhtml(""":ticket:`316` implements :xep:`71`""") -
branches/gajim_0.11.1/src/htmltextview.py
r8597 r8665 41 41 import operator 42 42 43 if __name__ == '__main__': 44 from common import i18n 43 45 from common import gajim 44 #from common import i18n45 46 46 47 47 import tooltips … … 57 57 float(gtk.gdk.screen_height_mm())) 58 58 59 # embryo of CSS classes59 # embryo of CSS classes 60 60 classes = { 61 61 #'system-message':';display: none', … … 63 63 } 64 64 65 # styles for elemens65 # styles for elements 66 66 element_styles = { 67 67 'u' : ';text-decoration: underline', … … 83 83 element_styles['i'] = element_styles['em'] 84 84 element_styles['b'] = element_styles['strong'] 85 86 class_styles = {87 }88 85 89 86 """ … … 183 180 184 181 def build_patterns(view, config, interface): 185 # extra, rst does not mark _underline_ or /it/ up186 # actually <b>, <i> or <u> are not in the JEP-0071, but are seen in the wild182 # 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 187 184 basic_pattern = r'(?<!\w|\<|/|:)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\w|/|:)|'\ 188 185 r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)' 189 186 view.basic_pattern_re = re.compile(basic_pattern) 190 # TODO:emoticons187 # emoticons 191 188 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 = '|' + \ 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 = '|' + \ 216 214 '(?:(?<![\w.]' + emoticons_pattern_prematch[:-1] + '))' + \ 217 215 '(?:' + emoticons_pattern[:-1] + ')' + \ 218 216 '(?:(?![\w.]' + emoticons_pattern_postmatch[:-1] + '))' 217 except: 218 pass 219 219 220 220 # because emoticons match later (in the string) they need to be after … … 231 231 else: 232 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())) 233 236 234 237 235 238 class HtmlHandler(xml.sax.handler.ContentHandler): 236 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 """ 237 244 def __init__(self, textview, startiter): 238 245 xml.sax.handler.ContentHandler.__init__(self) … … 304 311 callback(allocation.width*frac, *args) 305 312 306 def _parse_length(self, value, font_relative, callback, *args):313 def _parse_length(self, value, font_relative, block_relative, minl, maxl, callback, *args): 307 314 '''Parse/calc length, converting to pixels, calls callback(length, *args) 308 315 when the length is first computed or changes''' 309 316 if value.endswith('%'): 310 frac = float(value[:-1])/100 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 311 322 if font_relative: 312 323 attrs = self._get_current_attributes() 313 324 font_size = attrs.font.get_size() / pango.SCALE 314 325 callback(frac*display_resolution*font_size, *args) 315 el se:326 elif block_relative: 316 327 # CSS says "Percentage values: refer to width of the closest 317 328 # block-level ancestor" … … 324 335 self.__parse_length_frac_size_allocate, 325 336 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 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 331 349 attrs = self._get_current_attributes() 332 350 font_size = attrs.font.get_size() / pango.SCALE 333 callback( float(value[:-2])*display_resolution*font_size, *args)351 callback(val*display_resolution*font_size, *args) 334 352 335 353 elif value.endswith('ex'): # x-height, ~ the height of the letter 'x' … … 338 356 attrs = self._get_current_attributes() 339 357 font_size = attrs.font.get_size() / pango.SCALE 340 callback( float(value[:-2])*display_resolution*font_size, *args)358 callback(val*display_resolution*font_size, *args) 341 359 342 360 elif value.endswith('px'): # pixels 343 callback( int(value[:-2]), *args)361 callback(val, *args) 344 362 345 363 else: 346 warnings.warn("Unable to parse length value '%s'" % value) 347 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 348 374 def __parse_font_size_cb(length, tag): 349 375 tag.set_property("size-points", length/display_resolution) … … 353 379 if value == 'none': 354 380 tag.set_property('invisible','true') 355 # Fixme: display: block, inline381 # FIXME: display: block, inline 356 382 357 383 def _parse_style_font_size(self, tag, value): … … 378 404 tag.set_property("scale", pango.SCALE_LARGE) 379 405 return 380 self._parse_length(value, True, self.__parse_font_size_cb, tag) 406 # font relative (5 ~ 4pt, 110 ~ 72pt) 407 self._parse_length(value, True, False, 5, 110, self.__parse_font_size_cb, tag) 381 408 382 409 def _parse_style_font_style(self, tag, value): … … 400 427 401 428 def _parse_style_margin_left(self, tag, value): 402 self._parse_length(value, False, self.__frac_length_tag_cb, 429 # block relative 430 self._parse_length(value, False, True, 1, 1000, self.__frac_length_tag_cb, 403 431 tag, "left-margin") 404 432 405 433 def _parse_style_margin_right(self, tag, value): 406 self._parse_length(value, False, self.__frac_length_tag_cb, 434 # block relative 435 self._parse_length(value, False, True, 1, 1000, self.__frac_length_tag_cb, 407 436 tag, "right-margin") 408 437 … … 470 499 elif value == 'nowrap': 471 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") 472 519 473 520 474 521 # build a dictionary mapping styles to methods, for greater speed 475 522 __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' ]: 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' ]: 479 527 try: 480 528 method = locals()["_parse_style_%s" % style.replace('-', '_')] … … 490 538 491 539 def _create_url(self, href, title, type_, id_): 540 '''Process a url tag. 541 ''' 492 542 tag = self.textbuf.create_tag(id_) 493 543 if href and href[0] != '#': … … 502 552 return tag 503 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 504 673 505 674 def _begin_span(self, style, tag=None, id_=None): … … 512 681 else: 513 682 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()683 for attr, val in style_iter(style): 684 attr = attr.lower() 685 val = val 517 686 try: 518 687 method = self.__style_methods[attr] … … 604 773 return 605 774 if allwhitespace_rx.match(content) is not None and self._starts_line(): 775 self.text += ' ' 606 776 return 607 777
