Changeset 8665

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

Location:
branches/gajim_0.11.1/src
Files:
2 modified

Legend:

Unmodified
Added
Removed
  • branches/gajim_0.11.1/src/common/rst_xhtml_generator.py

    r7940 r8665  
    2222        from docutils.parsers.rst.roles import set_classes 
    2323except: 
     24        print "Requires docutils 0.4 for set_classes to be available" 
    2425        def create_xhtml(text): 
    2526                return None 
    2627else: 
    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 
    3035 
    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  
    3354                ''' 
     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], [] 
    3468 
    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. 
    5270 
    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) 
    5788 
    5889        class HTMLGenerator: 
     
    109140 
    110141if __name__ == '__main__': 
    111         print Generator.create_xhtml(''' 
     142        print "test 1\n", Generator.create_xhtml(""" 
    112143test:: 
    113144 
     
    119150this `` should    trigger`` should trigger the &nbsp; problem. 
    120151 
    121 ''') 
    122         print Generator.create_xhtml(''' 
     152""") 
     153        print "test 2\n", Generator.create_xhtml(""" 
    123154*test1 
    124155 
    125156test2_ 
    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  
    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