Rework `be html` to use Jinja2 templates.
authorW. Trevor King <wking@drexel.edu>
Sun, 17 Apr 2011 06:41:07 +0000 (02:41 -0400)
committerW. Trevor King <wking@drexel.edu>
Sun, 17 Apr 2011 06:41:07 +0000 (02:41 -0400)
libbe/command/html.py

index bb5b5542f6ccd22fe98eb8627f697ff3323a969f..c403fcb1085ee7486a89b165fe577d013d710c5f 100644 (file)
@@ -26,6 +26,8 @@ import string
 import time
 import xml.sax.saxutils
 
+from jinja2 import Environment, FileSystemLoader, DictLoader, ChoiceLoader
+
 import libbe
 import libbe.command
 import libbe.command.util
@@ -82,12 +84,12 @@ class HTML (libbe.command.Command):
                     help='Set the bug repository title (%default)',
                     arg=libbe.command.Argument(
                         name='title', metavar='STRING',
-                        default='BugsEverywhere Issue Tracker')),
+                        default='Bugs Everywhere Issue Tracker')),
                 libbe.command.Option(name='index-header',
                     help='Set the index page headers (%default)',
                     arg=libbe.command.Argument(
                         name='index-header', metavar='STRING',
-                        default='BugsEverywhere Bug List')),
+                        default='Bugs Everywhere Bug List')),
                 libbe.command.Option(name='export-template', short_name='e',
                     help='Export the default template and exit.'),
                 libbe.command.Option(name='export-template-dir', short_name='d',
@@ -112,9 +114,9 @@ class HTML (libbe.command.Command):
             bugdir = self._get_bugdir()
             bugdir.load_all_bugs()
         html_gen = HTMLGen(bugdir,
-                           template=params['template-dir'],
+                           template_dir=params['template-dir'],
                            title=params['title'],
-                           index_header=params['index-header'],
+                           header=params['index-header'],
                            min_id_length=params['min-id-length'],
                            verbose=params['verbose'],
                            stdout=self.stdout)
@@ -132,28 +134,22 @@ directory.
 Html = HTML # alias for libbe.command.base.get_command_class()
 
 class HTMLGen (object):
-    def __init__(self, bd, template=None,
-                 title="Site Title", index_header="Index Header",
+    def __init__(self, bd, template_dir=None,
+                 title="Site Title", header="Header",
                  min_id_length=-1,
                  verbose=False, encoding=None, stdout=None,
                  ):
         self.generation_time = time.ctime()
         self.bd = bd
-        if template == None:
-            self.template = "default"
-        else:
-            self.template = os.path.abspath(os.path.expanduser(template))
         self.title = title
-        self.index_header = index_header
+        self.header = header
         self.verbose = verbose
         self.stdout = stdout
         if encoding != None:
             self.encoding = encoding
         else:
             self.encoding = libbe.util.encoding.get_filesystem_encoding()
-        self._load_default_templates()
-        if template != None:
-            self._load_user_templates()
+        self._load_templates(template_dir)
         self.min_id_length = min_id_length
 
     def run(self, out_dir):
@@ -179,21 +175,24 @@ class HTMLGen (object):
         self._create_output_directories(out_dir)
         self._write_css_file()
         for b in bugs:
-            if b.active:
+            if b.severity == 'target':
+                up_link = '../../index_target.html'
+            elif b.active:
                 up_link = '../../index.html'
             else:
-                up_link = '../../index_inactive.html'
-                
-            self._write_bug_file(b, up_link)
+                up_link = '../../index_inactive.html'                
+            self._write_bug_file(
+                b, title=self.title, header=self.header,
+                up_link=up_link)
         self._write_index_file(
             bugs_active, title=self.title,
-            index_header=self.index_header, bug_type='active')
+            header=self.header, bug_type='active')
         self._write_index_file(
             bugs_inactive, title=self.title,
-            index_header=self.index_header, bug_type='inactive')
+            header=self.header, bug_type='inactive')
         self._write_index_file(
             bugs_target, title=self.title,
-            index_header=self.index_header, bug_type='target')
+            header=self.header, bug_type='target')
 
     def _truncated_bug_id(self, bug):
         return libbe.util.id._truncate(
@@ -217,10 +216,10 @@ class HTMLGen (object):
             print >> self.stdout, 'Writing css file'
         assert hasattr(self, 'out_dir'), \
             'Must run after ._create_output_directories()'
-        self._write_file(self.css_file,
-                         [self.out_dir,'style.css'])
+        template = self.template.get_template('style.css')
+        self._write_file(template.render(), [self.out_dir, 'style.css'])
 
-    def _write_bug_file(self, bug, up_link):
+    def _write_bug_file(self, bug, title, header, up_link):
         if self.verbose:
             print >> self.stdout, '\tCreating bug file for %s' % bug.id.user()
         assert hasattr(self, 'out_dir_bugs'), \
@@ -229,96 +228,77 @@ class HTMLGen (object):
             
         if bug.active == True:
             index_type = 'Active'
-        else :
+        else:
             index_type = 'Inactive'
         if bug.severity == 'target':
             index_type = 'Target'
                 
         bug.load_comments(load_full=True)
-        comment_entries = self._generate_bug_comment_entries(bug)
+        bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
         dirname = self._truncated_bug_id(bug)
         fullpath = os.path.join(self.out_dir_bugs, dirname, 'index.html')
-        template_info = {'title':self.title,
-                         'charset':self.encoding,
-                         'up_link':up_link,
-                         'shortname':bug.id.user(),
-                         'comment_entries':comment_entries,
-                         'generation_time':self.generation_time,
-                         'index_type':index_type}
-        for attr in ['uuid', 'severity', 'status', 'assigned',
-                     'reporter', 'creator', 'time_string', 'summary']:
-            template_info[attr] = self._escape(getattr(bug, attr))
+        template_info = {
+            'title': title,
+            'charset': self.encoding,
+            'stylesheet': '../../style.css',
+            'header': header,
+            'backlinks': self.template.get_template('bug_backlinks.html'),
+            'up_link': up_link,
+            'index_type': index_type,
+            'bug': bug,
+            'comment_entry': self.template.get_template(
+                'bug_comment_entry.html'),
+            'comments': [(depth,comment) for depth,comment
+                         in bug.comment_root.thread(flatten=False)],
+            'comment_dir': self._truncated_comment_id,
+            'format_body': self._format_comment_body,
+            'div_close': _DivCloser(),
+            'generation_time': self.generation_time,
+            }
         fulldir = os.path.join(self.out_dir_bugs, dirname)
         if not os.path.exists(fulldir):
             os.mkdir(fulldir)
-        self._write_file(self.bug_file % template_info, [fullpath])
+        template = self.template.get_template('bug.html')
+        self._write_file(template.render(template_info), [fullpath])
 
-    def _generate_bug_comment_entries(self, bug):
-        assert hasattr(self, 'out_dir_bugs'), \
-            'Must run after ._create_output_directories()'
+    def _write_index_file(self, bugs, title, header, bug_type='active'):
+        if self.verbose:
+            print >> self.stdout, 'Writing %s index file for %d bugs' % (bug_type, len(bugs))
+        assert hasattr(self, 'out_dir'), 'Must run after ._create_output_directories()'
 
-        stack = []
-        comment_entries = []
-        bug.comment_root.sort(cmp=libbe.comment.cmp_time, reverse=True)
-        for depth,comment in bug.comment_root.thread(flatten=False):
-            while len(stack) > depth:
-                # pop non-parents off the stack
-                stack.pop(-1)
-                # close non-parent <div class="comment...
-                comment_entries.append('</div>\n')
-            assert len(stack) == depth
-            stack.append(comment)
-            template_info = {
-                'shortname': comment.id.user(),
-                'truncated_id': self._truncated_comment_id(comment)}
-            if depth == 0:
-                comment_entries.append('<div class="comment root">')
-            else:
-                comment_entries.append(
-                    '<div class="comment" id="%s">'
-                    % template_info['truncated_id'])
-            for attr in ['uuid', 'author', 'date', 'body']:
-                value = getattr(comment, attr)
-                if attr == 'body':
-                    link_long_ids = False
-                    save_body = False
-                    if comment.content_type == 'text/html':
-                        link_long_ids = True
-                    elif comment.content_type.startswith('text/'):
-                        value = '<pre>\n'+self._escape(value)+'\n</pre>'
-                        link_long_ids = True
-                    elif comment.content_type.startswith('image/'):
-                        save_body = True
-                        value = '<img src="./%s/%s" />' \
-                            % (self._truncated_bug_id(bug),
-                               self._truncated_comment_id(comment))
-                    else:
-                        save_body = True
-                        value = '<a href="./%s/%s">Link to %s file</a>.' \
-                            % (self._truncated_bug_id(bug),
-                               self._truncated_comment_id(comment),
-                               comment.content_type)
-                    if link_long_ids == True:
-                        value = self._long_to_linked_user(value)
-                    if save_body == True:
-                        per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
-                        if not os.path.exists(per_bug_dir):
-                            os.mkdir(per_bug_dir)
-                        comment_path = os.path.join(per_bug_dir, comment.uuid)
-                        self._write_file(
-                            '<Files %s>\n  ForceType %s\n</Files>' \
-                                % (comment.uuid, comment.content_type),
-                            [per_bug_dir, '.htaccess'], mode='a')
-                        self._write_file(comment.body,
-                            [per_bug_dir, comment.uuid], mode='wb')
-                else:
-                    value = self._escape(value)
-                template_info[attr] = value
-            comment_entries.append(self.bug_comment_entry % template_info)
-        while len(stack) > 0:
-            stack.pop(-1)
-            comment_entries.append('</div>\n') # close every remaining <div class='comment...
-        return '\n'.join(comment_entries)
+        if bug_type == 'active':
+            filename = 'index.html'
+        elif bug_type == 'inactive':
+            filename = 'index_inactive.html'
+        elif bug_type == 'target':
+            filename = 'index_by_target.html'
+        else:
+            raise ValueError('unrecognized bug_type: "%s"' % bug_type)
+
+        template_info = {
+            'title': title,
+            'charset': self.encoding,
+            'stylesheet': 'style.css',
+            'header': header,
+            'active_class': 'tab nsel',
+            'inactive_class': 'tab nsel',
+            'target_class': 'tab nsel',
+            'bugs': bugs,
+            'bug_entry': self.template.get_template('index_bug_entry.html'),
+            'bug_dir': self._truncated_bug_id,
+            'generation_time': self.generation_time,
+            }
+        template_info['%s_class' % bug_type] = 'tab sel'
+        if bug_type == 'target':
+            template = self.template.get_template('target_index.html')
+            template_info['targets'] = [
+                (target, sorted(libbe.command.depend.get_blocked_by(
+                            self.bd, target)))
+                for target in bugs]
+        else:
+            template = self.template.get_template('standard_index.html')           
+        self._write_file(
+            template.render(template_info)+'\n', [self.out_dir,filename])
 
     def _long_to_linked_user(self, text):
         """
@@ -382,91 +362,46 @@ class HTMLGen (object):
         raise Exception('Invalid id type %s for "%s"'
                         % (p['type'], long_id))
 
-    def _write_index_file(self, bugs, title, index_header, bug_type='active'):
-        if self.verbose:
-            print >> self.stdout, 'Writing %s index file for %d bugs' % (bug_type, len(bugs))
-        assert hasattr(self, 'out_dir'), 'Must run after ._create_output_directories()'
-        esc = self._escape
-        if (bug_type == 'target'):
-            bug_entries = self._generate_index_bug_entries_target(bugs)
+    def _format_comment_body(self, bug, comment):
+        link_long_ids = False
+        save_body = False
+        value = comment.body
+        if comment.content_type == 'text/html':
+            link_long_ids = True
+        elif comment.content_type.startswith('text/'):
+            value = '<pre>\n'+self._escape(value)+'\n</pre>'
+            link_long_ids = True
+        elif comment.content_type.startswith('image/'):
+            save_body = True
+            value = '<img src="./%s/%s" />' % (
+                self._truncated_bug_id(bug),
+                self._truncated_comment_id(comment))
         else:
-            bug_entries = self._generate_index_bug_entries(bugs)
-
-        if bug_type == 'active':
-            filename = 'index.html'
-        elif bug_type == 'inactive':
-            filename = 'index_inactive.html'
-        elif bug_type == 'target':
-            filename = 'index_by_target.html'
-        else:
-            raise Exception, 'Unrecognized bug_type: "%s"' % bug_type
-        template_info = {'title':title,
-                         'index_header':index_header,
-                         'charset':self.encoding,
-                         'active_class':'tab sel',
-                         'inactive_class':'tab nsel',
-                         'target_class':'tab nsel',
-                         'bug_entries':bug_entries,
-                         'generation_time':self.generation_time}
-        if bug_type == 'inactive':
-            template_info['active_class'] = 'tab nsel'
-            template_info['inactive_class'] = 'tab sel'
-        if bug_type == 'target':
-            template_info['active_class'] = 'tab nsel'
-            template_info['target_class'] = 'tab sel'
-        self._write_file(self.index_file % template_info,
-                         [self.out_dir, filename])
-
-    def _generate_index_bug_entries_target(self, targets):
-        
-        target_entries = []
-        for target in targets:
-            bug_entries = []
-            template_info_list = {'target':target.summary, 'bug_entries': '', 'status': target.status}
-            blocker = libbe.command.depend.get_blocked_by(self.bd, target)
-            for bug in blocker:
-                if self.verbose:
-                    print >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
-                template_info = {'shortname':bug.id.user()}
-                for attr in ['uuid', 'severity', 'status', 'assigned',
-                    'reporter', 'creator', 'time_string', 'summary']:
-                    template_info[attr] = self._escape(getattr(bug, attr))
-                template_info['dir'] = self._truncated_bug_id(bug)
-                bug_entries.append(self.index_bug_entry % template_info)
-            template_info_list['bug_entries'] = '\n'.join(bug_entries)
-            target_entries.append(self.target_bug_list % template_info_list)
-        return '\n'.join(target_entries)
-                
-    def _generate_index_bug_entries(self, bugs):
-        bug_entries = []
-        template_info_list = {'bug_entries': ''}
-        for bug in bugs:
-            if self.verbose:
-                print >> self.stdout, '\tCreating bug entry for %s' % bug.id.user()
-            template_info = {'shortname':bug.id.user()}
-            for attr in ['uuid', 'severity', 'status', 'assigned',
-                         'reporter', 'creator', 'time_string', 'summary']:
-                template_info[attr] = self._escape(getattr(bug, attr))
-            template_info['dir'] = self._truncated_bug_id(bug)
-            bug_entries.append(self.index_bug_entry % template_info)
-        template_info_list['bug_entries'] = '\n'.join(bug_entries)
-        return self.bug_list % template_info_list 
+            save_body = True
+            value = '<a href="./%s/%s">Link to %s file</a>.' % (
+                self._truncated_bug_id(bug),
+                self._truncated_comment_id(comment),
+                comment.content_type)
+        if link_long_ids == True:
+            value = self._long_to_linked_user(value)
+        if save_body == True:
+            per_bug_dir = os.path.join(self.out_dir_bugs, bug.uuid)
+            if not os.path.exists(per_bug_dir):
+                os.mkdir(per_bug_dir)
+            comment_path = os.path.join(per_bug_dir, comment.uuid)
+            self._write_file(
+                '<Files %s>\n  ForceType %s\n</Files>' \
+                    % (comment.uuid, comment.content_type),
+                [per_bug_dir, '.htaccess'], mode='a')
+            self._write_file(comment.body,
+                             [per_bug_dir, comment.uuid], mode='wb')
+        return value
 
     def _escape(self, string):
         if string == None:
             return ''
         return xml.sax.saxutils.escape(string)
 
-    def _load_user_templates(self):
-        for filename,attr in [('style.css','css_file'),
-                              ('index_file.tpl','index_file'),
-                              ('index_bug_entry.tpl','index_bug_entry'),
-                              ('bug_file.tpl','bug_file'),
-                              ('bug_comment_entry.tpl','bug_comment_entry')]:
-            fullpath = os.path.join(self.template, filename)
-            if os.path.exists(fullpath):
-                setattr(self, attr, self._read_file([fullpath]))
-
     def _make_dir(self, dir_path):
         dir_path = os.path.abspath(os.path.expanduser(dir_path))
         if not os.path.exists(dir_path):
@@ -489,388 +424,425 @@ class HTMLGen (object):
         if self.verbose:
             print >> self.stdout, 'Creating output directories'
         self.out_dir = self._make_dir(out_dir)
-        if self.verbose:
-            print >> self.stdout, 'Creating css file'
-        self._write_css_file()
-        if self.verbose:
-            print >> self.stdout, 'Creating index_file.tpl file'
-        self._write_file(self.index_file,
-                         [self.out_dir, 'index_file.tpl'])
-        if self.verbose:
-            print >> self.stdout, 'Creating index_bug_entry.tpl file'
-        self._write_file(self.index_bug_entry,
-                         [self.out_dir, 'index_bug_entry.tpl'])
-        if self.verbose:
-            print >> self.stdout, 'Creating bug_file.tpl file'
-        self._write_file(self.bug_file,
-                         [self.out_dir, 'bug_file.tpl'])
-        if self.verbose:
-            print >> self.stdout, 'Creating bug_comment_entry.tpl file'
-        self._write_file(self.bug_comment_entry,
-                         [self.out_dir, 'bug_comment_entry.tpl'])
-
-    def _load_default_templates(self):
-        self.css_file = """
-            body {
-              font-family: "lucida grande", "sans serif";
-              font-size: 14px;
-              color: #333;
-              width: auto;
-              margin: auto;
-            }
-
-            div.main {
-              padding: 20px;
-              margin: auto;
-              padding-top: 0;
-              margin-top: 1em;
-              background-color: #fcfcfc;
-              -moz-border-radius: 10px;
-              
-            }
-
-            div.footer {
-              font-size: small;
-              padding-left: 20px;
-              padding-right: 20px;
-              padding-top: 5px;
-              padding-bottom: 5px;
-              margin: auto;
-              background: #305275;
-              color: #fffee7;
-              -moz-border-radius: 10px;
-            }
-            
-            div.header {
-                font-size: xx-large;
-                padding-left: 20px;
-                padding-right: 20px;
-                padding-top: 10px;
-                font-weight:bold;
-                padding-bottom: 10px;
-                background: #305275;
-                color: #fffee7;
-                -moz-border-radius: 10px;
-            }
-            
-            div.target_name {
-                border: 1px solid;
-                border-color: #305275;
-                background-color: #305275;
-                color: #fff;
-                width: auto%;
-                -moz-border-radius-topleft: 8px;
-                -moz-border-radius-topright: 8px;
-                padding-left: 5px;
-                padding-right: 5px;
-            }
-            
-            table {
-              border-style: solid;
-              border: 1px #c3d9ff;
-              border-spacing: 0px 0px;
-              width: auto;
-              padding: 0px;
-              
-              }
-
-            tb { border: 1px; }
-
-            tr {
-              vertical-align: top;
-              border: 1px #c3d9ff;
-              border-style: dotted;
-              width: auto;
-              padding: 0px;
-            }
-            
-            th {
-                border-width: 1px;
-                border-style: solid;
-                border-color: #c3d9ff;
-                border-collapse: collapse;
-                padding-left: 5px;
-                padding-right: 5px;
-            }
-            
-            
-            td {
-                border-width: 1px;
-                border-color: #c3d9ff;
-                border-collapse: collapse;
-                padding-left: 5px;
-                padding-right: 5px;
-                width: auto%;
-            }
-            
-            img { border-style: none; }
-
-            h1 {
-              padding: 0.5em;
-              background-color: #305275;
-              margin-top: 0;
-              margin-bottom: 0;
-              color: #fff;
-              margin-left: -20px;
-              margin-right: -20px;
-            }
-
-            ul {
-              list-style-type: none;
-              padding: 0;
-            }
-
-            p { width: auto; }
-
-            a, a:visited {
-              background: inherit;
-              text-decoration: none;
-            }
-
-            a { color: #003d41; }
-            a:visited { color: #553d41; }
-            .footer a { color: #508d91; }
-
-            /* bug index pages */
-
-            td.tab {
-              padding-right: 1em;
-              padding-left: 1em;
-            }
-
-            td.sel.tab {
-                background-color: #c3d9ff ;
-                border: 1px solid #c3d9ff;
-                font-weight:bold;    
-                -moz-border-radius-topleft: 15px;
-                -moz-border-radius-topright: 15px;
-            }
-
-            td.nsel.tab { 
-                border: 1px solid #c3d9ff;
-                font-weight:bold;    
-                -moz-border-radius-topleft: 5px;
-                -moz-border-radius-topright: 5px;
-            }
-
-            table.bug_list {
-                border-width: 1px;
-                border-style: solid;
-                border-color: #c3d9ff;
-                padding: 0px;
-                width: 100%;            
-                border: 1px solid #c3d9ff;
-            }
-            
-            table.target_list {
-                border-width: 1px;
-                border-style: solid;
-                border-collapse: collapse;
-                border-color: #c3d9ff;
-                padding: 0px;
-                width: 100%;
-                margin-bottom: 10px;
-            }
-
-            table.target_list.td {
-                border-width: 1px;
-            }
-
-            tr.wishlist { background-color: #DCFAFF;}
-            tr.wishlist:hover { background-color: #C2DCE1; }
-            
-            tr.minor { background-color: #FFFFA6; }
-            tr.minor:hover { background-color: #E6E696; }
-            
-            tr.serious { background-color: #FF9077;}
-            tr.serious:hover { background-color: #E6826B; }
-            
-            tr.critical { background-color: #FF752A; }
-            tr.critical:hover { background-color: #D63905;}
-            
-            tr.fatal { background-color: #FF3300;}
-            tr.fatal:hover { background-color: #D60000;}
-            
-            td.uuid { width: 5%; border-style: dotted;}
-            td.status { width: 5%; border-style: dotted;}
-            td.severity { width: 5%; border-style: dotted;}
-            td.summary { border-style: dotted;}
-            td.date { width: 25%; border-style: dotted;}
-            
-            /* bug detail pages */
-
-            td.bug_detail_label { text-align: right; border: none;}
-            td.bug_detail { border: none;}
-            td.bug_comment_label { text-align: right; vertical-align: top; }
-            td.bug_comment { }
-
-            div.comment {
-              padding: 20px;
-              padding-top: 20px;
-              margin: auto;
-              margin-top: 0;
+        for filename,text in self.template_dict.iteritems():
+            if self.verbose:
+                print >> self.stdout, 'Creating %s file'
+            self._write_file(text, [self.out_dir, filename])
+
+    def _load_templates(self, template_dir=None):
+        if template_dir is not None:
+            template_dir = os.path.abspath(os.path.expanduser(template_dir))
+
+        self.template_dict = {
+##
+            'style.css':
+"""body {
+  font-family: "lucida grande", "sans serif";
+  font-size: 14px;
+  color: #333;
+  width: auto;
+  margin: auto;
+}
+
+div.main {
+  padding: 20px;
+  margin: auto;
+  padding-top: 0;
+  margin-top: 1em;
+  background-color: #fcfcfc;
+  -moz-border-radius: 10px;
+  
+}
+
+div.footer {
+  font-size: small;
+  padding-left: 20px;
+  padding-right: 20px;
+  padding-top: 5px;
+  padding-bottom: 5px;
+  margin: auto;
+  background: #305275;
+  color: #fffee7;
+  -moz-border-radius: 10px;
+}
+
+div.header {
+    font-size: xx-large;
+    padding-left: 20px;
+    padding-right: 20px;
+    padding-top: 10px;
+    font-weight:bold;
+    padding-bottom: 10px;
+    background: #305275;
+    color: #fffee7;
+    -moz-border-radius: 10px;
+}
+
+th.target_name {
+    text-align:left;
+    border: 1px solid;
+    border-color: #305275;
+    background-color: #305275;
+    color: #fff;
+    width: auto%;
+    -moz-border-radius-topleft: 8px;
+    -moz-border-radius-topright: 8px;
+    padding-left: 5px;
+    padding-right: 5px;
+}
+
+table {
+  border-style: solid;
+  border: 1px #c3d9ff;
+  border-spacing: 0px 0px;
+  width: auto;
+  padding: 0px;
+  
+  }
+
+tb { border: 1px; }
+
+tr {
+  vertical-align: top;
+  border: 1px #c3d9ff;
+  border-style: dotted;
+  width: auto;
+  padding: 0px;
+}
+
+th {
+    border-width: 1px;
+    border-style: solid;
+    border-color: #c3d9ff;
+    border-collapse: collapse;
+    padding-left: 5px;
+    padding-right: 5px;
+}
+
+
+td {
+    border-width: 1px;
+    border-color: #c3d9ff;
+    border-collapse: collapse;
+    padding-left: 5px;
+    padding-right: 5px;
+    width: auto%;
+}
+
+img { border-style: none; }
+
+ul {
+  list-style-type: none;
+  padding: 0;
+}
+
+p { width: auto; }
+
+p.backlink {
+  width: auto;
+  font-weight: bold;
+}
+
+a {
+  background: inherit;
+  text-decoration: none;
+}
+
+a { color: #553d41; }
+a:hover { color: #003d41; }
+a:visited { color: #305275; }
+.footer a { color: #508d91; }
+
+/* bug index pages */
+
+td.tab {
+  padding-right: 1em;
+  padding-left: 1em;
+}
+
+td.sel.tab {
+    background-color: #c3d9ff ;
+    border: 1px solid #c3d9ff;
+    font-weight:bold;    
+    -moz-border-radius-topleft: 15px;
+    -moz-border-radius-topright: 15px;
+}
+
+td.nsel.tab { 
+    border: 1px solid #c3d9ff;
+    font-weight:bold;    
+    -moz-border-radius-topleft: 5px;
+    -moz-border-radius-topright: 5px;
+}
+
+table.bug_list {
+    border-width: 1px;
+    border-style: solid;
+    border-color: #c3d9ff;
+    padding: 0px;
+    width: 100%;            
+    border: 1px solid #c3d9ff;
+}
+
+table.target_list {
+    padding: 0px;
+    width: 100%;
+    margin-bottom: 10px;
+}
+
+table.target_list.td {
+    border-width: 1px;
+}
+
+tr.wishlist { background-color: #DCFAFF;}
+tr.wishlist:hover { background-color: #C2DCE1; }
+
+tr.minor { background-color: #FFFFA6; }
+tr.minor:hover { background-color: #E6E696; }
+
+tr.serious { background-color: #FF9077;}
+tr.serious:hover { background-color: #E6826B; }
+
+tr.critical { background-color: #FF752A; }
+tr.critical:hover { background-color: #D63905;}
+
+tr.fatal { background-color: #FF3300;}
+tr.fatal:hover { background-color: #D60000;}
+
+td.uuid { width: 5%; border-style: dotted;}
+td.status { width: 5%; border-style: dotted;}
+td.severity { width: 5%; border-style: dotted;}
+td.summary { border-style: dotted;}
+td.date { width: 25%; border-style: dotted;}
+
+/* bug detail pages */
+
+td.bug_detail_label { text-align: right; border: none;}
+td.bug_detail { border: none;}
+td.bug_comment_label { text-align: right; vertical-align: top; }
+td.bug_comment { }
+
+div.comment {
+  padding: 20px;
+  padding-top: 20px;
+  margin: auto;
+  margin-top: 0;
+}
+
+div.root.comment {
+  padding: 0px;
+  /* padding-top: 0px; */
+  padding-bottom: 20px;
+}
+""",
+##
+            'base.html':
+"""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <head>
+    <title>{{ title }}</title>
+    <meta http-equiv="Content-Type" content="text/html; charset={{ charset }}" />
+    <link rel="stylesheet" href="{{ stylesheet }}" type="text/css" />
+  </head>
+  <body>
+    <div class="header">{{ header }}</div>
+    <div class="main">
+      {% block content %}{% endblock %}
+    </div>
+    <div class="footer">
+      <p>Generated by <a href="http://www.bugseverywhere.org/">
+      Bugs Everywhere</a> on {{ generation_time }}</p>
+      <p>
+        <a href="http://validator.w3.org/check?uri=referer">
+          Validate XHTML</a>&nbsp;|&nbsp;
+        <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">
+          Validate CSS</a>
+      </p>
+    </div>
+  </body>
+</html>
+""",
+            'index.html':
+"""{% extends "base.html" %}
+
+{% block content %}
+<table>
+  <tbody>
+    <tr>
+      <td class="{{ active_class }}"><a href="index.html">Active Bugs</a></td>
+      <td class="{{ inactive_class }}"><a href="index_inactive.html">Inactive Bugs</a></td>
+      <td class="{{ target_class }}"><a href="index_by_target.html">Divided by target</a></td>
+    </tr>
+  </tbody>
+</table>
+{% if bugs %}
+{% block bug_table %}{% endblock %}
+{% else %}
+<p>No bugs.</p>
+{% endif %}
+{% endblock %}
+""",
+##
+            'standard_index.html':
+"""{% extends "index.html" %}
+
+{% block bug_table %}
+<table class="bug_list">
+  <thead>
+    <tr>
+      <th>UUID</th>
+      <th>Status</th>
+      <th>Severity</th>
+      <th>Summary</th>
+      <th>Date</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for bug in bugs %}
+    {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug)}) }}
+    {% endfor %}
+  <tbody>
+</table>
+{% endblock %}
+""",
+##
+        'target_index.html':
+"""{% extends "index.html" %}
+
+{% block bug_table %}
+{% for target,bugs in targets %}
+<table class="target_list">
+  <thead>
+    <tr>
+      <th class="target_name" colspan="5">
+        Target: {{ target.summary|e }} ({{ target.status|e }})
+      </th>
+    </tr>
+    <tr>
+      <th>UUID</th>
+      <th>Status</th>
+      <th>Severity</th>
+      <th>Summary</th>
+      <th>Date</th>
+    </tr>
+  </thead>
+  <tbody>
+    {% for bug in bugs %}
+    {{ bug_entry.render({'bug':bug, 'dir':bug_dir(bug)}) }}
+    {% endfor %}
+  </tbody>
+</table>
+{% endfor %}
+{% endblock %}
+""",
+##
+            'index_bug_entry.html':
+"""<tr class="{{ bug.severity }}">
+  <td class="uuid"><a href="bugs/{{ dir }}/index.html">{{ bug.id.user()|e }}</a></td>
+  <td class="status"><a href="bugs/{{ dir }}/index.html">{{ bug.status|e }}</a></td>
+  <td class="severity"><a href="bugs/{{ dir }}/index.html">{{ bug.severity|e }}</a></td>
+  <td class="summary"><a href="bugs/{{ dir }}/index.html">{{ bug.summary|e }}</a></td>
+  <td class="date"><a href="bugs/{{ dir }}/index.html">{{ bug.time_string or ''|e }}</a></td>
+</tr>
+""",
+##
+        'bug.html':
+"""{% extends "base.html" %}
+
+{% block content %}
+{{ backlinks.render({'up_link': up_link, 'index_type':index_type}) }}
+<h1>Bug: {{ bug.id.user()|e }}</h1>
+
+<table>
+  <tbody>
+    <tr><td class="bug_detail_label">ID :</td>
+        <td class="bug_detail">{{ bug.uuid|e }}</td></tr>
+    <tr><td class="bug_detail_label">Short name :</td>
+        <td class="bug_detail">{{ bug.id.user()|e }}</td></tr>
+    <tr><td class="bug_detail_label">Status :</td>
+        <td class="bug_detail">{{ bug.status|e }}</td></tr>
+    <tr><td class="bug_detail_label">Severity :</td>
+        <td class="bug_detail">{{ bug.severity|e }}</td></tr>
+    <tr><td class="bug_detail_label">Assigned :</td>
+        <td class="bug_detail">{{ bug.assigned or ''|e }}</td></tr>
+    <tr><td class="bug_detail_label">Reporter :</td>
+        <td class="bug_detail">{{ bug.reporter or ''|e }}</td></tr>
+    <tr><td class="bug_detail_label">Creator :</td>
+        <td class="bug_detail">{{ bug.creator or ''|e }}</td></tr>
+    <tr><td class="bug_detail_label">Created :</td>
+        <td class="bug_detail">{{ bug.time_string or ''|e }}</td></tr>
+    <tr><td class="bug_detail_label">Summary :</td>
+        <td class="bug_detail">{{ bug.summary|e }}</td></tr>
+  </tbody>
+</table>
+
+<hr/>
+
+{% if comments %}
+{% for depth,comment in comments %}
+{% if depth == 0 %}
+<div class="comment root" id="{{ comment_dir(comment) }}">
+{% else %}
+<div class="comment" id="{{ comment_dir(comment) }}">
+{% endif %}
+{{ comment_entry.render({
+       'depth':depth, 'bug': bug, 'comment':comment, 'comment_dir':comment_dir,
+       'format_body': format_body, 'div_close': div_close}) }}
+{{ div_close(depth) }}
+{% endfor %}
+{% if comments[-1][0] > 0 %}
+{{ div_close(0) }}
+{% endif %}
+{% else %}
+<p>No comments.</p>
+{% endif %}
+{{ backlinks.render({'up_link': up_link, 'index_type': index_type}) }}
+{% endblock %}
+""",
+##
+            'bug_backlinks.html':
+"""<p class="backlink"><a href="{{ up_link }}">Back to {{ index_type }} Index</a></p>
+<p class="backlink"><a href="../../index_by_target.html">Back to Target Index</a></p>
+""",
+##
+            'bug_comment_entry.html':
+"""<table>
+  <tdata>
+    <tr>
+      <td class="bug_comment_label">Comment:</td>
+      <td class="bug_comment">
+        --------- Comment ---------<br/>
+        ID: {{ comment.uuid }}<br/>
+        Short name: {{ comment.id.user() }}<br/>
+        From: {{ comment.author or ''|e }}<br/>
+        Date: {{ comment.date or ''|e }}<br/>
+        <br/>
+        {{ format_body(bug, comment) }}
+      </td>
+    </tr>
+  </tdata>
+</table>
+""",
             }
 
-            div.root.comment {
-              padding: 0px;
-              /* padding-top: 0px; */
-              padding-bottom: 20px;
-            }
-       """
-
-        self.index_file = """
-            <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-              "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-            <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-            <head>
-            <title>%(title)s</title>
-            <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
-            <link rel="stylesheet" href="style.css" type="text/css" />
-            </head>
-            <body>
-
-            <div class="header">%(index_header)s</div>
-            <div class="main">
-            <p></p>
-            
-            <table>
-            <tr>
-            <td class="%(active_class)s"><a href="index.html">Active Bugs</a></td>
-            <td class="%(inactive_class)s"><a href="index_inactive.html">Inactive Bugs</a></td>
-            <td class="%(target_class)s"><a href="index_by_target.html">Divided by target</a></td>
-            </tr>
-            </table>
+        loader = DictLoader(self.template_dict)
 
+        if template_dir:
+            file_system_loader = FileSystemLoader(template_dir)
+            loader = ChoiceLoader([file_system_loader, loader])
 
-            %(bug_entries)s
+        self.template = Environment(loader=loader)
 
-            </div>
-            
-            <div class="footer">
-            <p>Generated by <a href="http://www.bugseverywhere.org/">
-            BugsEverywhere</a> on %(generation_time)s</p>
-            <p>
-            <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
-            <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
-            </p>
-            </div>
-
-            </body>
-            </html>
-        """
-        
-        self.index_bug_entry ="""
-            <tr class="%(severity)s">
-              <td class="uuid"><a href="bugs/%(dir)s/index.html">%(shortname)s</a></td>
-              <td class="status"><a href="bugs/%(dir)s/index.html">%(status)s</a></td>
-              <td class="severity"><a href="bugs/%(dir)s/index.html">%(severity)s</a></td>
-              <td class="summary"><a href="bugs/%(dir)s/index.html">%(summary)s</a></td>
-              <td class="date"><a href="bugs/%(dir)s/index.html">%(time_string)s</a></td>
-            </tr>
-        """
-        self.target_bug_list = """
-            <tr>
-            <td>
-            <div class="target_name">
-            Target: %(target)s (%(status)s)
-            </div>
-            <div>
-            <table class="target_list">
-            
-            %(bug_entries)s
-            </table>
-            </div>
-            </td>
-            </tr>
-        """
-        self.bug_list = """
-        <table class="bug_list">
-        
-        %(bug_entries)s
-        
-        </table>
-        
-        """
-        self.bug_file = """
-            <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-              "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-            <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-            <head>
-            <title>%(title)s</title>
-            <meta http-equiv="Content-Type" content="text/html; charset=%(charset)s" />
-            <link rel="stylesheet" href="../../style.css" type="text/css" />
-            </head>
-            <body>
-
-            <div class="header">BugsEverywhere Bug List</div>
-            <div class="main">
-            <h5><a href="%(up_link)s">Back to %(index_type)s Index</a></h5>
-            <h5><a href="../../index_by_target.html">Back to Target Index</a></h5>
-            <h2>Bug: %(shortname)s</h2>
-            <table>
-            <tbody>
-
-            <tr><td class="bug_detail_label">ID :</td>
-                <td class="bug_detail">%(uuid)s</td></tr>
-            <tr><td class="bug_detail_label">Short name :</td>
-                <td class="bug_detail">%(shortname)s</td></tr>
-            <tr><td class="bug_detail_label">Status :</td>
-                <td class="bug_detail">%(status)s</td></tr>
-            <tr><td class="bug_detail_label">Severity :</td>
-                <td class="bug_detail">%(severity)s</td></tr>
-            <tr><td class="bug_detail_label">Assigned :</td>
-                <td class="bug_detail">%(assigned)s</td></tr>
-            <tr><td class="bug_detail_label">Reporter :</td>
-                <td class="bug_detail">%(reporter)s</td></tr>
-            <tr><td class="bug_detail_label">Creator :</td>
-                <td class="bug_detail">%(creator)s</td></tr>
-            <tr><td class="bug_detail_label">Created :</td>
-                <td class="bug_detail">%(time_string)s</td></tr>
-            <tr><td class="bug_detail_label">Summary :</td>
-                <td class="bug_detail">%(summary)s</td></tr>
-            </tbody>
-            </table>
-
-            <hr/>
-
-            %(comment_entries)s
-
-            </div>
-            <h5><a href="%(up_link)s">Back to %(index_type)s Index</a></h5>
-            <h5><a href="../../index_by_target.html">Back to Target Index</a></h5>
-            <div class="footer">
-            <p>Generated by <a href="http://www.bugseverywhere.org/">
-            BugsEverywhere</a> on %(generation_time)s</p>
-            <p>
-            <a href="http://validator.w3.org/check?uri=referer">Validate XHTML</a>&nbsp;|&nbsp;
-            <a href="http://jigsaw.w3.org/css-validator/check?uri=referer">Validate CSS</a>
-            </p>
-            </div>
-
-            </body>
-            </html>
-        """
 
-        self.bug_comment_entry ="""
-            <table>
-            <tr>
-              <td class="bug_comment_label">Comment:</td>
-              <td class="bug_comment">
-            --------- Comment ---------<br/>
-            ID: %(uuid)s<br/>
-            Short name: %(shortname)s<br/>
-            From: %(author)s<br/>
-            Date: %(date)s<br/>
-            <br/>
-            %(body)s
-              </td>
-            </tr>
-            </table>
-        """
+class _DivCloser (object):
+    def __init__(self, depth=0):
+        self.depth = depth
 
-        # strip leading whitespace
-        for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file',
-                     'bug_comment_entry']:
-            value = getattr(self, attr)
-            value = value.replace('\n'+' '*12, '\n')
-            setattr(self, attr, value.strip()+'\n')
+    def __call__(self, depth):
+        ret = []
+        while self.depth >= depth:
+            self.depth -= 1
+            ret.append('</div>')
+        self.depth = depth
+        return '\n'.join(ret)