commit 8f15afcad1c73f4e54e7b1bd6778c4b126b57fd4
Author: tri <tri@thac.loan>
Date:   Sat Oct 4 17:03:40 2025 +0700

    line number with anchor links

diff --git a/src/assets/style.css b/src/assets/style.css
index 7cb1499..05e0e37 100644
--- a/src/assets/style.css
+++ b/src/assets/style.css
@@ -19,6 +19,8 @@
   --diff-removed-fg: brown;
   --diff-removed-bg: transparent;
 
+  --selected-line-bg: gainsboro;
+
   --serif-fonts: Charter, "Bitstream Charter", "Sitka Text", Cambria, serif;
   --sans-serif-fonts: system-ui, sans-serif;
   --monospace-fonts:
@@ -29,6 +31,7 @@
   color: var(--fg);
   font-family: var(--serif-fonts);
   font-size: 100%;
+  scroll-behavior: smooth;
 }
 
 .monospace,
@@ -51,7 +54,7 @@ header h1 {
   margin: 0;
 }
 
-table {
+table.fancy {
   text-align: left;
   border-collapse: collapse;
   max-width: 100%;
@@ -59,38 +62,32 @@ table {
 
   white-space: nowrap;
   /*border-top: 2px solid var(--table-border-color);*/
+
+  tbody tr:nth-child(odd) {
+    background-color: var(--table-stripe-bg);
+  }
+  tbody tr:hover {
+    background-color: var(--table-row-hover-bg);
+  }
+  td,
+  th {
+    padding-top: 0.3rem;
+    padding-bottom: 0.3rem;
+  }
+  td + td,
+  th + th {
+    padding-left: 2rem;
+  }
+  td:first-child,
+  th:first-child {
+    padding-left: 0.4rem;
+  }
+  td:last-child,
+  th:last-child {
+    padding-right: 0.4rem;
+  }
 }
-/*
-tbody tr:nth-child(odd) {
-  background-color: var(--table-odd-row-bg);
-}
-tr {
-  border-top: 1px solid var(--table-border-color);
-}
-*/
-tbody tr:nth-child(odd) {
-  background-color: var(--table-stripe-bg);
-}
-tbody tr:hover {
-  background-color: var(--table-row-hover-bg);
-}
-td,
-th {
-  padding-top: 0.3rem;
-  padding-bottom: 0.3rem;
-}
-td + td,
-th + th {
-  padding-left: 2rem;
-}
-td:first-child,
-th:first-child {
-  padding-left: 0.4rem;
-}
-td:last-child,
-th:last-child {
-  padding-right: 0.4rem;
-}
+
 table {
   font-variant-numeric: tabular-nums;
 }
@@ -196,6 +193,7 @@ div.readme-content {
     }
   }
 }
+
 .repo-content {
   border: 1px solid var(--fg);
   margin-top: -1px; /* so nav item's bottom border overlaps its border */
@@ -220,6 +218,26 @@ div.readme-content {
   border: 1px solid var(--table-border-color);
 }
 
+table.text-blob-content {
+  border-collapse: collapse;
+
+  .line-content {
+    white-space: pre;
+    padding-left: 0.5rem;
+  }
+  .line-number {
+    margin-right: 1rem;
+    text-align: right;
+    border-right: 1px solid var(--fg);
+    padding-right: 0.5rem;
+    user-select: none;
+  }
+
+  tr:has(a:target) {
+    background-color: var(--selected-line-bg);
+  }
+}
+
 .commit-content {
   width: fit-content;
   max-width: 100%;
@@ -262,6 +280,8 @@ div.readme-content {
     --diff-removed-fg: lightcoral;
     --diff-removed-bg: transparent;
 
+    --selected-line-bg: #444;
+
     --table-border-color: #444;
     --table-row-hover-bg: #444;
     --table-border-color: #ddd;
diff --git a/src/main.zig b/src/main.zig
index e05e18a..21ef889 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -167,7 +167,7 @@ pub fn writeHomePage(arena: mem.Allocator, dir: fs.Dir, repos: []RepoSummary) !v
     try writer.print(
         \\    <p>listing <b>{d}</b> repos:</p>
         \\    <div style="overflow-x:auto; padding-bottom:1rem">
-        \\      <table>
+        \\      <table class="fancy">
         \\        <thead>
         \\          <tr>
         \\            <th>name</th>
@@ -287,7 +287,7 @@ pub fn writeRepoPage(
     try templates.repo_content_start(writer);
 
     try writer.writeAll(
-        \\      <table>
+        \\      <table class="fancy">
         \\        <thead>
         \\          <tr>
         \\            <th>name</th>
@@ -453,7 +453,7 @@ pub fn writeCommitsPage(
     try templates.repo_content_start(writer);
 
     try writer.print(
-        \\      <table>
+        \\      <table class="fancy">
         \\        <thead>
         \\          <tr>
         \\            <th>hash</th>
@@ -739,7 +739,38 @@ pub fn writeBlobPage(
             \\
         );
     } else {
-        try html.escape(writer, file_content);
+        try writer.writeAll(
+            \\<table class="text-blob-content">
+            \\  <tbody>
+            \\
+        );
+
+        const escaped = try html.escapeAlloc(arena, file_content);
+        var lines = mem.splitScalar(u8, escaped, '\n');
+        var line_number: usize = 0;
+        while (lines.next()) |line| {
+            if (line.len == 0 and lines.peek() == null) {
+                break;
+            }
+
+            line_number += 1;
+            try writer.writeAll(
+                \\<tr>
+            );
+
+            try writer.print(
+                \\<td class="line-number"><a href="#L{0d}" id="L{0d}">{0d}</a></td><td class="line-content">{1s}</td>
+            , .{ line_number, line });
+
+            try writer.writeAll(
+                \\</tr>
+            );
+        }
+
+        try writer.writeAll(
+            \\  </tbody>
+            \\</table>
+        );
     }
 
     try writer.writeAll(