Index: COPYING
===================================================================
--- COPYING	(revision 18015)
+++ COPYING	(working copy)
@@ -1,5 +1,6 @@
 Copyright (C) 2010-2011 Richard Liao <richard.liao.i@gmail.com>
 Copyright (C) 2014-2017 Ryan J Ollos <ryan.j.ollos@gmail.com>
+Copyright (C) 2021 Jun Omae <jun66j5@gmail.com>
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
Index: setup.py
===================================================================
--- setup.py	(revision 18015)
+++ setup.py	(working copy)
@@ -41,7 +41,7 @@
     license="3-Clause BSD",
     packages=find_packages(exclude=['ez_setup', 'examples', 'tests*']),
     include_package_data=True,
-    package_data={'ticketlog': ['*.txt', 'templates/*.*', 'htdocs/*.*',
+    package_data={'ticketlog': ['*.txt', 'htdocs/*.*',
                                 'tests/*.*', 'locale/*.*',
                                 'locale/*/LC_MESSAGES/*.*']},
     zip_safe=False,
Index: ticketlog/htdocs/ticketlog.js
===================================================================
--- ticketlog/htdocs/ticketlog.js	(nonexistent)
+++ ticketlog/htdocs/ticketlog.js	(working copy)
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2021 Jun Omae <jun66j5@gmail.com>
+ * All rights reserved.
+ *
+ * This software is licensed as described in the file COPYING, which
+ * you should have received as part of this distribution.
+ */
+
+jQuery(function($) {
+    var data = window.ticketlog;
+    if (data === undefined)
+        return;
+    var target = $('div#ticket');
+    if (target.siblings('#ticket_log').length !== 0)
+        return;
+    var content = $('<div>').html(data.content).contents();
+    target.after(content);
+});
Index: ticketlog/templates/ticketlog.html
===================================================================
--- ticketlog/templates/ticketlog.html	(revision 18015)
+++ ticketlog/templates/ticketlog.html	(nonexistent)
@@ -1,33 +0,0 @@
-<!--!
-  Copyright (C) 2014-2017 Ryan J Ollos <ryan.j.ollos@gmail.com>
-  All rights reserved.
-
-  This software is licensed as described in the file COPYING, which
-  you should have received as part of this distribution.
--->
-<html xmlns="http://www.w3.org/1999/xhtml"
-      xmlns:py="http://genshi.edgewall.org/"
-      xmlns:xi="http://www.w3.org/2001/XInclude"
-      xmlns:i18n="http://genshi.edgewall.org/i18n"
-      i18n:domain="ticketlog"
-      py:strip="">
-  <script type="text/javascript">
-    jQuery(document).ready(function($) {
-      $("#ticket_log").toggleClass("collapsed");
-    });
-  </script>
-  <div id="ticket_log">
-    <h3 class="foldable">Commit History <span class="trac-count">(${len(revisions)})</span></h3>
-    <table py:if="revisions" id="ticket_revisions" class="listing">
-      <thead>
-        <tr><th class="rev">Changeset</th><th class="author">Author</th><th class="time">Time</th><th class="summary">ChangeLog</th></tr>
-      </thead>
-      <tbody py:for="entry in revisions">
-        <tr><td>${entry['rev']}</td><td>${entry['author']}</td><td>${entry['time']}</td><td>${entry['message']}</td></tr>
-      </tbody>
-    </table>
-    <div py:if="not revisions">
-      <p class="hint">(No commits)</p>
-    </div>
-  </div>
-</html>
Index: ticketlog/web_ui.py
===================================================================
--- ticketlog/web_ui.py	(revision 18015)
+++ ticketlog/web_ui.py	(working copy)
@@ -10,7 +10,6 @@
 
 from pkg_resources import resource_filename
 
-from genshi.filters.transform import Transformer
 from trac.admin.api import IAdminCommandProvider
 from trac.config import IntOption
 from trac.core import implements
@@ -19,11 +18,12 @@
 from trac.env import IEnvironmentSetupParticipant
 from trac.resource import Resource
 from trac.versioncontrol.api import RepositoryManager
-from trac.web.api import ITemplateStreamFilter
-from trac.web.chrome import Chrome, ITemplateProvider, add_stylesheet, \
-                            web_context
+from trac.web.api import IRequestFilter
+from trac.web.chrome import (Chrome, ITemplateProvider, add_script,
+                             add_script_data, add_stylesheet, web_context)
 from trac.wiki.formatter import format_to_html, format_to_oneliner
 from trac.util.datefmt import format_datetime, from_utimestamp, user_time
+from trac.util.html import Markup, tag
 from trac.util.text import exception_to_unicode, shorten_line
 from trac.util.translation import domain_functions
 from tracopt.ticket.commit_updater import CommitTicketUpdater
@@ -34,7 +34,7 @@
 
 class TicketLogModule(CommitTicketUpdater):
     implements(IAdminCommandProvider, IEnvironmentSetupParticipant,
-               ITemplateProvider, ITemplateStreamFilter)
+               IRequestFilter, ITemplateProvider)
 
     max_message_length = IntOption('ticketlog', 'log_message_maxlength',
         doc="""Maximum length of log message to display.""")
@@ -134,21 +134,25 @@
     def get_htdocs_dirs(self):
         return [('ticketlog', resource_filename(__name__, 'htdocs'))]
 
-    # ITemplateStreamFilter methods
+    # IRequestFilter methods
 
-    def filter_stream(self, req, method, filename, stream, data):
-        if filename == 'ticket.html' and \
+    def pre_process_request(self, req, handler):
+        return handler
+
+    def post_process_request(self, req, template, data, metadata):
+        if template == 'ticket.html' and \
                 req.path_info.startswith('/ticket/'):
             ticket_id = req.args.getint('id')
             resource = Resource('ticket', ticket_id)
             if 'CHANGESET_VIEW' in req.perm(resource):
                 add_stylesheet(req, 'ticketlog/ticketlog.css')
+                add_script(req, 'ticketlog/ticketlog.js')
                 revisions = self._get_ticket_revisions(req, ticket_id)
-                template = Chrome(self.env) \
-                    .load_template('ticketlog.html', None) \
-                    .generate(revisions=revisions)
-                stream |= Transformer('//div[@id="ticket"]').after(template)
-        return stream
+                script_data = {
+                    'content': self._render_commit_history(revisions),
+                }
+                add_script_data(req, ticketlog=script_data)
+        return template, data, metadata
 
     # Internal methods
 
@@ -230,3 +234,24 @@
         revisions.sort(key=lambda r: r['timestamp'], reverse=True)
 
         return revisions
+
+    def _render_commit_history(self, revisions):
+        header = tag.h3(_("Commit History"), ' ',
+                        tag.span('(%d)' % len(revisions), class_='trac-count'),
+                        class_='foldable')
+        if revisions:
+            thead = tag.thead(tag.tr(tag.th(_("Changeset"), class_='rev'),
+                                     tag.th(_("Author"), class_='author'),
+                                     tag.th(_("Time"), class_='time'),
+                                     tag.th(_("ChangeLog"), class_='summary')))
+            tbody = tag.tbody()
+            for entry in revisions:
+                tr = tag.tr(tag.td(entry['rev']), tag.td(entry['author']),
+                            tag.td(entry['time']), tag.td(entry['message']))
+                tbody.append(tr)
+            content = tag.table(thead, tbody, id='ticket_revisions',
+                                class_='listing')
+        else:
+            content = tag.div(tag.p(_("(No commits)"), class_='hint'))
+        return Markup(tag.div(header, content, id='ticket_log',
+                              class_='collapsed'))
