Skip to content

Commit ee13ef8

Browse files
ShlomoSteptclaude
andcommitted
Add syntax highlighting with Pygments for code blocks
- Add Pygments dependency for syntax highlighting - Implement highlight_code() function that detects language from file extension - Apply highlighting to Write and Edit tool content - Add Monokai-inspired dark theme CSS for highlighted code - Supports Python, JavaScript, HTML, CSS, JSON, and many other languages - Falls back gracefully to plain text for unrecognized file types This significantly improves code readability in transcript outputs by providing proper syntax coloring for different programming languages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7242cd2 commit ee13ef8

11 files changed

Lines changed: 261 additions & 28 deletions

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies = [
1414
"httpx",
1515
"jinja2",
1616
"markdown",
17+
"pygments>=2.17.0",
1718
"questionary",
1819
]
1920

src/claude_code_transcripts/__init__.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
import httpx
1818
from jinja2 import Environment, PackageLoader
1919
import markdown
20+
from pygments import highlight
21+
from pygments.lexers import get_lexer_for_filename, get_lexer_by_name, TextLexer
22+
from pygments.formatters import HtmlFormatter
23+
from pygments.util import ClassNotFound
2024
import questionary
2125

2226
# Set up Jinja2 environment
@@ -122,6 +126,35 @@ def render_content_block_array(blocks):
122126
return "".join(parts) if parts else None
123127

124128

129+
def highlight_code(code, filename=None, language=None):
130+
"""Apply syntax highlighting to code using Pygments.
131+
132+
Args:
133+
code: The source code to highlight.
134+
filename: Optional filename to detect language from extension.
135+
language: Optional explicit language name.
136+
137+
Returns:
138+
HTML string with syntax highlighting, or escaped plain text if highlighting fails.
139+
"""
140+
if not code:
141+
return ""
142+
143+
try:
144+
if language:
145+
lexer = get_lexer_by_name(language)
146+
elif filename:
147+
lexer = get_lexer_for_filename(filename)
148+
else:
149+
lexer = TextLexer()
150+
except ClassNotFound:
151+
lexer = TextLexer()
152+
153+
formatter = HtmlFormatter(nowrap=True, cssclass="highlight")
154+
highlighted = highlight(code, lexer, formatter)
155+
return highlighted
156+
157+
125158
def extract_text_from_content(content):
126159
"""Extract plain text from message content.
127160
@@ -728,7 +761,9 @@ def render_write_tool(tool_input, tool_id):
728761
"""Render Write tool calls with file path header and content preview."""
729762
file_path = tool_input.get("file_path", "Unknown file")
730763
content = tool_input.get("content", "")
731-
return _macros.write_tool(file_path, content, tool_id)
764+
# Apply syntax highlighting based on file extension
765+
highlighted_content = highlight_code(content, filename=file_path)
766+
return _macros.write_tool(file_path, highlighted_content, tool_id)
732767

733768

734769
def render_edit_tool(tool_input, tool_id):
@@ -737,7 +772,12 @@ def render_edit_tool(tool_input, tool_id):
737772
old_string = tool_input.get("old_string", "")
738773
new_string = tool_input.get("new_string", "")
739774
replace_all = tool_input.get("replace_all", False)
740-
return _macros.edit_tool(file_path, old_string, new_string, replace_all, tool_id)
775+
# Apply syntax highlighting based on file extension
776+
highlighted_old = highlight_code(old_string, filename=file_path)
777+
highlighted_new = highlight_code(new_string, filename=file_path)
778+
return _macros.edit_tool(
779+
file_path, highlighted_old, highlighted_new, replace_all, tool_id
780+
)
741781

742782

743783
def render_bash_tool(tool_input, tool_id):
@@ -1037,8 +1077,37 @@ def render_message(log_type, message_json, timestamp):
10371077
.todo-pending .todo-content { color: #616161; }
10381078
pre { background: var(--code-bg); color: var(--code-text); padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; line-height: 1.5; margin: 8px 0; white-space: pre-wrap; word-wrap: break-word; }
10391079
pre.json { color: #e0e0e0; }
1080+
pre.highlight { color: #e0e0e0; }
10401081
code { background: rgba(0,0,0,0.08); padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
10411082
pre code { background: none; padding: 0; }
1083+
.highlight .hll { background-color: #49483e }
1084+
.highlight .c { color: #75715e } /* Comment */
1085+
.highlight .err { color: #f92672 } /* Error */
1086+
.highlight .k { color: #66d9ef } /* Keyword */
1087+
.highlight .l { color: #ae81ff } /* Literal */
1088+
.highlight .n { color: #e0e0e0 } /* Name */
1089+
.highlight .o { color: #f92672 } /* Operator */
1090+
.highlight .p { color: #e0e0e0 } /* Punctuation */
1091+
.highlight .ch, .highlight .cm, .highlight .c1, .highlight .cs, .highlight .cp, .highlight .cpf { color: #75715e } /* Comments */
1092+
.highlight .gd { color: #f92672 } /* Generic.Deleted */
1093+
.highlight .gi { color: #a6e22e } /* Generic.Inserted */
1094+
.highlight .kc, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt { color: #66d9ef } /* Keywords */
1095+
.highlight .ld { color: #e6db74 } /* Literal.Date */
1096+
.highlight .m, .highlight .mb, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .mo { color: #ae81ff } /* Numbers */
1097+
.highlight .s, .highlight .sa, .highlight .sb, .highlight .sc, .highlight .dl, .highlight .sd, .highlight .s2, .highlight .se, .highlight .sh, .highlight .si, .highlight .sx, .highlight .sr, .highlight .s1, .highlight .ss { color: #e6db74 } /* Strings */
1098+
.highlight .na { color: #a6e22e } /* Name.Attribute */
1099+
.highlight .nb { color: #e0e0e0 } /* Name.Builtin */
1100+
.highlight .nc { color: #a6e22e } /* Name.Class */
1101+
.highlight .no { color: #66d9ef } /* Name.Constant */
1102+
.highlight .nd { color: #a6e22e } /* Name.Decorator */
1103+
.highlight .ne { color: #a6e22e } /* Name.Exception */
1104+
.highlight .nf { color: #a6e22e } /* Name.Function */
1105+
.highlight .nl { color: #e0e0e0 } /* Name.Label */
1106+
.highlight .nn { color: #e0e0e0 } /* Name.Namespace */
1107+
.highlight .nt { color: #f92672 } /* Name.Tag */
1108+
.highlight .nv, .highlight .vc, .highlight .vg, .highlight .vi, .highlight .vm { color: #e0e0e0 } /* Variables */
1109+
.highlight .ow { color: #f92672 } /* Operator.Word */
1110+
.highlight .w { color: #e0e0e0 } /* Text.Whitespace */
10421111
.user-content { margin: 0; }
10431112
.truncatable { position: relative; }
10441113
.truncatable.truncated .truncatable-content { max-height: 200px; overflow: hidden; }

src/claude_code_transcripts/templates/macros.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,25 @@
6767
</ul></div>
6868
{%- endmacro %}
6969

70-
{# Write tool #}
70+
{# Write tool - content is pre-highlighted so needs |safe #}
7171
{% macro write_tool(file_path, content, tool_id) %}
7272
{%- set filename = file_path.split('/')[-1] if '/' in file_path else file_path -%}
7373
<div class="file-tool write-tool" data-tool-id="{{ tool_id }}">
7474
<div class="file-tool-header write-header"><span class="file-tool-icon">📝</span> Write <span class="file-tool-path">{{ filename }}</span></div>
7575
<div class="file-tool-fullpath">{{ file_path }}</div>
76-
<div class="truncatable"><div class="truncatable-content"><pre class="file-content">{{ content }}</pre></div><button class="expand-btn">Show more</button></div>
76+
<div class="truncatable"><div class="truncatable-content"><pre class="file-content highlight">{{ content|safe }}</pre></div><button class="expand-btn">Show more</button></div>
7777
</div>
7878
{%- endmacro %}
7979

80-
{# Edit tool #}
80+
{# Edit tool - old/new strings are pre-highlighted so need |safe #}
8181
{% macro edit_tool(file_path, old_string, new_string, replace_all, tool_id) %}
8282
{%- set filename = file_path.split('/')[-1] if '/' in file_path else file_path -%}
8383
<div class="file-tool edit-tool" data-tool-id="{{ tool_id }}">
8484
<div class="file-tool-header edit-header"><span class="file-tool-icon">✏️</span> Edit <span class="file-tool-path">{{ filename }}</span>{% if replace_all %} <span class="edit-replace-all">(replace all)</span>{% endif %}</div>
8585
<div class="file-tool-fullpath">{{ file_path }}</div>
8686
<div class="truncatable"><div class="truncatable-content">
87-
<div class="edit-section edit-old"><div class="edit-label"></div><pre class="edit-content">{{ old_string }}</pre></div>
88-
<div class="edit-section edit-new"><div class="edit-label">+</div><pre class="edit-content">{{ new_string }}</pre></div>
87+
<div class="edit-section edit-old"><div class="edit-label"></div><pre class="edit-content highlight">{{ old_string|safe }}</pre></div>
88+
<div class="edit-section edit-new"><div class="edit-label">+</div><pre class="edit-content highlight">{{ new_string|safe }}</pre></div>
8989
</div><button class="expand-btn">Show more</button></div>
9090
</div>
9191
{%- endmacro %}

tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_index_html.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,37 @@
7676
.todo-pending .todo-content { color: #616161; }
7777
pre { background: var(--code-bg); color: var(--code-text); padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; line-height: 1.5; margin: 8px 0; white-space: pre-wrap; word-wrap: break-word; }
7878
pre.json { color: #e0e0e0; }
79+
pre.highlight { color: #e0e0e0; }
7980
code { background: rgba(0,0,0,0.08); padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
8081
pre code { background: none; padding: 0; }
82+
.highlight .hll { background-color: #49483e }
83+
.highlight .c { color: #75715e } /* Comment */
84+
.highlight .err { color: #f92672 } /* Error */
85+
.highlight .k { color: #66d9ef } /* Keyword */
86+
.highlight .l { color: #ae81ff } /* Literal */
87+
.highlight .n { color: #e0e0e0 } /* Name */
88+
.highlight .o { color: #f92672 } /* Operator */
89+
.highlight .p { color: #e0e0e0 } /* Punctuation */
90+
.highlight .ch, .highlight .cm, .highlight .c1, .highlight .cs, .highlight .cp, .highlight .cpf { color: #75715e } /* Comments */
91+
.highlight .gd { color: #f92672 } /* Generic.Deleted */
92+
.highlight .gi { color: #a6e22e } /* Generic.Inserted */
93+
.highlight .kc, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt { color: #66d9ef } /* Keywords */
94+
.highlight .ld { color: #e6db74 } /* Literal.Date */
95+
.highlight .m, .highlight .mb, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .mo { color: #ae81ff } /* Numbers */
96+
.highlight .s, .highlight .sa, .highlight .sb, .highlight .sc, .highlight .dl, .highlight .sd, .highlight .s2, .highlight .se, .highlight .sh, .highlight .si, .highlight .sx, .highlight .sr, .highlight .s1, .highlight .ss { color: #e6db74 } /* Strings */
97+
.highlight .na { color: #a6e22e } /* Name.Attribute */
98+
.highlight .nb { color: #e0e0e0 } /* Name.Builtin */
99+
.highlight .nc { color: #a6e22e } /* Name.Class */
100+
.highlight .no { color: #66d9ef } /* Name.Constant */
101+
.highlight .nd { color: #a6e22e } /* Name.Decorator */
102+
.highlight .ne { color: #a6e22e } /* Name.Exception */
103+
.highlight .nf { color: #a6e22e } /* Name.Function */
104+
.highlight .nl { color: #e0e0e0 } /* Name.Label */
105+
.highlight .nn { color: #e0e0e0 } /* Name.Namespace */
106+
.highlight .nt { color: #f92672 } /* Name.Tag */
107+
.highlight .nv, .highlight .vc, .highlight .vg, .highlight .vi, .highlight .vm { color: #e0e0e0 } /* Variables */
108+
.highlight .ow { color: #f92672 } /* Operator.Word */
109+
.highlight .w { color: #e0e0e0 } /* Text.Whitespace */
81110
.user-content { margin: 0; }
82111
.truncatable { position: relative; }
83112
.truncatable.truncated .truncatable-content { max-height: 200px; overflow: hidden; }

tests/__snapshots__/test_generate_html/TestGenerateHtml.test_generates_page_001_html.html

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,37 @@
7676
.todo-pending .todo-content { color: #616161; }
7777
pre { background: var(--code-bg); color: var(--code-text); padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 0.85rem; line-height: 1.5; margin: 8px 0; white-space: pre-wrap; word-wrap: break-word; }
7878
pre.json { color: #e0e0e0; }
79+
pre.highlight { color: #e0e0e0; }
7980
code { background: rgba(0,0,0,0.08); padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }
8081
pre code { background: none; padding: 0; }
82+
.highlight .hll { background-color: #49483e }
83+
.highlight .c { color: #75715e } /* Comment */
84+
.highlight .err { color: #f92672 } /* Error */
85+
.highlight .k { color: #66d9ef } /* Keyword */
86+
.highlight .l { color: #ae81ff } /* Literal */
87+
.highlight .n { color: #e0e0e0 } /* Name */
88+
.highlight .o { color: #f92672 } /* Operator */
89+
.highlight .p { color: #e0e0e0 } /* Punctuation */
90+
.highlight .ch, .highlight .cm, .highlight .c1, .highlight .cs, .highlight .cp, .highlight .cpf { color: #75715e } /* Comments */
91+
.highlight .gd { color: #f92672 } /* Generic.Deleted */
92+
.highlight .gi { color: #a6e22e } /* Generic.Inserted */
93+
.highlight .kc, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt { color: #66d9ef } /* Keywords */
94+
.highlight .ld { color: #e6db74 } /* Literal.Date */
95+
.highlight .m, .highlight .mb, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .mo { color: #ae81ff } /* Numbers */
96+
.highlight .s, .highlight .sa, .highlight .sb, .highlight .sc, .highlight .dl, .highlight .sd, .highlight .s2, .highlight .se, .highlight .sh, .highlight .si, .highlight .sx, .highlight .sr, .highlight .s1, .highlight .ss { color: #e6db74 } /* Strings */
97+
.highlight .na { color: #a6e22e } /* Name.Attribute */
98+
.highlight .nb { color: #e0e0e0 } /* Name.Builtin */
99+
.highlight .nc { color: #a6e22e } /* Name.Class */
100+
.highlight .no { color: #66d9ef } /* Name.Constant */
101+
.highlight .nd { color: #a6e22e } /* Name.Decorator */
102+
.highlight .ne { color: #a6e22e } /* Name.Exception */
103+
.highlight .nf { color: #a6e22e } /* Name.Function */
104+
.highlight .nl { color: #e0e0e0 } /* Name.Label */
105+
.highlight .nn { color: #e0e0e0 } /* Name.Namespace */
106+
.highlight .nt { color: #f92672 } /* Name.Tag */
107+
.highlight .nv, .highlight .vc, .highlight .vg, .highlight .vi, .highlight .vm { color: #e0e0e0 } /* Variables */
108+
.highlight .ow { color: #f92672 } /* Operator.Word */
109+
.highlight .w { color: #e0e0e0 } /* Text.Whitespace */
81110
.user-content { margin: 0; }
82111
.truncatable { position: relative; }
83112
.truncatable.truncated .truncatable-content { max-height: 200px; overflow: hidden; }
@@ -172,9 +201,9 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
172201
<div class="assistant-text"><p>I'll create a simple Python function for you. Let me write it now.</p></div><div class="file-tool write-tool" data-tool-id="toolu_write_001">
173202
<div class="file-tool-header write-header"><span class="file-tool-icon">📝</span> Write <span class="file-tool-path">math_utils.py</span></div>
174203
<div class="file-tool-fullpath">/project/math_utils.py</div>
175-
<div class="truncatable"><div class="truncatable-content"><pre class="file-content">def add(a: int, b: int) -&gt; int:
176-
&#34;&#34;&#34;Add two numbers together.&#34;&#34;&#34;
177-
return a + b
204+
<div class="truncatable"><div class="truncatable-content"><pre class="file-content highlight"><span class="k">def</span><span class="w"> </span><span class="nf">add</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
205+
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Add two numbers together.&quot;&quot;&quot;</span>
206+
<span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
178207
</pre></div><button class="expand-btn">Show more</button></div>
179208
</div></div></div>
180209
<div class="message tool-reply" id="msg-2025-12-24T10-00-10-000Z"><div class="message-header"><span class="role-label">Tool reply</span><a href="#msg-2025-12-24T10-00-10-000Z" class="timestamp-link"><time datetime="2025-12-24T10:00:10.000Z" data-timestamp="2025-12-24T10:00:10.000Z">2025-12-24T10:00:10.000Z</time></a></div><div class="message-content"><div class="tool-result"><div class="truncatable"><div class="truncatable-content"><pre>File written successfully</pre></div><button class="expand-btn">Show more</button></div></div></div></div>
@@ -225,14 +254,14 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
225254
<div class="file-tool-header edit-header"><span class="file-tool-icon">✏️</span> Edit <span class="file-tool-path">math_utils.py</span></div>
226255
<div class="file-tool-fullpath">/project/math_utils.py</div>
227256
<div class="truncatable"><div class="truncatable-content">
228-
<div class="edit-section edit-old"><div class="edit-label"></div><pre class="edit-content"> return a + b
257+
<div class="edit-section edit-old"><div class="edit-label"></div><pre class="edit-content highlight"> <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
229258
</pre></div>
230-
<div class="edit-section edit-new"><div class="edit-label">+</div><pre class="edit-content"> return a + b
259+
<div class="edit-section edit-new"><div class="edit-label">+</div><pre class="edit-content highlight"> <span class="k">return</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span>
231260

232261

233-
def subtract(a: int, b: int) -&gt; int:
234-
&#34;&#34;&#34;Subtract b from a.&#34;&#34;&#34;
235-
return a - b
262+
<span class="k">def</span><span class="w"> </span><span class="nf">subtract</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
263+
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Subtract b from a.&quot;&quot;&quot;</span>
264+
<span class="k">return</span> <span class="n">a</span> <span class="o">-</span> <span class="n">b</span>
236265
</pre></div>
237266
</div><button class="expand-btn">Show more</button></div>
238267
</div></div></div>
@@ -270,8 +299,10 @@ <h1><a href="index.html" style="color: inherit; text-decoration: none;">Claude C
270299
<div class="file-tool-header edit-header"><span class="file-tool-icon">✏️</span> Edit <span class="file-tool-path">test_math.py</span> <span class="edit-replace-all">(replace all)</span></div>
271300
<div class="file-tool-fullpath">/project/tests/test_math.py</div>
272301
<div class="truncatable"><div class="truncatable-content">
273-
<div class="edit-section edit-old"><div class="edit-label"></div><pre class="edit-content">assert subtract(10, 5) == None</pre></div>
274-
<div class="edit-section edit-new"><div class="edit-label">+</div><pre class="edit-content">assert subtract(10, 5) == 5</pre></div>
302+
<div class="edit-section edit-old"><div class="edit-label"></div><pre class="edit-content highlight"><span class="k">assert</span> <span class="n">subtract</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span> <span class="o">==</span> <span class="kc">None</span>
303+
</pre></div>
304+
<div class="edit-section edit-new"><div class="edit-label">+</div><pre class="edit-content highlight"><span class="k">assert</span> <span class="n">subtract</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">5</span><span class="p">)</span> <span class="o">==</span> <span class="mi">5</span>
305+
</pre></div>
275306
</div><button class="expand-btn">Show more</button></div>
276307
</div></div></div>
277308
<div class="message tool-reply" id="msg-2025-12-24T10-03-10-000Z"><div class="message-header"><span class="role-label">Tool reply</span><a href="#msg-2025-12-24T10-03-10-000Z" class="timestamp-link"><time datetime="2025-12-24T10:03:10.000Z" data-timestamp="2025-12-24T10:03:10.000Z">2025-12-24T10:03:10.000Z</time></a></div><div class="message-content"><div class="tool-result"><div class="truncatable"><div class="truncatable-content"><pre>File edited successfully</pre></div><button class="expand-btn">Show more</button></div></div></div></div>

0 commit comments

Comments
 (0)