Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 57 additions & 10 deletions src/claude_code_transcripts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,7 @@ def render_message(log_type, message_json, timestamp):
"""

# JavaScript to fix relative URLs when served via gisthost.github.io or gistpreview.github.io
# Fixes issue #26: Pagination links broken on gisthost.github.io
GIST_PREVIEW_JS = r"""
(function() {
var hostname = window.location.hostname;
Expand All @@ -1056,17 +1057,63 @@ def render_message(log_type, message_json, timestamp):
var match = window.location.search.match(/^\?([^/]+)/);
if (!match) return;
var gistId = match[1];
document.querySelectorAll('a[href]').forEach(function(link) {
var href = link.getAttribute('href');
// Skip external links and anchors
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('//')) return;
// Handle anchor in relative URL (e.g., page-001.html#msg-123)
var parts = href.split('#');
var filename = parts[0];
var anchor = parts.length > 1 ? '#' + parts[1] : '';
link.setAttribute('href', '?' + gistId + '/' + filename + anchor);

function rewriteLinks(root) {
(root || document).querySelectorAll('a[href]').forEach(function(link) {
var href = link.getAttribute('href');
// Skip already-rewritten links (issue #26 fix)
if (href.startsWith('?')) return;
// Skip external links and anchors
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('//')) return;
// Handle anchor in relative URL (e.g., page-001.html#msg-123)
var parts = href.split('#');
var filename = parts[0];
var anchor = parts.length > 1 ? '#' + parts[1] : '';
link.setAttribute('href', '?' + gistId + '/' + filename + anchor);
});
}

// Run immediately
rewriteLinks();

// Also run on DOMContentLoaded in case DOM isn't ready yet
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() { rewriteLinks(); });
}

// Use MutationObserver to catch dynamically added content
// gistpreview.github.io may add content after initial load
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) { // Element node
rewriteLinks(node);
// Also check if the node itself is a link
if (node.tagName === 'A' && node.getAttribute('href')) {
var href = node.getAttribute('href');
if (!href.startsWith('?') && !href.startsWith('http') &&
!href.startsWith('#') && !href.startsWith('//')) {
var parts = href.split('#');
var filename = parts[0];
var anchor = parts.length > 1 ? '#' + parts[1] : '';
node.setAttribute('href', '?' + gistId + '/' + filename + anchor);
}
}
}
});
});
});

// Start observing once body exists
function startObserving() {
if (document.body) {
observer.observe(document.body, { childList: true, subtree: true });
} else {
setTimeout(startObserving, 10);
}
}
startObserving();

// Handle fragment navigation after dynamic content loads
// gisthost.github.io/gistpreview.github.io loads content dynamically, so the browser's
// native fragment navigation fails because the element doesn't exist yet
Expand All @@ -1085,7 +1132,7 @@ def render_message(log_type, message_json, timestamp):
// Try immediately in case content is already loaded
if (!scrollToFragment()) {
// Retry with increasing delays to handle dynamic content loading
var delays = [100, 300, 500, 1000];
var delays = [100, 300, 500, 1000, 2000];
delays.forEach(function(delay) {
setTimeout(scrollToFragment, delay);
});
Expand Down
37 changes: 37 additions & 0 deletions tests/test_generate_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,43 @@ def test_handles_empty_directory(self, output_dir):
inject_gist_preview_js(output_dir)
# Should complete without error

def test_gist_preview_js_skips_already_rewritten_links(self):
"""Test that GIST_PREVIEW_JS skips links that have already been rewritten.

When navigating between pages on gistpreview.github.io, the JS may run
multiple times. Links that have already been rewritten to the
?GIST_ID/filename.html format should be skipped to avoid double-rewriting.

This fixes issue #26 where pagination links break on later pages.
"""
# The JS should check if href already starts with '?'
assert "href.startsWith('?')" in GIST_PREVIEW_JS

def test_gist_preview_js_uses_mutation_observer(self):
"""Test that GIST_PREVIEW_JS uses MutationObserver for dynamic content.

gistpreview.github.io loads content dynamically. When navigating between
pages via SPA-style navigation, new content is inserted without a full
page reload. The JS needs to use MutationObserver to detect and rewrite
links in dynamically added content.

This fixes issue #26 where pagination links break on later pages.
"""
# The JS should use MutationObserver
assert "MutationObserver" in GIST_PREVIEW_JS

def test_gist_preview_js_runs_on_dom_content_loaded(self):
"""Test that GIST_PREVIEW_JS runs on DOMContentLoaded.

The script is injected at the end of the body, but in some cases
(especially on gistpreview.github.io), the DOM might not be fully ready
when the script runs. We should also run on DOMContentLoaded as a fallback.

This fixes issue #26 where pagination links break on later pages.
"""
# The JS should listen for DOMContentLoaded
assert "DOMContentLoaded" in GIST_PREVIEW_JS


class TestCreateGist:
"""Tests for the create_gist function."""
Expand Down