Index: htdocs/css/code.css
===================================================================
--- htdocs/css/code.css	(revision 4333)
+++ htdocs/css/code.css	(working copy)
@@ -50,17 +50,17 @@
 table.code tbody th :link:hover, table.code tbody th :visited:hover {
  color: #000;
 }
-table.code tbody td {
+table.code td {
  background: #fff;
  font: normal 11px monospace;
  overflow: hidden;
  padding: 1px 2px;
  vertical-align: top;
 }
-table.code tbody tr.hilite th {
+table.code tr.hilite th {
  background: #ccf;
 }
-table.code tbody tr.hilite td {
+table.code tr.hilite td {
  background: #ddf;
 }
 .image-file { background: #eee; padding: .3em }
Index: trac/mimeview/api.py
===================================================================
--- trac/mimeview/api.py	(revision 4335)
+++ trac/mimeview/api.py	(working copy)
@@ -59,8 +59,10 @@
 import re
 from StringIO import StringIO
 
-from genshi.core import escape, Markup, Stream
+from genshi import escape, Markup, Stream
 from genshi.builder import Fragment, tag
+from genshi.core import START, END, START_NS, END_NS, TEXT
+from genshi.input import HTML
 
 from trac.config import IntOption, ListOption, Option
 from trac.core import *
@@ -450,63 +452,52 @@
                                          filename, url)
                 if not result:
                     continue
-                elif isinstance(result, (Fragment, Stream)):
-                    return result
-                elif isinstance(result, basestring):
+                if isinstance(result, basestring):
                     return Markup(to_unicode(result))
-                elif annotations:
+
+                if isinstance(result, list):
+                    result = HTML('\n'.join(result))
+                elif isinstance(result, Fragment):
+                    result = result.generate()
+
+                if annotations:
                     m = req.args.get('marks')
-                    return Markup(self._annotate(result, annotations,
-                                                 m and Ranges(m)))
+                    return self._annotate(result, annotations, m and Ranges(m))
                 else:
-                    buf = StringIO()
-                    buf.write('<div class="code"><pre>')
-                    for line in result:
-                        buf.write(line + '\n')
-                    buf.write('</pre></div>')
-                    return Markup(buf.getvalue())
+                    return tag.div(class_='code')(tag.pre(result)).generate()
+
             except Exception, e:
                 self.log.warning('HTML preview using %s failed (%s)'
                                  % (renderer, e), exc_info=True)
 
-    def _annotate(self, lines, annotations, marks=None):
-        buf = StringIO()
-        buf.write('<table class="code"><thead><tr>')
+    def _annotate(self, stream, annotations, marks=None):
+        annotypes = []
         annotators = []
         for annotator in self.annotators:
-            atype, alabel, adesc = annotator.get_annotation_type()
+            atype, alabel, _ = annotator.get_annotation_type()
             if atype in annotations:
-                buf.write('<th class="%s">%s</th>' % (atype, alabel))
+                annotypes.append((atype, alabel))
                 annotators.append(annotator)
-        buf.write('<th class="content">&nbsp;</th>')
-        buf.write('</tr></thead><tbody>')
 
-        space_re = re.compile('(?P<spaces> (?: +))|'
-                              '^(?P<tag><\w+.*?>)?( )')
-        def htmlify(match):
-            m = match.group('spaces')
-            if m:
-                div, mod = divmod(len(m), 2)
-                return div * '&nbsp; ' + mod * '&nbsp;'
-            return (match.group('tag') or '') + '&nbsp;'
+        def _head_rows():
+            yield tag.tr(
+                [tag.th(alabel, class_=atype) for atype, alabel in annotypes],
+                tag.th(u' ', class_='content')
+            )
 
-        num = -1
-        for num, line in enumerate(_html_splitlines(lines)):
-            cells = []
-            for annotator in annotators:
-                cells.append(annotator.annotate_line(num + 1, line))
-            cells.append('<td>%s</td>\n' % space_re.sub(htmlify, line))
-            if marks and num+1 in marks:
-                buf.write('<tr class="%s">' % ('hilite',) +
-                          '\n'.join(cells) + '</tr>')
-            else:
-                buf.write('<tr>' + '\n'.join(cells) + '</tr>')
-        else:
-            if num < 0:
-                return ''
-        buf.write('</tbody></table>')
-        return buf.getvalue()
+        def _body_rows():
+            for num, substream in enumerate(_stream_splitlines(stream)):
+                hilite = (marks and num + 1 in marks) and 'hilite' or None
+                yield tag.tr(class_=hilite)(
+                    [a.annotate_line(num + 1, substream) for a in annotators],
+                    tag.td(substream)
+                )
 
+        return tag.table(class_='code')(
+            tag.thead(_head_rows()),
+            tag.tbody(_body_rows())
+        ).generate()
+
     def get_max_preview_size(self):
         """Deprecated: use `max_preview_size` attribute directly."""
         return self.max_preview_size
@@ -617,41 +608,73 @@
         req.end_headers()
         req.write(content)
         raise RequestDone        
-        
 
-def _html_splitlines(lines):
-    """Tracks open and close tags in lines of HTML text and yields lines that
-    have no tags spanning more than one line."""
-    open_tag_re = re.compile(r'<(\w+)(\s.*?)?[^/]?>')
-    close_tag_re = re.compile(r'</(\w+)>')
-    open_tags = []
-    for line in lines:
-        # Reopen tags still open from the previous line
-        for tag in open_tags:
-            line = tag.group(0) + line
-        open_tags = []
 
-        # Find all tags opened on this line
-        for tag in open_tag_re.finditer(line):
-            open_tags.append(tag)
+def _stream_splitlines(stream):
+    space_re = re.compile('(?P<spaces> (?: +))|^(?P<tag><\w+.*?>)?( )')
+    def pad_spaces(match):
+        m = match.group('spaces')
+        if m:
+            div, mod = divmod(len(m), 2)
+            return div * u'\xa0 ' + mod * u'\xa0'
+        return (match.group('tag') or '') + u'\xa0'
 
-        open_tags.reverse()
+    def _generate():
+        stack = []
+        for kind, data, pos in stream:
+            if kind is TEXT:
+                if '\n' in data:
+                    lines = data.splitlines(True)
+                    for e in stack:
+                        yield e
+                    yield kind, lines.pop(0).rstrip('\n'), pos
+                    for event in reversed(stack):
+                        if event[0] is START:
+                            yield END, event[1][0], event[2]
+                        else:
+                            yield END_NS, event[1][0], event[2]
+                    yield TEXT, '\n', pos
+                    for line in lines:
+                        for event in stack:
+                            yield event
+                        yield kind, line.rstrip('\n'), pos
+                        if line.endswith('\n'):
+                            for event in reversed(stack):
+                                if event[0] is START:
+                                    yield END, event[1][0], event[2]
+                                else:
+                                    yield END_NS, event[1][0], event[2]
+                            yield TEXT, '\n', pos
+                else:
+                    for e in stack:
+                        yield e
+                    yield kind, data, pos
+                    for event in reversed(stack):
+                        if event[0] is START:
+                            yield END, event[1][0], event[2]
+                        else:
+                            yield END_NS, event[1][0], event[2]
+            else:
+                if kind is START or kind is START_NS:
+                    stack.append((kind, data, pos))
+                elif kind is END or kind is END_NS:
+                    stack.pop()
+                else:
+                    yield kind, data, pos
 
-        # Find all tags closed on this line
-        for ctag in close_tag_re.finditer(line):
-            for otag in open_tags:
-                if otag.group(1) == ctag.group(1):
-                    open_tags.remove(otag)
-                    break
+    buf = []
+    for kind, data, pos in _generate():
+        if kind is TEXT and data == '\n':
+            yield Stream(buf[:])
+            del buf[:]
+        else:
+            if kind is TEXT:
+                data = space_re.sub(pad_spaces, data)
+            buf.append((kind, data, pos))
+    if buf:
+        yield Stream(buf[:])
 
-        # Close all tags still open at the end of line, they'll get reopened at
-        # the beginning of the next line
-        for tag in open_tags:
-            line += '</%s>' % tag.group(1)
 
-        yield line
-
-
 # -- Default annotators
 
 class LineNumberAnnotator(Component):
@@ -664,8 +687,9 @@
         return 'lineno', 'Line', 'Line numbers'
 
     def annotate_line(self, number, content):
-        return '<th id="L%s"><a href="#L%s">%s</a></th>' % (number, number,
-                                                            number)
+        return tag.th(id='L%s' % number)(
+            tag.a(number, href='#L%s' % number)
+        )
 
 
 # -- Default renderers
@@ -696,8 +720,8 @@
 
         self.env.log.debug("Using default plain text mimeviewer")
         content = content_to_unicode(self.env, content, mimetype)
-        for line in content.splitlines():
-            yield escape(line)
+        for line in content.splitlines(True):
+            yield TEXT, line, (None, -1, -1)
 
 
 class ImageRenderer(Component):
Index: trac/mimeview/tests/api.py
===================================================================
--- trac/mimeview/tests/api.py	(revision 4335)
+++ trac/mimeview/tests/api.py	(working copy)
@@ -13,8 +13,9 @@
 
 import unittest
 
-from trac.mimeview.api import get_mimetype, _html_splitlines
+from trac.mimeview.api import get_mimetype
 
+
 class GetMimeTypeTestCase(unittest.TestCase):
 
     def test_from_suffix_using_MIME_MAP(self):
@@ -52,39 +53,9 @@
                          get_mimetype('xxx', "abc\0xyz"))
         
     
-class MimeviewTestCase(unittest.TestCase):
-
-    def test_html_splitlines_without_markup(self):
-        lines = ['line 1', 'line 2']
-        self.assertEqual(lines, list(_html_splitlines(lines)))
-
-    def test_html_splitlines_with_markup(self):
-        lines = ['<p><b>Hi', 'How are you</b></p>']
-        result = list(_html_splitlines(lines))
-        self.assertEqual('<p><b>Hi</b></p>', result[0])
-        self.assertEqual('<p><b>How are you</b></p>', result[1])
-
-    def test_html_splitlines_with_multiline(self):
-        """
-        Regression test for http://trac.edgewall.org/ticket/2655
-        """
-        lines = ['<span class="p_tripledouble">"""',
-                'a <a href="http://google.com">http://google.com</a>/',
-                'Test', 'Test', '"""</span>']
-        result = list(_html_splitlines(lines))
-        self.assertEqual('<span class="p_tripledouble">"""</span>', result[0])
-        self.assertEqual('<span class="p_tripledouble">a '
-                         '<a href="http://google.com">http://google.com</a>/'
-                         '</span>', result[1])
-        self.assertEqual('<span class="p_tripledouble">Test</span>', result[2])
-        self.assertEqual('<span class="p_tripledouble">Test</span>', result[3])
-        self.assertEqual('<span class="p_tripledouble">"""</span>', result[4])
-
-
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(GetMimeTypeTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(MimeviewTestCase, 'test'))
     return suite
 
 if __name__ == '__main__':
Index: trac/web/chrome.py
===================================================================
--- trac/web/chrome.py	(revision 4335)
+++ trac/web/chrome.py	(working copy)
@@ -70,7 +70,9 @@
         href = Href(req.chrome['htdocs_location'])
         filename = filename[7:]
     else:
-        href = Href(req.base_path).chrome
+        href = Href(req.base_path)
+        if not filename.startswith('/'):
+            href = href.chrome
     add_link(req, 'stylesheet', href(filename), mimetype=mimetype)
 
 def add_script(req, filename, mimetype='text/javascript'):
@@ -83,7 +85,9 @@
         href = Href(req.chrome['htdocs_location'])
         path = filename[7:]
     else:
-        href = Href(req.base_path).chrome
+        href = Href(req.base_path)
+        if not filename.startswith('/'):
+            href = href.chrome
         path = filename
     script = {'href': href(path), 'type': mimetype}
 

