Show
Ignore:
Timestamp:
08/31/07 10:43:27 (15 months ago)
Author:
asterix
Message:

[sgala and I] better support of sizes in XHTML, simplify rst generator

Files:
1 modified

Legend:

Unmodified
Added
Removed
  • branches/gajim_0.11.1/src/htmltextview.py

    r8597 r8665  
    4141import operator 
    4242 
     43if __name__ == '__main__': 
     44        from common import i18n 
    4345from common import gajim 
    44 #from common import i18n 
    45  
    4646 
    4747import tooltips 
     
    5757                                        float(gtk.gdk.screen_height_mm())) 
    5858 
    59 #embryo of CSS classes 
     59# embryo of CSS classes 
    6060classes = { 
    6161        #'system-message':';display: none', 
     
    6363} 
    6464 
    65 #styles for elemens 
     65# styles for elements 
    6666element_styles = { 
    6767                'u'                     : ';text-decoration: underline', 
     
    8383element_styles['i']   = element_styles['em'] 
    8484element_styles['b']   = element_styles['strong'] 
    85  
    86 class_styles = { 
    87 } 
    8885 
    8986""" 
     
    183180 
    184181def 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 
     182        # extra, rst does not mark _underline_ or /it/ up 
     183        # actually <b>, <i> or <u> are not in the JEP-0071, but are seen in the wild 
    187184        basic_pattern = r'(?<!\w|\<|/|:)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\w|/|:)|'\ 
    188185                                        r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)' 
    189186        view.basic_pattern_re = re.compile(basic_pattern) 
    190         #TODO: emoticons 
     187        # emoticons 
    191188        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 = '|' + \ 
    216214                        '(?:(?<![\w.]' + emoticons_pattern_prematch[:-1]   + '))' + \ 
    217215                        '(?:'       + emoticons_pattern[:-1]            + ')'  + \ 
    218216                        '(?:(?![\w.]'  + emoticons_pattern_postmatch[:-1]  + '))' 
     217        except: 
     218                pass 
    219219 
    220220        # because emoticons match later (in the string) they need to be after 
     
    231231        else: 
    232232                return gtk.gdk.color_parse(color) 
     233 
     234def style_iter(style): 
     235        return (map(lambda x:x.strip(),item.split(':', 1)) for item in style.split(';') if len(item.strip())) 
    233236         
    234237 
    235238class 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        """ 
    237244        def __init__(self, textview, startiter): 
    238245                xml.sax.handler.ContentHandler.__init__(self) 
     
    304311                callback(allocation.width*frac, *args) 
    305312 
    306         def _parse_length(self, value, font_relative, callback, *args): 
     313        def _parse_length(self, value, font_relative, block_relative, minl, maxl, callback, *args): 
    307314                '''Parse/calc length, converting to pixels, calls callback(length, *args) 
    308315                when the length is first computed or changes''' 
    309316                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  
    311322                        if font_relative: 
    312323                                attrs = self._get_current_attributes() 
    313324                                font_size = attrs.font.get_size() / pango.SCALE 
    314325                                callback(frac*display_resolution*font_size, *args) 
    315                         else: 
     326                        elif block_relative: 
    316327                                # CSS says "Percentage values: refer to width of the closest 
    317328                                #           block-level ancestor" 
     
    324335                                                                          self.__parse_length_frac_size_allocate, 
    325336                                                                          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 
    331349                        attrs = self._get_current_attributes() 
    332350                        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) 
    334352 
    335353                elif value.endswith('ex'): # x-height, ~ the height of the letter 'x' 
     
    338356                        attrs = self._get_current_attributes() 
    339357                        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) 
    341359 
    342360                elif value.endswith('px'): # pixels 
    343                         callback(int(value[:-2]), *args) 
     361                        callback(val, *args) 
    344362 
    345363                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 
    348374        def __parse_font_size_cb(length, tag): 
    349375                tag.set_property("size-points", length/display_resolution) 
     
    353379                if value == 'none': 
    354380                        tag.set_property('invisible','true') 
    355                 #Fixme: display: block, inline 
     381                # FIXME: display: block, inline 
    356382 
    357383        def _parse_style_font_size(self, tag, value): 
     
    378404                        tag.set_property("scale", pango.SCALE_LARGE) 
    379405                        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) 
    381408 
    382409        def _parse_style_font_style(self, tag, value): 
     
    400427                 
    401428        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, 
    403431                                                   tag, "left-margin") 
    404432 
    405433        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, 
    407436                                                   tag, "right-margin") 
    408437 
     
    470499                elif value == 'nowrap': 
    471500                        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") 
    472519          
    473520         
    474521        # build a dictionary mapping styles to methods, for greater speed 
    475522        __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' ]: 
    479527                try: 
    480528                        method = locals()["_parse_style_%s" % style.replace('-', '_')] 
     
    490538 
    491539        def _create_url(self, href, title, type_, id_): 
     540                '''Process a url tag. 
     541                ''' 
    492542                tag = self.textbuf.create_tag(id_) 
    493543                if href and href[0] != '#': 
     
    502552                return tag 
    503553 
     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 
    504673 
    505674        def _begin_span(self, style, tag=None, id_=None): 
     
    512681                        else: 
    513682                                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 
    517686                        try: 
    518687                                method = self.__style_methods[attr] 
     
    604773                        return 
    605774                if allwhitespace_rx.match(content) is not None and self._starts_line(): 
     775                        self.text += ' ' 
    606776                        return 
    607777                self.text += content 
     
    612782                self._flush_text() 
    613783                klass = [i for i in attrs.get('class',' ').split(' ') if i] 
    614                 style = attrs.get('style','') 
     784                style = '' 
    615785                #Add styles defined for classes 
    616                 #TODO: priority between class and style elements? 
    617786                for k in klass: 
    618787                        if k  in classes: 
     
    642811                elif name in LIST_ELEMS: 
    643812                        style += ';margin-left: 2em' 
     813                elif name == 'img': 
     814                        tag = self._process_img(attrs) 
    644815                if name in element_styles: 
    645816                        style += element_styles[name] 
    646  
     817                # so that explicit styles override implicit ones,  
     818                # we add the attribute last 
     819                style += ";"+attrs.get('style','') 
    647820                if style == '': 
    648821                        style = None         
     
    682855                        if not self.starting: 
    683856                                self._jump_line() 
    684                 elif name == 'img': 
    685                         # Wait maximum 1s for connection 
    686                         socket.setdefaulttimeout(1) 
    687                         try: 
    688                                 f = urllib2.urlopen(attrs['src']) 
    689                         except Exception, ex: 
    690                                 gajim.log.debug(str('Error loading image %s ' % attrs['src'] + ex)) 
    691                                 pixbuf = None 
    692                                 alt = attrs.get('alt', 'Broken image') 
    693                         else: 
    694                                 # Wait 10ms between each byte 
    695                                 try: 
    696                                         f.fp._sock.fp._sock.settimeout(0.1) 
    697                                 except: 
    698                                         pass 
    699                                 # Max image size = 2 MB (to try to prevent DoS) in Max 3s 
    700                                 mem = '' 
    701                                 deadline = time.time() + 3 
    702                                 while True: 
    703                                         if time.time() > deadline: 
    704                                                 gajim.log.debug(str('Timeout loading image %s ' % \ 
    705                                                         attrs['src'] + ex)) 
    706                                                 mem = '' 
    707                                                 alt = attrs.get('alt', '') 
    708                                                 if alt: 
    709                                                         alt += '\n' 
    710                                                 alt += _('Timeout loading image') 
    711                                                 break 
    712                                         try: 
    713                                                 temp = f.read(100) 
    714                                         except socket.timeout, ex: 
    715                                                 gajim.log.debug('Timeout loading image %s ' % attrs['src'] + \ 
    716                                                         str(ex)) 
    717                                                 mem = '' 
    718                                                 alt = attrs.get('alt', '') 
    719                                                 if alt: 
    720                                                         alt += '\n' 
    721                                                 alt += _('Timeout loading image') 
    722                                                 break 
    723                                         if temp: 
    724                                                 mem += temp 
    725                                         else: 
    726                                                 break 
    727                                         if len(mem) > 2*1024*1024: 
    728                                                 alt = attrs.get('alt', '') 
    729                                                 if alt: 
    730                                                         alt += '\n' 
    731                                                 alt += _('Image is too big') 
    732                                                 break 
    733  
    734                                 if mem: 
    735                                         # Caveat: GdkPixbuf is known not to be safe to load 
    736                                         # images from network... this program is now potentially 
    737                                         # hackable ;) 
    738                                         loader = gtk.gdk.PixbufLoader() 
    739                                         loader.write(mem) 
    740                                         loader.close() 
    741                                         pixbuf = loader.get_pixbuf() 
    742                                 else: 
    743                                         pixbuf = None 
    744                         if pixbuf is not None: 
    745                                 tags = self._get_style_tags() 
    746                                 if tags: 
    747                                         tmpmark = self.textbuf.create_mark(None, self.iter, True) 
    748  
    749                                 self.textbuf.insert_pixbuf(self.iter, pixbuf) 
    750  
    751                                 if tags: 
    752                                         start = self.textbuf.get_iter_at_mark(tmpmark) 
    753                                         for tag in tags: 
    754                                                 self.textbuf.apply_tag(tag, start, self.iter) 
    755                                         self.textbuf.delete_mark(tmpmark) 
    756                         else: 
    757