Restructured becommands/html to make templating more flexible + general cleanups.
authorW. Trevor King <wking@drexel.edu>
Tue, 20 Oct 2009 12:28:35 +0000 (08:28 -0400)
committerW. Trevor King <wking@drexel.edu>
Tue, 20 Oct 2009 12:28:35 +0000 (08:28 -0400)
becommands/html.py

index d94411911d74789245df2af00d01deac0b821e0c..dd7ce6567bca05f7506599b082199cbf7090ea14 100644 (file)
@@ -46,48 +46,32 @@ def execute(args, manipulate_encodings=True):
     complete(options, args, parser)
     cmdutil.default_complete(options, args, parser)
 
-    if len(args) == 0:
-        out_dir = options.outdir
-        template = options.template
-        if template == None:
-            _css_file = "default"
-        else:
-            _css_file = template
-        if options.verbose == True:
-            print "Creating the html output in %s using %s template"%(out_dir, _css_file)
     if len(args) > 0:
-        raise cmdutil.UsageError, "Too many arguments."
+        raise cmdutil.UsageError, 'Too many arguments.'
 
+    template = options.template
     bd = bugdir.BugDir(from_disk=True,
                        manipulate_encodings=manipulate_encodings)
     bd.load_all_bugs()
-    bugs_active = []
-    bugs_inactive = []
-    bugs = [b for b in bd]
-    bugs.sort()
-    bugs_active = [b for b in bugs if b.active == True]
-    bugs_inactive = [b for b in bugs if b.active != True]
-
-    html_gen = BEHTMLGen(bd, template, options.verbose, bd.encoding)
-    html_gen.create_output_directories(out_dir)
-    html_gen.write_css_file()
-    for b in bugs:
-        if b.active:
-            up_link = "../index.html"
-        else:
-            up_link = "../index_inactive.html"
-        html_gen.write_detail_file(b, up_link)
-    html_gen.write_index_file(bugs_active, "active")
-    html_gen.write_index_file(bugs_inactive, "inactive")
+
+    html_gen = HTMLGen(bd, template=options.template, verbose=options.verbose,
+                       title=options.title, )
+    html_gen.run(options.out_dir)
 
 def get_parser():
-    parser = cmdutil.CmdOptionParser("be html [options]")
-    parser.add_option("-o", "--output", metavar="export_dir", dest="outdir",
-        help="Set the output path, default is ./html_export", default="html_export")
-    parser.add_option("-t", "--template-dir", metavar="template", dest="template",
-        help="Use a different template, default is empty", default=None)
-    parser.add_option("-v", "--verbose",  action="store_true", metavar="verbose", dest="verbose",
-        help="Verbose output, default is no", default=False)
+    parser = cmdutil.CmdOptionParser('be html [options]')
+    parser.add_option('-o', '--output', metavar='DIR', dest='out_dir',
+        help='Set the output path (%default)', default='./html_export')
+    parser.add_option('-t', '--template-dir', metavar='DIR', dest='template',
+        help='Use a different template, defaults to internal templates', default=None)
+    parser.add_option('--title', metavar='STRING', dest='title',
+        help='Set the bug repository title (%default)',
+        default='BugsEverywhere Issue Tracker')
+    parser.add_option('--index-header', metavar='STRING', dest='index_header',
+        help='Set the index page headers (%default)',
+        default='BugsEverywhere Bug List')
+    parser.add_option('-v', '--verbose',  action='store_true', metavar='verbose', dest='verbose',
+        help='Verbose output, default is no', default=False)
     return parser
 
 longhelp="""
@@ -103,294 +87,326 @@ def complete(options, args, parser):
         if "--complete" in args:
             raise cmdutil.GetCompletions() # no positional arguments for list
 
-
-def escape(string):
-    if string == None:
-        return ""
-    chars = []
-    for char in xml.sax.saxutils.escape(string):
-        codepoint = ord(char)
-        if codepoint in htmlentitydefs.codepoint2name:
-            char = "&%s;" % htmlentitydefs.codepoint2name[codepoint]
-        chars.append(char)
-    return "".join(chars)
-
-class BEHTMLGen():
-    def __init__(self, bd, template, verbose, encoding):
-        self.index_value = ""
+class HTMLGen (object):
+    def __init__(self, bd, template=None, verbose=False, encoding=None,
+                 title="Site Title", index_header="Index Header",
+                 ):
+        self.generation_time = time.ctime()
         self.bd = bd
         self.verbose = verbose
-        self.encoding = encoding
+        self.title = title
+        self.index_header = index_header
+        if encoding != None:
+            self.encoding = encoding
+        else:
+            self.encoding = self.bd.encoding
         if template == None:
             self.template = "default"
         else:
             self.template = os.path.abspath(os.path.expanduser(template))
+        self._load_default_templates()
 
-        self.css_file = """
-            body {
-            font-family: "lucida grande", "sans serif";
-            color: #333;
-            width: auto;
-            margin: auto;
-            }
-
-
-            div.main {
-            padding: 20px;
-            margin: auto;
-            padding-top: 0;
-            margin-top: 1em;
-            background-color: #fcfcfc;
-            }
-
-            .comment {
-            padding: 20px;
-            margin: auto;
-            padding-top: 20px;
-            margin-top: 0;
-            }
-
-            .commentF {
-            padding: 0px;
-            margin: auto;
-            padding-top: 0px;
-            paddin-bottom: 20px;
-            margin-top: 0;
-            }
-
-            tb {
-            border = 1;
-            }
-
-            .wishlist-row {
-            background-color: #B4FF9B;
-            width: auto;
-            }
-
-            .minor-row {
-            background-color: #FCFF98;
-            width: auto;
-            }
-
-
-            .serious-row {
-            background-color: #FFB648;
-            width: auto;
-            }
+        if template != None:
+            self._load_user_templates()
+
+    def run(self, out_dir):
+        if self.verbose == True:
+            print "Creating the html output in %s using templates in %s" \
+                % (out_dir, self.template)
+
+        bugs_active = []
+        bugs_inactive = []
+        bugs = [b for b in self.bd]
+        bugs.sort()
+        bugs_active = [b for b in bugs if b.active == True]
+        bugs_inactive = [b for b in bugs if b.active != True]
+
+        self._create_output_directories(out_dir)
+        self._write_css_file()
+        for b in bugs:
+            if b.active:
+                up_link = "../index.html"
+            else:
+                up_link = "../index_inactive.html"
+            self._write_bug_file(b, up_link)
+        self._write_index_file(bugs_active, title=self.title,
+                               index_header=self.index_header, bug_type="active")
+        self._write_index_file(bugs_inactive, title=self.title,
+                               index_header=self.index_header, bug_type="inactive")
+
+    def _create_output_directories(self, out_dir):
+        if self.verbose:
+            print "Creating output directories"
+        self.out_dir = os.path.abspath(os.path.expanduser(out_dir))
+        if not os.path.exists(self.out_dir):
+            try:
+                os.mkdir(self.out_dir)
+            except:
+                raise cmdutil.UsageError, "Cannot create output directory '%s'." % self.out_dir
+        self.out_dir_bugs = os.path.join(self.out_dir, "bugs")
+        if not os.path.exists(self.out_dir_bugs):
+            os.mkdir(self.out_dir_bugs)
 
-            .critical-row {
-            background-color: #FF752A;
-            width: auto;
-            }
+    def _write_css_file(self):
+        if self.verbose:
+            print "Writing css file"
+        assert hasattr(self, "out_dir"), "Must run after ._create_output_directories()"
+        f = codecs.open(os.path.join(self.out_dir,"style.css"), "w", self.encoding)
+        f.write(self.css_file)
+        f.close()
 
-            .fatal-row {
-            background-color: #FF3300;
-            width: auto;
-            }
+    def _write_bug_file(self, bug, up_link):
+        if self.verbose:
+            print "\tCreating bug file for %s" % self.bd.bug_shortname(bug)
+        assert hasattr(self, "out_dir_bugs"), "Must run after ._create_output_directories()"
+        esc = self._escape
 
-            .person {
-            font-family: courier;
-            }
+        bug.load_comments(load_full=True)
+        stack = []
+        comment_entries = []
+        for depth,comment in bug.comment_root.thread(flatten=False):
+            while len(stack) > depth:
+                stack.pop(-1)      # pop non-parents off the stack
+                comment_entries.append("</div>\n") # close non-parent <div class="comment...
+            assert len(stack) == depth
+            stack.append(comment)
+            if depth == 0:
+                comment_entries.append('<div class="comment root">')
+            else:
+                comment_entries.append('<div class="comment">')
+            template_info = {}
+            for attr in ['uuid', 'author', 'date', 'body']:
+                value = getattr(comment, attr)
+                if attr == 'body':
+                    if comment.content_type == 'text/html':
+                        pass # no need to escape html...
+                    elif comment.content_type.startswith('text/'):
+                        value = '<pre>\n'+esc(value)+'\n</pre>'
+                    else:
+                        value = "TODO: linkout to %s" % comment.content_type
+                else:
+                    value = esc(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...
 
-            a, a:visited {
-            background: inherit;
-            text-decoration: none;
-            }
+        filename = "%s.html" % bug.uuid
+        fullpath = os.path.join(self.out_dir_bugs, filename)
+        template_info = {'title':self.title,
+                         'charset':self.encoding,
+                         'up_link':up_link,
+                         'shortname':self.bd.bug_shortname(bug),
+                         'comment_entries':'\n'.join(comment_entries),
+                         'generation_time':self.generation_time}
+        for attr in ['uuid', 'severity', 'status', 'assigned', 'target',
+                     'reporter', 'creator', 'time_string', 'summary']:
+            template_info[attr] = esc(getattr(bug, attr))
+        f = codecs.open(fullpath, "w", self.encoding)
+        f.write(self.bug_file % template_info)
+        f.close()
 
-            a {
-            color: #003d41;
-            }
+    def _write_index_file(self, bugs, title, index_header, bug_type="active"):
+        if self.verbose:
+            print "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
 
-            a:visited {
-            color: #553d41;
-            }
+        bug_entries = []
+        for b in bugs:
+            if self.verbose:
+                print "\tCreating bug entry for %s" % self.bd.bug_shortname(b)
+            template_info = {'shortname':self.bd.bug_shortname(b)}
+            for attr in ['uuid', 'severity', 'status', 'assigned', 'target',
+                         'reporter', 'creator', 'time_string', 'summary']:
+                template_info[attr] = esc(getattr(b, attr))
+            bug_entries.append(self.index_bug_entry % template_info)
+
+        if bug_type == "active":
+            filename = "index.html"
+        elif bug_type == "inactive":
+            filename = "index_inactive.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',
+                         'bug_entries':'\n'.join(bug_entries),
+                         'generation_time':self.generation_time}
+        if bug_type == "inactive":
+            template_info['active_class'] = 'tab nsel'
+            template_info['inactive_class'] = 'tab sel'
 
-            ul {
-            list-style-type: none;
-            padding: 0;
-            }
+        f = codecs.open(os.path.join(self.out_dir, filename), "w", self.encoding)
+        f.write(self.index_file % template_info)
+        f.close()
 
-            p {
-            width: auto;
+    def _escape(self, string):
+        if string == None:
+            return ""
+        chars = []
+        for char in string:
+            codepoint = ord(char)
+            if codepoint in htmlentitydefs.codepoint2name:
+                char = "&%s;" % htmlentitydefs.codepoint2name[codepoint]
+            #else: xml.sax.saxutils.escape(char)
+            chars.append(char)
+        return "".join(chars)
+
+    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):
+                f = codecs.open(fullpath, "r", self.encoding)
+                setattr(self, attr, f.read())
+                f.close()
+
+    def _load_default_templates(self):
+        self.css_file = """
+            body {
+              font-family: "lucida grande", "sans serif";
+              color: #333;
+              width: auto;
+              margin: auto;
             }
 
-            .inline-status-image {
-            position: relative;
-            top: 0.2em;
+            div.main {
+              padding: 20px;
+              margin: auto;
+              padding-top: 0;
+              margin-top: 1em;
+              background-color: #fcfcfc;
             }
 
-            .dimmed {
-            color: #bbb;
+            div.footer {
+              font-size: small;
+              padding-left: 20px;
+              padding-right: 20px;
+              padding-top: 5px;
+              padding-bottom: 5px;
+              margin: auto;
+              background: #305275;
+              color: #fffee7;
             }
 
             table {
-            border-style: 10px solid #313131;
-            border-spacing: 0;
-            width: auto;
-            }
-
-            table.log {
-            }
-
-            td {
-            border-width: 0;
-            border-style: none;
-            padding-right: 0.5em;
-            padding-left: 0.5em;
-            width: auto;
+              border-style: solid;
+              border: 10px #313131;
+              border-spacing: 0;
+              width: auto;
             }
 
-            .td_sel {
-            background-color: #afafaf;
-            border: 1px solid #afafaf;
-            font-weight:bold;
-            padding-right: 1em;
-            padding-left: 1em;
-
-            }
-
-            .td_nsel {
-            border: 0px;
-            padding-right: 1em;
-            padding-left: 1em;
-            }
+            tb { border: 1px; }
 
             tr {
-            vertical-align: top;
-            width: auto;
-            }
-
-            h1 {
-            padding: 0.5em;
-            background-color: #305275;
-            margin-top: 0;
-            margin-bottom: 0;
-            color: #fff;
-            margin-left: -20px;
-            margin-right: -20px;
-            }
-
-            wid {
-            text-transform: uppercase;
-            font-size: smaller;
-            margin-top: 1em;
-            margin-left: -0.5em;
-            /*background: #fffbce;*/
-            /*background: #628a0d;*/
-            padding: 5px;
-            color: #305275;
-            }
-
-            .attrname {
-            text-align: right;
-            font-size: smaller;
-            }
-
-            .attrval {
-            color: #222;
+              vertical-align: top;
+              width: auto;
             }
 
-            .issue-closed-fixed {
-            background-image: "green-check.png";
+            td {
+              border-width: 0;
+              border-style: none;
+              padding-right: 0.5em;
+              padding-left: 0.5em;
+              width: auto;
             }
 
-            .issue-closed-wontfix {
-            background-image: "red-check.png";
-            }
+            img { border-style: none; }
 
-            .issue-closed-reorg {
-            background-image: "blue-check.png";
-            }
-
-            .inline-issue-link {
-            text-decoration: underline;
+            h1 {
+              padding: 0.5em;
+              background-color: #305275;
+              margin-top: 0;
+              margin-bottom: 0;
+              color: #fff;
+              margin-left: -20px;
+              margin-right: -20px;
             }
 
-            img {
-            border: 0;
+            ul {
+              list-style-type: none;
+              padding: 0;
             }
 
+            p { width: auto; }
 
-            div.footer {
-            font-size: small;
-            padding-left: 20px;
-            padding-right: 20px;
-            padding-top: 5px;
-            padding-bottom: 5px;
-            margin: auto;
-            background: #305275;
-            color: #fffee7;
-            }
-
-            .footer a {
-            color: #508d91;
+            a, a:visited {
+              background: inherit;
+              text-decoration: none;
             }
 
+            a { color: #003d41; }
+            a:visited { color: #553d41; }
+            .footer a { color: #508d91; }
 
-            .header {
-            font-family: "lucida grande", "sans serif";
-            font-size: smaller;
-            background-color: #a9a9a9;
-            text-align: left;
-
-            padding-right: 0.5em;
-            padding-left: 0.5em;
-
-            }
-
+            /* bug index pages */
 
-            .selected-cell {
-            background-color: #e9e9e2;
+            td.tab {
+              padding-right: 1em;
+              padding-left: 1em;
             }
 
-            .plain-cell {
-            background-color: #f9f9f9;
+            td.sel.tab {
+              background-color: #afafaf;
+              border: 1px solid #afafaf;
+              font-weight:bold;
             }
 
+            td.nsel.tab { border: 0px; }
 
-            .logcomment {
-            padding-left: 4em;
-            font-size: smaller;
+            table.bug_list {
+              background-color: #afafaf;
+              border: 2px solid #afafaf;
             }
 
-            .id {
-            font-family: courier;
-            }
+            .bug_list tr { width: auto; }
+            tr.wishlist { background-color: #B4FF9B; }
+            tr.minor { background-color: #FCFF98; }
+            tr.serious { background-color: #FFB648; }
+            tr.critical { background-color: #FF752A; }
+            tr.fatal { background-color: #FF3300; }
 
-            .table_bug {
-            background-color: #afafaf;
-            border: 2px solid #afafaf;
-            }
+            /* bug detail pages */
 
-            .message {
-            }
+            td.bug_detail_label { text-align: right; }
+            td.bug_detail { } 
+            td.bug_comment_label { text-align: right; vertical-align: top; }
+            td.bug_comment { } 
 
-            .progress-meter-done {
-            background-color: #03af00;
+            div.comment {
+              padding: 20px;
+              padding-top: 20px;
+              margin: auto;
+              margin-top: 0;
             }
 
-            .progress-meter-undone {
-            background-color: #ddd;
+            div.root.comment {
+              padding: 0px;
+              /* padding-top: 0px; */
+              padding-bottom: 20px;
             }
-
-            .progress-meter {
-            }
-        """
+       """
 
         self.index_file = """
             <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-            "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+              "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>BugsEverywhere Issue Tracker</title>
+            <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="main">
-            <h1>BugsEverywhere Bug List</h1>
+            <h1>%(index_header)s</h1>
             <p></p>
             <table>
 
@@ -400,206 +416,116 @@ class BEHTMLGen():
             </tr>
 
             </table>
-            <table class="table_bug">
+            <table class="bug_list">
             <tbody>
 
-            %(bug_table)s
+            %(bug_entries)s
 
             </tbody>
             </table>
-
             </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.bug_line ="""
-        <tr class="%(severity)s-row">
-        <td ><a href="bugs/%(uuid)s.html">%(shortname)s</a></td>
-        <td ><a href="bugs/%(uuid)s.html">%(status)s</a></td>
-        <td><a href="bugs/%(uuid)s.html">%(severity)s</a></td>
-        <td><a href="bugs/%(uuid)s.html">%(summary)s</a></td>
-        <td><a href="bugs/%(uuid)s.html">%(time_string)s</a></td>
-        </tr>
-        """
-
-        self.detail_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>BugsEverywhere Issue Tracker</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="main">
-        <h1>BugsEverywhere Bug List</h1>
-        <h5><a href="%(up_link)s">Back to Index</a></h5>
-        <h2>Bug: %(shortname)s</h2>
-        <table >
-        <tbody>
-
-        %(bug_lines)s
-
-        %(comment_lines)s
-        </tbody>
-        </table>
-        </div>
-        <h5><a href="%(up_link)s">Back to Index</a></h5>
-        <div class="footer">Generated by <a href="http://www.bugseverywhere.org/">BugsEverywhere</a>.</div>
-        </body>
-        </html>
-
-        """
-
-        self.detail_line ="""
-        <tr>
-        <td align="right">%(label)s :</td><td>%(value)s</td>
-        </tr>
-        """
-
-
-        self.comment_section ="""
-        <tr>
-        <td align="right">Comments:
-        </td>
-        <td>
-        %(comment)s
-        </td>
-        </tr>
+        self.index_bug_entry ="""
+            <tr class="%(severity)s-row">
+              <td><a href="bugs/%(uuid)s.html">%(shortname)s</a></td>
+              <td><a href="bugs/%(uuid)s.html">%(status)s</a></td>
+              <td><a href="bugs/%(uuid)s.html">%(severity)s</a></td>
+              <td><a href="bugs/%(uuid)s.html">%(summary)s</a></td>
+              <td><a href="bugs/%(uuid)s.html">%(time_string)s</a></td>
+            </tr>
         """
 
-        if template != None:
-            for filename,attr in [('style.css','css_file'),
-                                  ('index_file.tpl','index_file'),
-                                  ('detail_file.tpl','detail_file'),
-                                  ('comment_section.tpl','comment_section')]:
-                fullpath = os.path.join(self.template, filename)
-                if os.path.exists(fullpath):
-                    f = codecs.open(fullpath, "r", self.encoding)
-                    setattr(self, attr, f.read())
-                    f.close()
-
-    def create_output_directories(self, out_dir):
-        if self.verbose:
-            print "Creating output directories"
-        self.out_dir = os.path.abspath(os.path.expanduser(out_dir))
-        if not os.path.exists(self.out_dir):
-            try:
-                os.mkdir(self.out_dir)
-            except:
-                raise cmdutil.UsageError, "Cannot create output directory '%s'." % self.out_dir
-        self.out_dir_bugs = os.path.join(self.out_dir, "bugs")
-        if not os.path.exists(self.out_dir_bugs):
-            os.mkdir(self.out_dir_bugs)
+        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="main">
+            <h1>BugsEverywhere Bug List</h1>
+            <h5><a href="%(up_link)s">Back to 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>
 
-    def write_css_file(self):
-        if self.verbose:
-            print "Writing css file"
-        assert hasattr(self, "out_dir"), "Must run after ._create_output_directories()"
-        f = codecs.open(os.path.join(self.out_dir,"style.css"), "w", self.encoding)
-        f.write(self.css_file)
-        f.close()
+            <hr/>
 
-    def write_detail_file(self, bug, up_link):
-        if self.verbose:
-            print "\tCreating detail entry for bug: %s" % escape(self.bd.bug_shortname(bug))
-        assert hasattr(self, "out_dir_bugs"), "Must run after ._create_output_directories()"
-        detail_file_ = re.sub('_bug_id_', bug.uuid[0:3], self.detail_file)
-
-        bug_ = self.bd.bug_from_shortname(bug.uuid)
-        bug_.load_comments(load_full=True)
-        detail_lines = []
-        for label,value in [('ID', bug.uuid),
-                            ('Short name', escape(self.bd.bug_shortname(bug))),
-                            ('Severity', escape(bug.severity)),
-                            ('Status', escape(bug.status)),
-                            ('Assigned', escape(bug.assigned)),
-                            ('Target', escape(bug.target)),
-                            ('Reporter', escape(bug.reporter)),
-                            ('Creator', escape(bug.creator)),
-                            ('Created', escape(bug.time_string)),
-                            ('Summary', escape(bug.summary)),
-                            ]:
-            detail_lines.append(self.detail_line % {'label':label, 'value':value})
-        detail_lines.append('<tr><td colspan="2"><hr /></td></tr>')
+            %(comment_entries)s
 
-        stack = []
-        comment_lines = []
-        for depth,comment in bug_.comment_root.thread(flatten=False):
-            while len(stack) > depth:
-                stack.pop(-1)      # pop non-parents off the stack
-                comment_lines.append("</div>\n") # close non-parent <div class="comment...
-            assert len(stack) == depth
-            stack.append(comment)
-            lines = ["--------- Comment ---------",
-                     "Name: %s" % comment.uuid,
-                     "From: %s" % escape(comment.author),
-                     "Date: %s" % escape(comment.date),
-                     ""]
-            lines.extend(escape(comment.body).splitlines())
-            if depth == 0:
-                comment_lines.append('<div class="commentF">')
-            else:
-                comment_lines.append('<div class="comment">')
-            comment_lines.append("<br />\n".join(lines)+"<br />\n")
-        while len(stack) > 0:
-            stack.pop(-1)
-            comment_lines.append("</div>\n") # close every remaining <div class="comment...
-        comments = self.comment_section % {'comment':'\n'.join(comment_lines)}
+            </div>
+            <h5><a href="%(up_link)s">Back to Index</a></h5>
 
-        filename = "%s.html" % bug.uuid
-        fullpath = os.path.join(self.out_dir_bugs, filename)
-        template_info = {'charset':self.encoding,
-                         'shortname':self.bd.bug_shortname(bug),
-                         'up_link':up_link,
-                         'bug_lines':'\n'.join(detail_lines),
-                         'comment_lines':comments}
-        f = codecs.open(fullpath, "w", self.encoding)
-        f.write(detail_file_ % template_info)
-        f.close()
+            <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>
 
-    def write_index_file(self, bugs, fileid):
-        if self.verbose:
-            print "Writing %s index file for %d bugs" % (fileid, len(bugs))
-        assert hasattr(self, "out_dir"), "Must run after ._create_output_directories()"
+            </body>
+            </html>
+        """
 
-        bug_lines = []
-        for b in bugs:
-            if self.verbose:
-                print "Creating bug entry: %s" % escape(self.bd.bug_shortname(b))
-            template_info = {'uuid':b.uuid,
-                             'shortname':self.bd.bug_shortname(b),
-                             'status':b.status,
-                             'severity':b.severity,
-                             'summary':b.summary,
-                             'time_string':b.time_string}
-            bug_lines.append(self.bug_line % template_info)
-
-        if fileid == "active":
-            filename = "index.html"
-        elif fileid == "inactive":
-            filename = "index_inactive.html"
-        else:
-            raise Exception, "Unrecognized fileid: '%s'" % fileid
-        template_info = {'charset':self.encoding,
-                         'active_class':'td_sel',
-                         'inactive_class':'td_nsel',
-                         'bug_table':'\n'.join(bug_lines),
-                         'generation_time':time.ctime()}
-        if fileid == "inactive":
-            template_info['active_class'] = 'td_nsel'
-            template_info['inactive_class'] = 'td_sel'
+        self.bug_comment_entry ="""
+            <table>
+            <tr>
+              <td class="bug_comment_label">Comment:</td>
+              <td class="bug_comment">
+            --------- Comment ---------<br/>
+            Name: %(uuid)s<br/>
+            From: %(author)s<br/>
+            Date: %(date)s<br/>
+            <br/>
+            %(body)s
+              </td>
+            </tr>
+            </table>
+        """
 
-        f = codecs.open(os.path.join(self.out_dir, filename), "w", self.encoding)
-        f.write(self.index_file % template_info)
-        f.close()
+        # strip leading whitespace
+        for attr in ['css_file', 'index_file', 'index_bug_entry', 'bug_file',
+                     'bug_comment_entry']:
+            value = getattr(self, attr)
+            value = '\n'.join(value.split('\n'+' '*12))
+            setattr(self, attr, value.strip()+'\n')