patx/gitman
render line numbers client side instead of server side for file.tpl
Commit 821a3a7 · patx · 2026-05-06T14:44:56-04:00
Comments
No comments yet.
Diff
diff --git a/app.py b/app.py
index d8d9be1..55ab332 100644
--- a/app.py
+++ b/app.py
@@ -3556,7 +3556,6 @@ def repo_source(owner, repo_name, file_path=""):
content = read_file_bytes(path, file_path, revision=revision)
is_binary = b"\0" in content[:4096]
text_content = content.decode("utf-8", "replace") if not is_binary else ""
- line_count = max(1, text_content.count("\n") + (0 if text_content.endswith("\n") else 1))
return render(
"file.tpl",
repo=repo,
@@ -3564,7 +3563,6 @@ def repo_source(owner, repo_name, file_path=""):
content=text_content,
is_binary=is_binary,
language_class=highlight_language_class(file_path),
- line_numbers="\n".join(str(line_number) for line_number in range(1, line_count + 1)),
size=len(content),
quote_path=quote_path,
**repo_page_context(repo, path, selected_ref=selected_ref),
diff --git a/static/styles.css b/static/styles.css
index 111f299..41fb15f 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -61,8 +61,8 @@ pre, .clone-box code { overflow-x: auto; padding: .6rem; border: 1px solid var(-
pre code.hljs { padding: 0; background: transparent; }
.copyable-code {
position: relative;
- display: grid;
- grid-template-columns: max-content minmax(0, 1fr);
+ display: flex;
+ align-items: stretch;
border: 1px solid var(--border);
background: var(--soft-surface);
}
@@ -71,14 +71,23 @@ pre code.hljs { padding: 0; background: transparent; }
border: 0;
background: transparent;
}
-.copyable-code pre.code { padding: 2.1rem .6rem .6rem; }
+.copyable-code pre.code {
+ flex: 1 1 auto;
+ min-width: 0;
+ padding: 2.1rem .6rem .6rem;
+}
.line-numbers {
+ display: flex;
+ flex: 0 0 auto;
+ flex-direction: column;
padding: 2.1rem .55rem .6rem .6rem;
color: var(--muted);
text-align: right;
user-select: none;
border-right: 1px solid var(--border);
- overflow: hidden;
+}
+.line-numbers span {
+ display: block;
}
.copy-button {
position: absolute;
diff --git a/templates/base.tpl b/templates/base.tpl
index a6bc22e..db0fb2f 100644
--- a/templates/base.tpl
+++ b/templates/base.tpl
@@ -48,9 +48,45 @@
</main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>
- if (window.hljs) {
- document.querySelectorAll("pre code").forEach((block) => hljs.highlightElement(block));
- }
+ (() => {
+ const addLineNumbers = (code, rawText) => {
+ const viewer = code.closest("[data-code-viewer]");
+ const pre = code.closest("pre.code");
+ if (!viewer || !pre || viewer.querySelector(".line-numbers")) return;
+
+ const text = rawText ?? code.textContent ?? "";
+ const lineCount = Math.max(1, text.split("\n").length - (text.endsWith("\n") ? 1 : 0));
+ const lineNumbers = document.createElement("div");
+ const fragment = document.createDocumentFragment();
+ lineNumbers.className = "line-numbers";
+ lineNumbers.setAttribute("aria-hidden", "true");
+
+ for (let line = 1; line <= lineCount; line += 1) {
+ const number = document.createElement("span");
+ number.textContent = line;
+ fragment.append(number);
+ }
+
+ lineNumbers.append(fragment);
+ viewer.insertBefore(lineNumbers, pre);
+ };
+
+ const blocks = document.querySelectorAll("pre code");
+ if (window.hljs) {
+ if (hljs.addPlugin) {
+ hljs.addPlugin({
+ "after:highlightElement": ({ el, text }) => addLineNumbers(el, text),
+ });
+ }
+ blocks.forEach((block) => {
+ hljs.highlightElement(block);
+ addLineNumbers(block);
+ });
+ return;
+ }
+
+ document.querySelectorAll("[data-code-viewer] pre.code code").forEach((block) => addLineNumbers(block));
+ })();
</script>
<script>
(() => {
diff --git a/templates/file.tpl b/templates/file.tpl
index 0d106b7..1d5c747 100644
--- a/templates/file.tpl
+++ b/templates/file.tpl
@@ -29,7 +29,7 @@
% if is_binary:
<p class="empty">Binary file, {{size}} bytes. Use the raw view to download it.</p>
% else:
- <div class="copyable-code">
+ <div class="copyable-code" data-code-viewer>
<button
class="copy-button"
type="button"
@@ -43,7 +43,6 @@
</svg>
<span class="sr-only">Copy file contents</span>
</button>
- <pre class="line-numbers" aria-hidden="true">{{line_numbers}}</pre>
<pre class="code"><code class="{{language_class}}">{{content}}</code></pre>
</div>
% end
diff --git a/tests/test_app.py b/tests/test_app.py
index 6b21f6f..d4edc8e 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -479,7 +479,7 @@ def test_readme_preview_is_truncated_but_source_files_render_fully(isolated_app,
assert response.status_code == 200
assert "File preview truncated." not in response.text
assert readme_content in response.text
- assert '<pre class="line-numbers" aria-hidden="true">1\n2\n3\n4</pre>' in response.text
+ assert '<div class="copyable-code" data-code-viewer>' in response.text
def test_build_tree_deduplicates_entries_and_sorts_directories_first():