diff --git a/src/claude_code_transcripts/__init__.py b/src/claude_code_transcripts/__init__.py index 33c322d..f2246a2 100644 --- a/src/claude_code_transcripts/__init__.py +++ b/src/claude_code_transcripts/__init__.py @@ -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; @@ -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 @@ -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); }); diff --git a/tests/test_generate_html.py b/tests/test_generate_html.py index 2fa6e03..32120c5 100644 --- a/tests/test_generate_html.py +++ b/tests/test_generate_html.py @@ -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."""