Source

trac-ticketlinks / tracopt / mimeview / php.py

Full commit
# -*- coding: utf-8 -*-
#
# Copyright (C) 2005-2009 Edgewall Software
# Copyright (C) 2005 Christian Boos <cboos@bct-technology.com>
# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/log/.
#
# Author: Christian Boos <cboos@bct-technology.com>
#         Christopher Lenz <cmlenz@gmx.de>

import os
import re

from genshi.core import Markup

from trac.core import *
from trac.config import Option
from trac.mimeview.api import IHTMLPreviewRenderer, content_to_unicode
from trac.util import NaivePopen
from trac.util.html import Deuglifier
from trac.util.translation import _

__all__ = ['PHPRenderer']

php_types = ('text/x-php', 'application/x-httpd-php',
             'application/x-httpd-php4', 'application/x-httpd-php1')


class PhpDeuglifier(Deuglifier):

    def format(self, indata):
        # The PHP highlighter produces the end-span tags on the next line
        # instead of the line they actually apply to, which causes
        # Trac to produce lots of (useless) open-and-immediately-close
        # spans beginning each line.  This tries to curtail by bubbling
        # the first span after a set of 1+ "<br />" to before them.
        r_fixeol = re.compile(r"((?:<br />)+)(</(?:font|span)>)")
        indata = r_fixeol.sub(lambda m: m.group(2) + m.group(1), indata)
        
        # Now call superclass implementation that handles the dirty work
        # of applying css classes.
        return Deuglifier.format(self, indata)

    @classmethod
    def rules(cls):
        colors = dict(comment='FF8000', lang='0000BB', keyword='007700',
                      string='DD0000')
        # rules check for <font> for PHP 4 or <span> for PHP 5
        color_rules = [
            r'(?P<%s><(?:font color="|span style="color: )#%s">)' % c
            for c in colors.items()
            ]
        return color_rules + [ r'(?P<font><font.*?>)', r'(?P<endfont></font>)' ]


class PHPRenderer(Component):
    """Syntax highlighter using the PHP executable."""

    implements(IHTMLPreviewRenderer)

    path = Option('mimeviewer', 'php_path', 'php',
        """Path to the PHP executable (''since 0.9'').""")

    returns_source = True

    # IHTMLPreviewRenderer methods

    def get_quality_ratio(self, mimetype):
        if mimetype in php_types:
            return 5
        return 0

    def render(self, context, mimetype, content, filename=None, rev=None):
        # -n to ignore php.ini so we're using default colors
        cmdline = '%s -sn' % self.path
        self.env.log.debug("PHP command line: %s" % cmdline)

        content = content_to_unicode(self.env, content, mimetype)
        content = content.encode('utf-8')
        np = NaivePopen(cmdline, content, capturestderr=1)
        if (os.name != 'nt' and np.errorlevel) or np.err:
            msg = 'Running (%s) failed: %s, %s.' % (cmdline,
                                                    np.errorlevel,
                                                    np.err)
            raise Exception(msg)

        odata = ''.join(np.out.splitlines()[1:-1])
        if odata.startswith('X-Powered-By:') or \
                odata.startswith('Content-type:'):
            raise TracError(_('You appear to be using the PHP CGI '
                              'binary. Trac requires the CLI version '
                              'for syntax highlighting.'))

        epilogues = ["</span>", "</font>"]
        for e in epilogues:
            if odata.endswith(e):
                odata = odata[:-len(e)]
                break

        html = PhpDeuglifier().format(odata.decode('utf-8'))

        # PHP generates _way_ too many non-breaking spaces...
        # We don't need them anyway, so replace them by normal spaces
        return [Markup(line.replace('&nbsp;', ' '))
                for line in html.split('<br />')]