Edgewall Software

Changeset 1764

Show
Ignore:
Timestamp:
06/06/2005 03:05:42 PM (3 years ago)
Author:
cmlenz
Message:

Display line numbers for files in the repository. Closes #520.

Location:
trunk
Files:
6 modified

Legend:

Unmodified
Added
Removed
  • trunk/htdocs/css/code.css

    r1519 r1764  
    1 .code-block { 
     1table.code-block-block { 
     2 border: 1px solid #ddd; 
     3 border-spacing: 0; 
     4 border-top: 0; 
     5 empty-cells: show; 
     6 font-size: 12px; 
     7 line-height: 130%; 
     8 padding: 0; 
     9 margin: 0 auto; 
     10 width: 100%; 
     11} 
     12table.code-block th { 
     13 border-right: 1px solid #d7d7d7; 
     14 border-bottom: 1px solid #998; 
     15 font-size: 11px; 
     16} 
     17table.code-block thead th { 
     18 background: #eee; 
     19 border-top: 1px solid #d7d7d7; 
     20 color: #999; 
     21 padding: 0 .25em; 
     22 text-align: center; 
     23 white-space: nowrap; 
     24} 
     25table.code-block tbody th { 
     26 background: #eed; 
     27 color: #886; 
     28 font-weight: normal; 
     29 padding: 0 .5em; 
     30 text-align: right; 
     31 vertical-align: top; 
     32} 
     33table.code-block tbody td { 
     34 background: #fff; 
     35 font: normal 11px monospace; 
     36 overflow: hidden; 
     37 padding: 1px 2px; 
     38 vertical-align: top; 
     39} 
     40 
     41div.code-block { 
    242 border: 1px dotted #d7d7d7; 
    343 margin: 1em 0; 
  • trunk/trac/Browser.py

    r1763 r1764  
    230230            else: 
    231231                preview = Mimeview(self.env).render(req, mime_type, content, 
    232                                                     node.name, node.rev) 
     232                                                    node.name, node.rev, 
     233                                                    annotations=['lineno']) 
    233234            req.hdf['file.preview'] = preview 
    234235 
  • trunk/trac/mimeview/api.py

    r1697 r1764  
    2323# 
    2424 
     25from __future__ import generators 
     26import re 
     27try: 
     28    from cStringIO import StringIO 
     29except ImportError: 
     30    from StringIO import StringIO 
     31 
    2532from trac.core import * 
     33from trac.util import enum, escape 
    2634 
    2735__all__ = ['get_charset', 'get_mimetype', 'is_binary', 'Mimeview'] 
     
    9199 
    92100def get_charset(mimetype): 
    93     """ 
    94     Returns the character encoding included in the given content type string, 
     101    """Return the character encoding included in the given content type string, 
    95102    or `None` if `mimetype` is `None` or empty or if no charset information is 
    96103    available. 
     
    102109 
    103110def get_mimetype(filename): 
     111    """Guess the most probable MIME type of a file with the given name.""" 
    104112    try: 
    105113        suffix = filename.split('.')[-1] 
     
    112120 
    113121def is_binary(str): 
    114     """ 
    115     Try to detect content by checking the first thousand bytes for zeroes. 
    116     """ 
     122    """Detect binary content by checking the first thousand bytes for zeroes.""" 
    117123    for i in range(0, min(len(str), 1000)): 
    118124        if str[i] == '\0': 
     
    120126    return False 
    121127 
    122  
    123128class IHTMLPreviewRenderer(Interface): 
     129    """Extension point interface for components that add HTML renderers of 
     130    specific content types to the `Mimeview` component. 
    124131    """ 
    125     Extension point interface for components that add HTML renderers of specific 
    126     content types to the `Mimeview` component. 
    127     """ 
    128132 
    129133    def get_quality_ratio(mimetype): 
    130         """ 
    131         Return the level of support this renderer provides for the content of 
     134        """Return the level of support this renderer provides for the content of 
    132135        the specified MIME type. The return value must be a number between 0 
    133136        and 9, where 0 means no support and 9 means "perfect" support. 
     
    135138 
    136139    def render(req, mimetype, content, filename=None, rev=None): 
    137         """ 
    138         Render an XHTML preview of the given content of the specified MIME type, 
    139         and return the generated XHTML text as a string. 
     140        """Render an XHTML preview of the given content of the specified MIME 
     141        type. 
     142         
     143        Can return the generated XHTML text as a single string or as an iterable 
     144        that yields strings. In the latter case, the list will be considered to 
     145        correspond to lines of text in the original content. 
    140146 
    141147        The `filename` and `rev` parameters are provided for renderers that 
     
    144150        """ 
    145151 
     152class IHTMLPreviewAnnotator(Interface): 
     153    """Extension point interface for components that can annotate an XHTML 
     154    representation of file contents with additional information.""" 
     155 
     156    def get_annotation_type(): 
     157        """Return a (type, label, description) tuple that defines the type of 
     158        annotation and provides human readable names. The `type` element should 
     159        be unique to the annotator. The `label` element is used as column 
     160        heading for the table, while `description` is used as a display name to 
     161        let the user toggle the appearance of the annotation type. 
     162        """ 
     163 
     164    def annotate_line(number, content): 
     165        """Return the XHTML markup for the table cell that contains the 
     166        annotation data.""" 
     167 
    146168 
    147169class Mimeview(Component): 
     
    149171 
    150172    renderers = ExtensionPoint(IHTMLPreviewRenderer) 
    151  
    152     def render(self, req, mimetype, content, filename=None, rev=None): 
     173    annotators = ExtensionPoint(IHTMLPreviewAnnotator) 
     174 
     175    # Public API 
     176 
     177    def get_annotation_types(self): 
     178        """Generator that returns all available annotation types.""" 
     179        for annotator in self.annotators: 
     180            yield annotator.get_annotation_type() 
     181 
     182    def render(self, req, mimetype, content, filename=None, rev=None, 
     183               annotations=None): 
     184        """Render an XHTML preview of the given content of the specified MIME 
     185        type, selecting the most appropriate `IHTMLPreviewRenderer` 
     186        implementation available for the given MIME type. 
     187 
     188        Return a string containing the XHTML text. 
     189        """ 
    153190        if not content: 
    154191            return '' 
     
    168205                self.log.debug('Trying to render HTML preview using %s' 
    169206                               % renderer.__class__.__name__) 
    170                 return renderer.render(req, mimetype, content, filename, rev) 
     207                result = renderer.render(req, mimetype, content, filename, rev) 
     208                break 
    171209            except Exception, e: 
    172210                self.log.warning('HTML preview using %s failed (%s)' 
    173211                                 % (renderer, e)) 
    174212 
    175         return None 
     213        if result and not isinstance(result, (str, unicode)): 
     214            if annotations: 
     215                return self._annotate(result, annotations) 
     216            else: 
     217                buf = StringIO() 
     218                buf.write('<div class="code-block"><pre>') 
     219                for line in result: 
     220                    buf.write(line + '\n') 
     221                buf.write('<pre></div>') 
     222                return buf.getvalue() 
     223 
     224        return result 
     225 
     226    def _annotate(self, lines, annotations): 
     227        buf = StringIO() 
     228        buf.write('<table class="code-block listing"><thead><tr>') 
     229        annotators = [] 
     230        for annotator in self.annotators: 
     231            atype, alabel, adesc = annotator.get_annotation_type() 
     232            if atype in annotations: 
     233                buf.write('<th class="%s">%s</th>' % (atype, alabel)) 
     234                annotators.append(annotator) 
     235        buf.write('<th class="content">&nbsp;</th>') 
     236        buf.write('</tr></thead><tbody>') 
     237 
     238        space_re = re.compile(' ( +)|^ ') 
     239        def htmlify(match): 
     240            div, mod = divmod(len(match.group(0)), 2) 
     241            return div * '&nbsp; ' + mod * '&nbsp;' 
     242 
     243        for num, line in enum(_html_splitlines(lines)): 
     244            cells = [] 
     245            for annotator in annotators: 
     246                cells.append(annotator.annotate_line(num + 1, line)) 
     247            cells.append('<td>%s</td>\n' % space_re.sub(htmlify, line)) 
     248            buf.write('<tr>' + '\n'.join(cells) + '</tr>') 
     249        buf.write('</tbody></table>') 
     250        return buf.getvalue() 
     251 
     252 
     253def _html_splitlines(lines): 
     254    """Tracks open and close tags in lines of HTML text and yields lines that 
     255    have no tags spanning more than one line.""" 
     256    open_tag_re = re.compile(r'<(\w+)\s.*?[^/]?>') 
     257    close_tag_re = re.compile(r'</(\w+)>') 
     258    open_tags = [] 
     259    for line in lines: 
     260        # Reopen tags still open from the previous line 
     261        for tag in open_tags: 
     262            line = tag.group(0) + line 
     263        open_tags = [] 
     264 
     265        # Find all tags opened on this line 
     266        for tag in open_tag_re.finditer(line): 
     267            open_tags.append(tag) 
     268 
     269        # Find all tags closed on this line 
     270        for ctag in close_tag_re.finditer(line): 
     271            for otag in open_tags: 
     272                if otag.group(1) == ctag.group(1): 
     273                    open_tags.remove(otag) 
     274                    break 
     275 
     276        # Close all tags still open at the end of line, they'll get reopened at 
     277        # the beginning of the next line 
     278        for tag in open_tags: 
     279            line += '</%s>' % tag.group(1) 
     280 
     281        yield line 
     282 
     283 
     284class LineNumberAnnotator(Component): 
     285    """Text annotator that adds a column with line numbers.""" 
     286    implements(IHTMLPreviewAnnotator) 
     287 
     288    # ITextAnnotator methods 
     289 
     290    def get_annotation_type(self): 
     291        return 'lineno', 'Line', 'Line numbers' 
     292 
     293    def annotate_line(self, number, content): 
     294        return '<th id="l%s">%s</th>' % (number, number) 
    176295 
    177296 
    178297class PlainTextRenderer(Component): 
     298    """HTML preview renderer for plain text, and fallback for any kind of text 
     299    for which no more specific renderer is available. 
    179300    """ 
    180     HTML preview renderer for plain text, and fallback for any kind of text for 
    181     which no more specific renderer is available. 
    182     """ 
    183  
    184301    implements(IHTMLPreviewRenderer) 
    185302 
     
    190307        if is_binary(content): 
    191308            self.env.log.debug("Binary data; no preview available") 
    192             return '' 
     309            return 
    193310 
    194311        self.env.log.debug("Using default plain text mimeviewer") 
    195312        from trac.util import escape 
    196         return '<pre class="code-block">' + escape(content) + '</pre>' 
     313        for line in content.splitlines(): 
     314            yield line 
    197315 
    198316 
    199317class ImageRenderer(Component): 
    200     """ 
    201     Inline image display. 
    202     """ 
    203  
     318    """Inline image display.""" 
    204319    implements(IHTMLPreviewRenderer) 
    205320 
     
    218333 
    219334class WikiTextRenderer(Component): 
    220     """ 
    221     Render files containing Trac's own Wiki formatting markup. 
    222     """ 
    223  
     335    """Render files containing Trac's own Wiki formatting markup.""" 
    224336    implements(IHTMLPreviewRenderer) 
    225337 
  • trunk/trac/mimeview/enscript.py

    r1620 r1764  
    9191            r'(?P<var><FONT COLOR="#DA70D6">)', 
    9292            r'(?P<font><FONT.*?>)', 
    93             r'(?P<endfont></FONT>)', 
     93            r'(?P<endfont></FONT>)' 
    9494        ] 
    9595    rules = classmethod(rules) 
     
    115115        np = NaivePopen(cmdline, content, capturestderr=1) 
    116116        if np.errorlevel or np.err: 
    117             err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel, np.err) 
     117            err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel, 
     118                                                    np.err) 
    118119            raise Exception, err 
    119120        odata = np.out 
    120121 
    121122        # Strip header and footer 
    122         i = odata.find('</H1>') 
    123         beg = i > 0 and i + 7 
     123        i = odata.find('<PRE>') 
     124        beg = i > 0 and i + 6 
    124125        i = odata.rfind('</PRE>') 
    125         end = i > 0 and i + 6 or len(odata) 
     126        end = i > 0 and i or len(odata) 
    126127 
    127128        odata = EnscriptDeuglifier().format(odata[beg:end]) 
    128         return '<div class="code-block">' + odata + '</div>' 
     129        for line in odata.splitlines(): 
     130            yield line 
  • trunk/trac/mimeview/php.py

    r1620 r1764  
    6767        np = NaivePopen(cmdline, content, capturestderr=1) 
    6868        if np.errorlevel or np.err: 
    69             err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel, np.err) 
     69            err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel, 
     70                                                    np.err) 
    7071            raise Exception, err 
    7172        odata = np.out 
    7273 
    7374        # Strip header 
    74         beg = odata.find('<code>') 
    75         odata = PhpDeuglifier().format(odata[beg:]) 
    76         return '<div class="code-block">' + odata + '</div>' 
     75        html = PhpDeuglifier().format(odata.splitlines()[1]) 
     76        for line in html.split('<br />'): 
     77            # PHP generates _way_ too many non-breaking spaces... 
     78            # We don't need them anyway, so replace them by normal spaces 
     79            yield line.replace('&nbsp;', ' ') 
  • trunk/trac/mimeview/silvercity.py

    r1620 r1764  
    2323# Get it at: http://silvercity.sourceforge.net/ 
    2424# 
     25 
     26import re 
     27try: 
     28    from cStringIO import StringIO 
     29except ImportError: 
     30    from StringIO import StringIO 
    2531 
    2632from trac.core import * 
     
    8389            raise Exception, err 
    8490 
    85         try: 
    86             from cStringIO import StringIO 
    87         except ImportError: 
    88             from StringIO import StringIO 
    89  
    9091        buf = StringIO() 
    9192        generator().generate_html(buf, content) 
    92         return '<div class="code-block">%s</div>\n' % buf.getvalue() 
     93 
     94        br_re = re.compile(r'<br\s*/?>$', re.MULTILINE) 
     95        span_default_re = re.compile(r'<span class="p_default">(.*?)</span>', 
     96                                     re.DOTALL) 
     97        html = span_default_re.sub(r'\1', br_re.sub('', buf.getvalue())) 
     98 
     99        for line in html.splitlines(): 
     100            # SilverCity generates _way_ too many non-breaking spaces... 
     101            # We don't need them anyway, so replace them by normal spaces 
     102            yield line.replace('&nbsp;', ' ')