Files

2124 lines
89 KiB
JavaScript
Raw Permalink Normal View History

2026-02-25 11:35:40 +00:00
/* GCOVR Custom JavaScript - Tree View & Interactivity */
(function() {
'use strict';
// Wait for DOM ready
document.addEventListener('DOMContentLoaded', function() {
initTheme();
initSidebar();
initSidebarResize();
initMobileMenu();
initFileTree();
initNavOverride();
initBreadcrumbs();
initSearch();
initFunctionRows();
initSorting();
initToggleButtons();
initCoverageNav();
initTreeControls();
initViewToggle();
initSettingsDropdown();
initTlaNavigation();
initLineHighlight();
initColumnToggles();
initPopupResize();
initFileNavTooltips();
initFileNavKeys();
initFunctionListPersistence();
// Reveal page now that all init is done
document.documentElement.classList.remove('no-transitions');
// Prefetch linked pages on hover for instant navigation
initPrefetch();
});
// ===========================================
// Breadcrumb Links
// ===========================================
// Find a node in the tree by its link (HTML filename) and return
// the full ancestor path as an array of nodes from root to target.
function findPathInTree(nodes, targetLink) {
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.link === targetLink) {
return [node];
}
if (node.children) {
var childPath = findPathInTree(node.children, targetLink);
if (childPath) {
return [node].concat(childPath);
}
}
}
return null;
}
function initBreadcrumbs() {
var currentSpan = document.querySelector('.breadcrumb .current');
if (!currentSpan || !window.GCOVR_TREE_DATA) {
if (currentSpan) currentSpan.classList.add('ready');
return;
}
// Find current page in tree by its HTML filename — this is unambiguous
// since each page only appears once in the tree.
var currentPage = window.location.pathname.split('/').pop() || 'index.html';
var treePath = findPathInTree(window.GCOVR_TREE_DATA, currentPage);
if (!treePath || treePath.length === 0) {
currentSpan.classList.add('ready');
return;
}
// Build breadcrumb from the tree path (ancestor nodes → current node)
var fragment = document.createDocumentFragment();
var matchedSegments = [];
// Fill an element with the segments of a (possibly joined) name like
// "boost/url", rendering "boost", a separator, "url". Used so a joined
// directory shows its segments inline yet remains one hyperlink target.
function appendSegments(parentEl, name) {
var segments = name.split('/');
for (var k = 0; k < segments.length; k++) {
if (k > 0) {
var inner = document.createElement('span');
inner.className = 'separator';
inner.textContent = '/';
parentEl.appendChild(inner);
}
parentEl.appendChild(document.createTextNode(segments[k]));
}
}
for (var i = 0; i < treePath.length; i++) {
var node = treePath[i];
var isLast = (i === treePath.length - 1);
if (i > 0) {
var sep = document.createElement('span');
sep.className = 'separator';
sep.textContent = '/';
fragment.appendChild(sep);
}
matchedSegments.push(node.name);
if (node.link && !isLast) {
var a = document.createElement('a');
a.href = node.link;
appendSegments(a, node.name);
fragment.appendChild(a);
} else {
var span = document.createElement('span');
span.className = 'current-file';
appendSegments(span, node.name);
fragment.appendChild(span);
}
}
currentSpan.innerHTML = '';
currentSpan.appendChild(fragment);
currentSpan.classList.add('ready');
// Update source-filename to match breadcrumb path
var sourceFilename = document.querySelector('.source-filename');
if (sourceFilename) {
sourceFilename.textContent = matchedSegments.join('/');
}
}
// ===========================================
// Theme Toggle
// ===========================================
function initTheme() {
const toggle = document.getElementById('theme-toggle');
const iconSun = toggle ? toggle.querySelector('.icon-sun') : null;
const iconMoon = toggle ? toggle.querySelector('.icon-moon') : null;
// Get system preference
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
// Get effective theme: saved preference or OS default
function getEffectiveTheme() {
var saved = localStorage.getItem('gcovr-theme');
return (saved === 'light' || saved === 'dark') ? saved : getSystemTheme();
}
// Apply theme to document
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
if (iconSun) iconSun.style.display = (theme === 'dark') ? 'block' : 'none';
if (iconMoon) iconMoon.style.display = (theme === 'light') ? 'block' : 'none';
}
// Apply current theme
applyTheme(getEffectiveTheme());
// Listen for system theme changes — only apply if no stored preference
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function() {
var saved = localStorage.getItem('gcovr-theme');
if (saved !== 'light' && saved !== 'dark') {
applyTheme(getSystemTheme());
}
});
// Toggle between light and dark on click
if (toggle) {
toggle.addEventListener('click', function() {
var current = getEffectiveTheme();
var next = (current === 'dark') ? 'light' : 'dark';
localStorage.setItem('gcovr-theme', next);
applyTheme(next);
});
}
}
// ===========================================
// Tree Controls (Expand/Collapse All)
// ===========================================
function initTreeControls() {
var expandBtn = document.getElementById('expand-all');
var collapseBtn = document.getElementById('collapse-all');
if (expandBtn) {
expandBtn.addEventListener('click', function() {
document.querySelectorAll('.tree-item').forEach(function(item) {
if (!item.classList.contains('no-children')) {
item.classList.add('expanded');
var toggle = item.querySelector(':scope > .tree-item-header > .tree-folder-toggle');
if (toggle) toggle.textContent = '';
}
});
saveExpandedFolders();
});
}
if (collapseBtn) {
collapseBtn.addEventListener('click', function() {
document.querySelectorAll('.tree-item').forEach(function(item) {
item.classList.remove('expanded');
var toggle = item.querySelector(':scope > .tree-item-header > .tree-folder-toggle');
if (toggle) toggle.textContent = '+';
});
saveExpandedFolders();
});
}
}
// ===========================================
// Sidebar Toggle
// ===========================================
function initSidebar() {
const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('sidebar-toggle');
const header = sidebar ? sidebar.querySelector('.sidebar-header') : null;
if (!sidebar) return;
// Load saved state
const isCollapsed = localStorage.getItem('sidebar-collapsed') === 'true';
if (isCollapsed) {
sidebar.classList.add('collapsed');
}
// Toggle button
if (toggle) {
toggle.addEventListener('click', function() {
sidebar.classList.toggle('collapsed');
sidebar.classList.remove('hover-expand');
var isNowCollapsed = sidebar.classList.contains('collapsed');
localStorage.setItem('sidebar-collapsed', isNowCollapsed);
// Restore custom width when un-collapsing
if (!isNowCollapsed) {
var savedWidth = localStorage.getItem('gcovr-sidebar-width');
if (savedWidth) {
document.documentElement.style.setProperty('--sidebar-width', savedWidth + 'px');
}
}
});
}
// Hover expand - expands when hovering sidebar content (not header or no-expand zones)
var hoverTimeout = null;
var HOVER_DELAY = 150; // ms delay before expanding
var isOverContent = false;
// Check if element is within a no-expand zone
function isInNoExpandZone(el) {
while (el && el !== sidebar) {
if (el.classList && el.classList.contains('no-expand')) {
return true;
}
el = el.parentElement;
}
return false;
}
function scheduleExpand() {
if (hoverTimeout) return; // already scheduled
if (sidebar.classList.contains('hover-expand')) return; // already expanded
hoverTimeout = setTimeout(function() {
if (isOverContent) {
sidebar.classList.add('hover-expand');
}
hoverTimeout = null;
}, HOVER_DELAY);
}
function cancelExpand() {
if (hoverTimeout) {
clearTimeout(hoverTimeout);
hoverTimeout = null;
}
sidebar.classList.remove('hover-expand');
}
sidebar.addEventListener('mouseenter', function(e) {
if (!sidebar.classList.contains('collapsed')) return;
// Check if entering over content area (not header or no-expand zones)
if (!header.contains(e.target) && !isInNoExpandZone(e.target)) {
isOverContent = true;
scheduleExpand();
}
});
sidebar.addEventListener('mousemove', function(e) {
if (!sidebar.classList.contains('collapsed')) return;
var wasOverContent = isOverContent;
isOverContent = !header.contains(e.target) && !isInNoExpandZone(e.target);
if (isOverContent && !wasOverContent && !sidebar.classList.contains('hover-expand')) {
scheduleExpand();
}
});
sidebar.addEventListener('mouseleave', function() {
isOverContent = false;
cancelExpand();
});
}
// ===========================================
// Sidebar Resize
// ===========================================
function initSidebarResize() {
var sidebar = document.getElementById('sidebar');
var handle = document.getElementById('sidebar-resize-handle');
if (!sidebar || !handle) return;
var MIN_WIDTH = 200;
var startX, startWidth;
// Restore saved width
var savedWidth = localStorage.getItem('gcovr-sidebar-width');
if (savedWidth && !sidebar.classList.contains('collapsed')) {
var w = parseInt(savedWidth, 10);
if (w >= MIN_WIDTH) {
document.documentElement.style.setProperty('--sidebar-width', w + 'px');
}
}
function getMaxWidth() {
return Math.floor(window.innerWidth * 0.5);
}
function onMouseMove(e) {
var newWidth = startWidth + (e.clientX - startX);
var maxW = getMaxWidth();
if (newWidth < MIN_WIDTH) newWidth = MIN_WIDTH;
if (newWidth > maxW) newWidth = maxW;
document.documentElement.style.setProperty('--sidebar-width', newWidth + 'px');
}
function onMouseUp() {
document.body.classList.remove('sidebar-resizing');
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// Save the current width
var computed = parseInt(getComputedStyle(sidebar).width, 10);
localStorage.setItem('gcovr-sidebar-width', computed);
}
handle.addEventListener('mousedown', function(e) {
if (sidebar.classList.contains('collapsed')) return;
e.preventDefault();
startX = e.clientX;
startWidth = parseInt(getComputedStyle(sidebar).width, 10);
document.body.classList.add('sidebar-resizing');
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
// Double-click to reset to default width
var DEFAULT_WIDTH = 320;
handle.addEventListener('dblclick', function() {
if (sidebar.classList.contains('collapsed')) return;
document.documentElement.style.setProperty('--sidebar-width', DEFAULT_WIDTH + 'px');
localStorage.setItem('gcovr-sidebar-width', DEFAULT_WIDTH);
});
}
// ===========================================
// Mobile Menu
// ===========================================
function initMobileMenu() {
var sidebar = document.getElementById('sidebar');
var menuBtn = document.getElementById('mobile-menu-btn');
var backdrop = document.getElementById('sidebar-backdrop');
if (!menuBtn || !sidebar) return;
// Open sidebar on hamburger click
menuBtn.addEventListener('click', function() {
sidebar.classList.add('mobile-open');
});
// Close on backdrop click
if (backdrop) {
backdrop.addEventListener('click', function() {
sidebar.classList.remove('mobile-open');
});
}
// Close when clicking a navigation link
sidebar.addEventListener('click', function(e) {
if (e.target.closest('a[href]')) {
sidebar.classList.remove('mobile-open');
}
});
// Close on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && sidebar.classList.contains('mobile-open')) {
sidebar.classList.remove('mobile-open');
}
});
}
// ===========================================
// File Tree - Load from tree.json
// ===========================================
function initFileTree() {
var treeContainer = document.getElementById('file-tree');
if (!treeContainer) return;
// Tree data is produced already-normalized and already-sorted by the
// upstream tooling (Python's gcovr_build_tree.py or gcovr itself), so
// no normalize/sort pass is needed here. We just join single-child
// dir chains for sidebar compactness before rendering.
joinSingleChildDirs(window.GCOVR_TREE_DATA);
sortTree(window.GCOVR_TREE_DATA);
renderTree(treeContainer, window.GCOVR_TREE_DATA);
}
// Re-sort the tree: directories first, then files, alphabetically within
// each group. Python already sorts each level, but normalizeTree creates
// synthetic directory nodes from multi-segment FILE entries (e.g. a deep
// chain like subdir1/subdir2/subdir3/file.hpp that gcovr itself collapsed).
// Those synthetic dirs end up wherever the originating file landed in the
// Python sort — i.e. in the file bucket — so without this pass they appear
// mixed in with the files instead of at the top with the other directories.
function sortTree(nodes) {
if (!nodes || nodes.length === 0) return;
nodes.sort(function(a, b) {
var aIsDir = a.isDirectory || (a.children && a.children.length > 0);
var bIsDir = b.isDirectory || (b.children && b.children.length > 0);
if (aIsDir && !bIsDir) return -1;
if (!aIsDir && bIsDir) return 1;
var aName = (a.name || '').toLowerCase();
var bName = (b.name || '').toLowerCase();
return aName.localeCompare(bName);
});
for (var i = 0; i < nodes.length; i++) {
if (nodes[i].children) sortTree(nodes[i].children);
}
}
// Join chains of single-child directories into one sidebar entry.
// If a directory contains nothing but one child directory, the two are
// merged: the name becomes "parent/child", and the link + stats are taken
// from the (deepest) child. The grandchildren become the new children.
// Repeats until the chain ends (multiple children, or a file appears).
// Result: e.g. include > boost > url > [files] becomes include/boost/url
// as a single entry whose click navigates straight to the url directory.
function joinSingleChildDirs(nodes) {
if (!nodes) return;
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (!node.isDirectory || !node.children) continue;
while (node.children.length === 1 && node.children[0].isDirectory) {
var child = node.children[0];
node.name = node.name + '/' + child.name;
// The joined entry represents the deepest directory for clicks
// and for the coverage stats shown next to it. Statically-named
// assignments avoid the dynamic [key] indexing that static
// analyzers flag as a prototype-pollution risk.
if (child.link) node.link = child.link;
if (child.coverage) node.coverage = child.coverage;
if (child.coverageClass) node.coverageClass = child.coverageClass;
if (child.linesTotal) node.linesTotal = child.linesTotal;
if (child.linesExec) node.linesExec = child.linesExec;
if (child.linesCoverage) node.linesCoverage = child.linesCoverage;
if (child.linesClass) node.linesClass = child.linesClass;
if (child.functionsCoverage) node.functionsCoverage = child.functionsCoverage;
if (child.functionsClass) node.functionsClass = child.functionsClass;
if (child.branchesCoverage) node.branchesCoverage = child.branchesCoverage;
if (child.branchesClass) node.branchesClass = child.branchesClass;
node.children = child.children || [];
}
joinSingleChildDirs(node.children);
}
}
// Save expanded folder paths to localStorage
function saveExpandedFolders() {
var paths = [];
document.querySelectorAll('.tree-item.expanded[data-tree-path]').forEach(function(el) {
paths.push(el.getAttribute('data-tree-path'));
});
localStorage.setItem('gcovr-expanded-folders', JSON.stringify(paths));
}
function renderTree(container, tree) {
container.innerHTML = '';
if (!tree || tree.length === 0) {
container.innerHTML = '<div class="tree-loading">No files found</div>';
return;
}
tree.forEach(function(item) {
container.appendChild(createTreeItem(item, ''));
});
// Auto-expand to current file and highlight it
expandToCurrentFile(container);
}
function expandToCurrentFile(container) {
// Get current page filename
var currentPage = window.location.pathname.split('/').pop() || 'index.html';
// Find the link matching current page
var currentLink = container.querySelector('a[href="' + currentPage + '"]');
if (currentLink) {
// Mark as active
var treeItem = currentLink.closest('.tree-item');
if (treeItem) {
treeItem.classList.add('active');
}
// Expand all parent folders
var parent = currentLink.closest('.tree-children');
while (parent) {
var parentItem = parent.closest('.tree-item');
if (parentItem) {
parentItem.classList.add('expanded');
var toggle = parentItem.querySelector(':scope > .tree-item-header > .tree-folder-toggle');
if (toggle) toggle.textContent = '';
}
parent = parentItem ? parentItem.parentElement.closest('.tree-children') : null;
}
}
// Restore previously expanded folders from localStorage
try {
var saved = localStorage.getItem('gcovr-expanded-folders');
if (saved) {
var paths = JSON.parse(saved);
paths.forEach(function(path) {
var el = container.querySelector('.tree-item[data-tree-path="' + CSS.escape(path) + '"]');
if (el && !el.classList.contains('no-children')) {
el.classList.add('expanded');
var toggle = el.querySelector(':scope > .tree-item-header > .tree-folder-toggle');
if (toggle) toggle.textContent = '';
}
});
}
} catch (e) {
// Ignore localStorage errors
}
// Scroll active item into view instantly
if (currentLink) {
currentLink.scrollIntoView({ block: 'center', behavior: 'instant' });
}
}
// Clean relative path prefixes like '../../../' from names
function cleanPathName(name) {
if (!name) return 'unknown';
// Remove leading ./ or ../
while (name.indexOf('./') === 0 || name.indexOf('../') === 0) {
if (name.indexOf('./') === 0) {
name = name.substring(2);
} else if (name.indexOf('../') === 0) {
name = name.substring(3);
}
}
return name || 'unknown';
}
// Get just the filename from a path
function getDisplayName(name) {
var cleaned = cleanPathName(name);
var lastSlash = cleaned.lastIndexOf('/');
return lastSlash >= 0 ? cleaned.substring(lastSlash + 1) : cleaned;
}
function createTreeItem(item, parentPath) {
var hasChildren = item.children && item.children.length > 0;
var isDirectory = item.isDirectory || hasChildren;
var cleanedName = cleanPathName(item.name);
var treePath = parentPath ? (parentPath + '/' + cleanedName) : cleanedName;
var div = document.createElement('div');
div.className = 'tree-item' + (isDirectory ? ' is-folder' : '') + (hasChildren ? '' : ' no-children');
div.setAttribute('data-tree-path', treePath);
var header = document.createElement('div');
header.className = 'tree-item-header';
var toggle = null;
// Toggle button (+/-) for folders with children
if (hasChildren) {
toggle = document.createElement('button');
toggle.className = 'tree-folder-toggle';
toggle.textContent = '+';
toggle.setAttribute('aria-label', 'Toggle folder');
toggle.addEventListener('click', function(e) {
e.stopPropagation();
e.preventDefault();
var isExpanded = div.classList.toggle('expanded');
toggle.textContent = isExpanded ? '' : '+';
saveExpandedFolders();
});
header.appendChild(toggle);
// Make entire header clickable to expand/collapse
header.style.cursor = 'pointer';
header.addEventListener('click', function(e) {
// If clicking directly on a link, let it navigate
if (e.target.closest('a')) return;
e.preventDefault();
var isExpanded = div.classList.toggle('expanded');
toggle.textContent = isExpanded ? '' : '+';
saveExpandedFolders();
});
} else {
var spacer = document.createElement('span');
spacer.className = 'tree-spacer';
header.appendChild(spacer);
}
// Icon - different for folders vs files
var icon = document.createElement('span');
if (isDirectory) {
icon.className = 'tree-icon tree-icon-folder';
icon.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M1.75 1A1.75 1.75 0 000 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25v-8.5A1.75 1.75 0 0014.25 3H7.5a.25.25 0 01-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75z"/></svg>';
} else {
icon.className = 'tree-icon tree-icon-file';
icon.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M3.75 1.5a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 019 4.25V1.5H3.75zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-2.938-2.938zM2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16h-9.5A1.75 1.75 0 012 14.25V1.75z"/></svg>';
}
header.appendChild(icon);
// Label (with link if available)
// Use the full cleaned name so joined-dir entries like "boost/url"
// display as a single multi-segment label in the sidebar.
var displayName = cleanPathName(item.name);
var tooltipText = cleanPathName(item.fullPath || item.name);
var label = document.createElement('span');
label.className = 'tree-label';
label.title = tooltipText;
if (item.link) {
var link = document.createElement('a');
link.href = item.link;
link.textContent = displayName;
link.title = tooltipText;
label.appendChild(link);
} else {
label.textContent = displayName;
}
header.appendChild(label);
div.appendChild(header);
// Children container (for expand/collapse)
if (hasChildren) {
var childrenWrapper = document.createElement('div');
childrenWrapper.className = 'tree-children';
var childrenInner = document.createElement('div');
childrenInner.className = 'tree-children-inner';
item.children.forEach(function(child) {
childrenInner.appendChild(createTreeItem(child, treePath));
});
childrenWrapper.appendChild(childrenInner);
div.appendChild(childrenWrapper);
}
return div;
}
// ===========================================
// Search
// ===========================================
function initSearch() {
const searchInput = document.getElementById('file-search');
const fileTree = document.getElementById('file-tree');
const clearBtn = document.getElementById('search-clear');
const searchContainer = searchInput ? searchInput.closest('.sidebar-search') : null;
if (!searchInput || !fileTree) return;
// Store pre-search expanded state so we can restore it
var preSearchExpanded = null;
// Create no-results message
var noResults = document.createElement('div');
noResults.className = 'search-no-results';
noResults.textContent = 'No matching files';
noResults.style.display = 'none';
fileTree.appendChild(noResults);
function updateClearButton() {
if (searchContainer) {
searchContainer.classList.toggle('has-query', searchInput.value.trim() !== '');
}
}
// Clear button
if (clearBtn) {
clearBtn.addEventListener('click', function() {
searchInput.value = '';
sessionStorage.removeItem('gcovr-search');
updateClearButton();
performSearch('');
searchInput.focus();
});
}
var debounceTimer = null;
searchInput.addEventListener('input', function() {
updateClearButton();
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
var val = searchInput.value;
if (val.trim() !== '') {
sessionStorage.setItem('gcovr-search', val);
} else {
sessionStorage.removeItem('gcovr-search');
}
performSearch(val);
}, 150);
});
// Restore search state from sessionStorage on page load (synchronous
// since initFileTree has already built the tree before initSearch runs)
var savedSearch = sessionStorage.getItem('gcovr-search');
if (savedSearch) {
searchInput.value = savedSearch;
updateClearButton();
performSearch(savedSearch);
}
function performSearch(value) {
var query = value.toLowerCase().trim();
var allItems = fileTree.querySelectorAll('.tree-item');
// Clear all highlights
fileTree.querySelectorAll('.search-highlight').forEach(function(mark) {
var parent = mark.parentNode;
parent.replaceChild(document.createTextNode(mark.textContent), mark);
parent.normalize();
});
// If query is empty, restore original state
if (query === '') {
noResults.style.display = 'none';
allItems.forEach(function(item) {
item.style.display = '';
item.classList.remove('search-match');
});
// Restore pre-search expanded state
if (preSearchExpanded !== null) {
allItems.forEach(function(item) {
var path = item.getAttribute('data-tree-path');
var toggle = item.querySelector(':scope > .tree-item-header > .tree-folder-toggle');
if (toggle) {
if (preSearchExpanded.indexOf(path) >= 0) {
item.classList.add('expanded');
toggle.textContent = '\u2212';
} else {
item.classList.remove('expanded');
toggle.textContent = '+';
}
}
});
preSearchExpanded = null;
}
return;
}
// Save expanded state before first search
if (preSearchExpanded === null) {
preSearchExpanded = [];
allItems.forEach(function(item) {
if (item.classList.contains('expanded')) {
preSearchExpanded.push(item.getAttribute('data-tree-path'));
}
});
}
// Determine which items match (check full path and display name)
var matchSet = new Set();
allItems.forEach(function(item) {
var path = (item.getAttribute('data-tree-path') || '').toLowerCase();
var label = item.querySelector(':scope > .tree-item-header > .tree-label');
var text = label ? label.textContent.toLowerCase() : '';
if (path.includes(query) || text.includes(query)) {
matchSet.add(item);
}
});
// Also mark all ancestor items of matches as visible
var visibleSet = new Set(matchSet);
matchSet.forEach(function(item) {
var parent = item.parentElement;
while (parent && parent !== fileTree) {
if (parent.classList && parent.classList.contains('tree-item')) {
visibleSet.add(parent);
}
parent = parent.parentElement;
}
});
// Apply visibility, expand parents of matches, highlight text
var anyVisible = false;
allItems.forEach(function(item) {
var isVisible = visibleSet.has(item);
item.style.display = isVisible ? '' : 'none';
item.classList.toggle('search-match', matchSet.has(item));
if (isVisible) {
anyVisible = true;
// Auto-expand folders that contain matches
var toggle = item.querySelector(':scope > .tree-item-header > .tree-folder-toggle');
if (toggle && visibleSet.has(item) && !matchSet.has(item) || (toggle && matchSet.has(item) && item.classList.contains('is-folder'))) {
item.classList.add('expanded');
toggle.textContent = '\u2212';
}
}
// Highlight matched text in label
if (matchSet.has(item)) {
var label = item.querySelector(':scope > .tree-item-header > .tree-label');
if (label) {
highlightText(label, query);
}
}
});
noResults.style.display = anyVisible ? 'none' : '';
}
function highlightText(container, query) {
// Walk text nodes inside the label (may be inside an <a> tag)
var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
var textNodes = [];
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
textNodes.forEach(function(node) {
var text = node.textContent;
var lowerText = text.toLowerCase();
var idx = lowerText.indexOf(query);
if (idx === -1) return;
var frag = document.createDocumentFragment();
var lastIdx = 0;
while (idx !== -1) {
if (idx > lastIdx) {
frag.appendChild(document.createTextNode(text.substring(lastIdx, idx)));
}
var mark = document.createElement('mark');
mark.className = 'search-highlight';
mark.textContent = text.substring(idx, idx + query.length);
frag.appendChild(mark);
lastIdx = idx + query.length;
idx = lowerText.indexOf(query, lastIdx);
}
if (lastIdx < text.length) {
frag.appendChild(document.createTextNode(text.substring(lastIdx)));
}
node.parentNode.replaceChild(frag, node);
});
}
}
// ===========================================
// Progressive Function Row Rendering
// ===========================================
function initFunctionRows() {
var dataEl = document.getElementById('functions-data');
if (!dataEl) return;
var config = window.__functionsPageConfig || {};
var data = JSON.parse(dataEl.textContent);
var container = document.querySelector('.functions-body');
var loadingEl = document.getElementById('functions-loading');
var showBranches = config.showBranches;
var showConditions = config.showConditions;
var singlePage = config.singlePage;
var currentFile = config.htmlFilename || '';
if (data.length === 0) {
if (loadingEl) loadingEl.remove();
return;
}
// --- Virtual scrolling setup ---
var ROW_HEIGHT = 52;
var BUFFER = 10;
var visibleCount = Math.max(30, Math.ceil(container.clientHeight / ROW_HEIGHT) + BUFFER * 2);
var viewport, visibleEl;
var lastStartIdx = -1;
window.addEventListener('resize', function() {
visibleCount = Math.max(30, Math.ceil(container.clientHeight / ROW_HEIGHT) + BUFFER * 2);
lastStartIdx = -1;
renderVisible();
});
function buildHref(entry) {
if (singlePage) return '#' + entry.html_filename + '|l' + entry.line;
if (currentFile !== entry.html_filename) return entry.html_filename + '#l' + entry.line;
return '#l' + entry.line;
}
function entryKey(entry) {
return entry.name + '|' + entry.filename + ':' + entry.line;
}
function el(tag, cls, text) {
var node = document.createElement(tag);
if (cls) node.className = cls;
if (text !== undefined) node.textContent = text;
return node;
}
function createRow(entry) {
var row = el('div', 'function-row');
if (highlightKey && entryKey(entry) === highlightKey) {
row.classList.add('function-row-visited');
}
// col-function
var colFn = el('div', 'col-function');
var a = document.createElement('a');
a.href = buildHref(entry);
a.appendChild(el('span', 'function-name', entry.name));
a.appendChild(el('span', 'function-location', entry.filename + ':' + entry.line));
colFn.appendChild(a);
row.appendChild(colFn);
// col-calls
var colCalls = el('div', 'col-calls');
var callSpan;
if (entry.excluded) {
callSpan = el('span', 'excluded', 'excluded');
} else if (entry.execution_count === 0) {
callSpan = el('span', 'not-called', 'not called');
} else {
callSpan = el('span', 'called', entry.execution_count + 'x');
}
colCalls.appendChild(callSpan);
row.appendChild(colCalls);
// col-lines
row.appendChild(el('div', 'col-lines', entry.line_coverage + '%'));
// col-branches (optional)
if (showBranches) {
row.appendChild(el('div', 'col-branches', entry.branch_coverage + '%'));
}
// col-conditions (optional)
if (showConditions) {
row.appendChild(el('div', 'col-conditions', entry.condition_coverage + '%'));
}
return row;
}
function setupVirtualScroll() {
if (loadingEl) loadingEl.remove();
viewport = document.createElement('div');
viewport.className = 'functions-viewport';
viewport.style.height = (data.length * ROW_HEIGHT) + 'px';
visibleEl = document.createElement('div');
visibleEl.className = 'functions-visible';
viewport.appendChild(visibleEl);
container.appendChild(viewport);
}
function renderVisible() {
var scrollTop = container.scrollTop;
var startIdx = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER);
var endIdx = Math.min(data.length, startIdx + visibleCount + BUFFER);
// Skip re-render if the window hasn't shifted
if (startIdx === lastStartIdx) return;
lastStartIdx = startIdx;
visibleEl.style.top = (startIdx * ROW_HEIGHT) + 'px';
var frag = document.createDocumentFragment();
for (var i = startIdx; i < endIdx; i++) {
frag.appendChild(createRow(data[i]));
}
visibleEl.replaceChildren(frag);
}
// --- Scroll listener (rAF-throttled) ---
var ticking = false;
container.addEventListener('scroll', function() {
if (!ticking) {
requestAnimationFrame(function() { renderVisible(); ticking = false; });
ticking = true;
}
});
// --- Save state on navigation for back-button restore ---
var highlightKey = null;
container.addEventListener('click', function(e) {
var row = e.target.closest('.function-row');
if (!row) return;
var link = row.querySelector('a');
if (!link) return;
var nameEl = row.querySelector('.function-name');
var locEl = row.querySelector('.function-location');
if (nameEl && locEl) {
sessionStorage.setItem('gcovr-functions-clicked', nameEl.textContent + '|' + locEl.textContent);
}
sessionStorage.setItem('gcovr-functions-scrollTop', String(container.scrollTop));
});
// --- Data-level sorting ---
function sortData(key, ascending) {
data.sort(function(a, b) {
var aVal, bVal;
switch (key) {
case 'name': aVal = a.name; bVal = b.name; break;
case 'calls': aVal = a.excluded ? -1 : a.execution_count; bVal = b.excluded ? -1 : b.execution_count; break;
case 'lines': aVal = parseFloat(a.line_coverage) || 0; bVal = parseFloat(b.line_coverage) || 0; break;
case 'branches': aVal = parseFloat(a.branch_coverage) || 0; bVal = parseFloat(b.branch_coverage) || 0; break;
case 'conditions': aVal = parseFloat(a.condition_coverage) || 0; bVal = parseFloat(b.condition_coverage) || 0; break;
default: aVal = a.name; bVal = b.name;
}
if (typeof aVal === 'string' && typeof bVal === 'string') {
return ascending ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
}
return ascending ? aVal - bVal : bVal - aVal;
});
lastStartIdx = -1; // force re-render
viewport.style.height = (data.length * ROW_HEIGHT) + 'px';
renderVisible();
}
// Intercept sort clicks on functions-header before initSorting runs
var funcHeaders = document.querySelectorAll('.functions-header .sortable');
funcHeaders.forEach(function(header) {
header.addEventListener('click', function(e) {
e.stopPropagation();
var sortKey = this.dataset.sort;
var isAscending = this.classList.contains('sorted-ascending');
// Update header classes
funcHeaders.forEach(function(h) {
h.classList.remove('sorted-ascending', 'sorted-descending');
});
this.classList.add(isAscending ? 'sorted-descending' : 'sorted-ascending');
sortData(sortKey, !isAscending);
}, true); // capture phase to beat initSorting
});
// --- Restore saved state (scroll + highlight) ---
function restoreSavedState() {
var saved = sessionStorage.getItem('gcovr-functions-clicked');
if (saved !== null) {
sessionStorage.removeItem('gcovr-functions-clicked');
highlightKey = saved;
}
var scroll = sessionStorage.getItem('gcovr-functions-scrollTop');
if (scroll !== null) {
sessionStorage.removeItem('gcovr-functions-scrollTop');
container.scrollTop = parseInt(scroll, 10);
}
if (saved !== null || scroll !== null) {
lastStartIdx = -1;
renderVisible();
}
}
// --- Initialize ---
data.sort(function(a, b) { return a.name.localeCompare(b.name); });
setupVirtualScroll();
renderVisible();
restoreSavedState();
// Also restore on bfcache navigation (browser Back button)
window.addEventListener('pageshow', function(e) {
if (e.persisted) restoreSavedState();
});
// Mark functions page so initSorting can skip it
container.dataset.virtualScroll = 'true';
}
// ===========================================
// Sorting
// ===========================================
function initSorting() {
var headerSets = [
{
selector: '.file-list-header .sortable, .functions-header .sortable',
getContainer: function() {
return document.getElementById('file-list') || document.querySelector('.functions-body');
},
defaultSort: { key: 'filename', ascending: true }
},
{
selector: '.source-function-header .sortable',
getContainer: function() {
return document.querySelector('.source-functions-list');
},
defaultSort: null
}
];
headerSets.forEach(function(set) {
var headers = document.querySelectorAll(set.selector);
if (!headers.length) return;
headers.forEach(function(header) {
header.addEventListener('click', function() {
var sortKey = this.dataset.sort;
var isAscending = this.classList.contains('sorted-ascending');
headers.forEach(function(h) {
h.classList.remove('sorted-ascending', 'sorted-descending');
});
this.classList.add(isAscending ? 'sorted-descending' : 'sorted-ascending');
sortList(set.getContainer(), sortKey, !isAscending);
});
});
if (set.defaultSort) {
sortList(set.getContainer(), set.defaultSort.key, set.defaultSort.ascending);
}
});
}
function sortList(container, key, ascending) {
if (!container) return;
// Virtual scroll handles its own sorting
if (container.dataset.virtualScroll) return;
var headerEl = container.querySelector('.source-function-header, .file-list-header, .functions-header');
var rows = Array.from(container.children).filter(function(el) { return el !== headerEl; });
rows.sort(function(a, b) {
// Directories always come first
var aIsDir = a.classList.contains('directory');
var bIsDir = b.classList.contains('directory');
if (aIsDir && !bIsDir) return -1;
if (!aIsDir && bIsDir) return 1;
var aVal = a.dataset[key] || a.querySelector('[data-sort]')?.dataset.sort || '';
var bVal = b.dataset[key] || b.querySelector('[data-sort]')?.dataset.sort || '';
// Try to parse as numbers
var aNum = parseFloat(aVal);
var bNum = parseFloat(bVal);
if (!isNaN(aNum) && !isNaN(bNum)) {
return ascending ? aNum - bNum : bNum - aNum;
}
// String comparison
return ascending ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
});
rows.forEach(function(row) {
container.appendChild(row);
});
}
// ===========================================
// Toggle Buttons (Coverage Lines)
// ===========================================
function initToggleButtons() {
const buttons = document.querySelectorAll('.button_toggle_coveredLine, .button_toggle_uncoveredLine, .button_toggle_partialCoveredLine, .button_toggle_excludedLine');
buttons.forEach(function(button) {
var lineClass = button.value;
if (!document.querySelector('.' + lineClass)) {
button.disabled = true;
button.classList.remove('show_' + lineClass);
return;
}
button.addEventListener('click', function() {
const lineClass = this.value;
const showClass = 'show_' + lineClass;
// Toggle the button state
this.classList.toggle(showClass);
// Toggle visibility of lines
const lines = document.querySelectorAll('.' + lineClass);
lines.forEach(function(line) {
line.classList.toggle(showClass);
});
document.dispatchEvent(new CustomEvent('coverage-toggled'));
});
});
// Also handle simpler toggle buttons
const simpleToggles = document.querySelectorAll('.btn-toggle');
simpleToggles.forEach(function(button) {
button.addEventListener('click', function() {
// Use data attribute to get line class (persists after toggle)
const lineClass = this.dataset.lineClass;
if (!lineClass) return;
const showClass = 'show_' + lineClass;
this.classList.toggle(showClass);
const lines = document.querySelectorAll('.' + lineClass);
lines.forEach(function(line) {
line.classList.toggle(showClass);
});
document.dispatchEvent(new CustomEvent('coverage-toggled'));
});
});
}
// ===========================================
// Coverage Navigation (prev/next uncovered)
// ===========================================
function initCoverageNav() {
var prevBtn = document.getElementById('nav-prev');
var nextBtn = document.getElementById('nav-next');
var counter = document.getElementById('nav-counter');
if (!prevBtn || !nextBtn || !counter) return;
var gapLines = [];
var currentIndex = -1;
function collectGapLines() {
var uncovered = document.querySelectorAll('tr.uncoveredLine.show_uncoveredLine');
var partial = document.querySelectorAll('tr.partialCoveredLine.show_partialCoveredLine');
var merged = [];
var i;
for (i = 0; i < uncovered.length; i++) merged.push(uncovered[i]);
for (i = 0; i < partial.length; i++) merged.push(partial[i]);
// Sort by DOM order
merged.sort(function(a, b) {
var pos = a.compareDocumentPosition(b);
if (pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
if (pos & Node.DOCUMENT_POSITION_PRECEDING) return 1;
return 0;
});
gapLines = merged;
currentIndex = -1;
updateCounter();
}
function updateCounter() {
if (gapLines.length === 0) {
counter.textContent = 'All lines covered';
prevBtn.disabled = true;
nextBtn.disabled = true;
} else {
var display = currentIndex >= 0 ? (currentIndex + 1) : 0;
counter.textContent = display + ' / ' + gapLines.length;
prevBtn.disabled = false;
nextBtn.disabled = false;
}
}
function navigateTo(index) {
if (gapLines.length === 0) return;
// Remove previous highlight
var prev = document.querySelector('tr.source-line.nav-highlight');
if (prev) prev.classList.remove('nav-highlight');
currentIndex = index;
var row = gapLines[currentIndex];
row.scrollIntoView({ block: 'center', behavior: 'instant' });
row.classList.add('nav-highlight');
setTimeout(function() {
row.classList.remove('nav-highlight');
}, 1500);
updateCounter();
}
function nextGap() {
if (gapLines.length === 0) return;
var next = currentIndex + 1;
if (next >= gapLines.length) next = 0;
navigateTo(next);
}
function prevGap() {
if (gapLines.length === 0) return;
var prev = currentIndex - 1;
if (prev < 0) prev = gapLines.length - 1;
navigateTo(prev);
}
prevBtn.addEventListener('click', prevGap);
nextBtn.addEventListener('click', nextGap);
document.addEventListener('keydown', function(e) {
var tag = (e.target.tagName || '').toLowerCase();
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
if (e.key === 'n') nextGap();
if (e.key === 'p') prevGap();
});
document.addEventListener('coverage-toggled', function() {
collectGapLines();
});
collectGapLines();
}
// ===========================================
// View Toggle (Nested / Flat)
// ===========================================
function initViewToggle() {
var toggleContainer = document.getElementById('view-toggle');
var fileList = document.getElementById('file-list');
var appContainer = document.querySelector('.app-container');
if (!toggleContainer) return;
// Always show the toggle
toggleContainer.style.display = '';
var buttons = toggleContainer.querySelectorAll('.view-btn');
var savedView = localStorage.getItem('gcovr-view-mode');
function setActiveButton(view) {
buttons.forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.view === view);
});
}
// On non-directory pages (file/source views), still respect flat mode for sidebar
if (!fileList) {
if (appContainer && savedView === 'flat') {
appContainer.classList.add('flat-mode');
setActiveButton('flat');
}
// Allow toggling view mode from source pages
buttons.forEach(function(btn) {
btn.addEventListener('click', function() {
var view = this.dataset.view;
localStorage.setItem('gcovr-view-mode', view);
setActiveButton(view);
if (appContainer) {
if (view === 'flat') {
appContainer.classList.add('flat-mode');
} else {
appContainer.classList.remove('flat-mode');
document.documentElement.classList.remove('early-flat-mode');
}
}
});
});
return;
}
var originalNodes = null; // stash for restoring nested view
function collectFlatFiles(nodes, parentPath) {
var results = [];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var cleanedName = node.name;
// Remove leading ./ or ../
while (cleanedName.indexOf('./') === 0 || cleanedName.indexOf('../') === 0) {
cleanedName = cleanedName.indexOf('./') === 0 ? cleanedName.substring(2) : cleanedName.substring(3);
}
var fullPath = parentPath ? (parentPath + '/' + cleanedName) : cleanedName;
if (node.isDirectory && node.children && node.children.length > 0) {
results = results.concat(collectFlatFiles(node.children, fullPath));
} else if (!node.isDirectory) {
var copy = {};
for (var key in node) {
if (node.hasOwnProperty(key)) copy[key] = node[key];
}
copy.fullPath = fullPath;
results.push(copy);
}
}
return results;
}
function buildFlatRow(file) {
var row = document.createElement('div');
row.className = 'file-row file';
row.setAttribute('data-filename', file.fullPath);
row.setAttribute('data-coverage', file.coverage || '0');
row.setAttribute('data-lines', file.linesTotal || '');
row.setAttribute('data-functions', file.functionsCoverage || '');
row.setAttribute('data-branches', file.branchesCoverage || '');
// Col name
var colName = document.createElement('div');
colName.className = 'col-name';
var icon = document.createElement('span');
icon.className = 'file-icon';
icon.innerHTML = '<svg viewBox="0 0 16 16" width="16" height="16"><path fill="currentColor" d="M3.75 1.5a.25.25 0 00-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 00.25-.25V6h-2.75A1.75 1.75 0 019 4.25V1.5H3.75zm6.75.062V4.25c0 .138.112.25.25.25h2.688a.252.252 0 00-.011-.013l-2.914-2.914a.272.272 0 00-.013-.011zM2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16h-9.5A1.75 1.75 0 012 14.25V1.75z"></path></svg>';
colName.appendChild(icon);
if (file.link) {
var a = document.createElement('a');
a.href = file.link;
a.textContent = file.fullPath;
a.title = file.fullPath;
colName.appendChild(a);
} else {
var span = document.createElement('span');
span.className = 'no-link';
span.textContent = file.fullPath;
span.title = file.fullPath;
colName.appendChild(span);
}
row.appendChild(colName);
// Col coverage
var colCov = document.createElement('div');
colCov.className = 'col-coverage';
var barContainer = document.createElement('div');
barContainer.className = 'coverage-bar-container';
var bar = document.createElement('div');
var linesCov = file.linesCoverage || '';
var linesClass = file.linesClass || file.coverageClass || '';
bar.className = 'coverage-bar ' + linesClass;
bar.style.width = (linesCov && linesCov !== '-') ? linesCov + '%' : '0%';
barContainer.appendChild(bar);
colCov.appendChild(barContainer);
var pct = document.createElement('span');
pct.className = 'coverage-percent ' + linesClass;
pct.textContent = (linesCov && linesCov !== '-') ? linesCov + '%' : '-';
colCov.appendChild(pct);
row.appendChild(colCov);
// Col lines
var colLines = document.createElement('div');
colLines.className = 'col-lines';
var execSpan = document.createElement('span');
execSpan.className = 'stat-value';
execSpan.textContent = file.linesExec || '';
colLines.appendChild(execSpan);
var sep = document.createElement('span');
sep.className = 'stat-separator';
sep.textContent = '/';
colLines.appendChild(sep);
var totalSpan = document.createElement('span');
totalSpan.className = 'stat-total';
totalSpan.textContent = file.linesTotal || '';
colLines.appendChild(totalSpan);
row.appendChild(colLines);
// Col functions (check if container has the column)
var container = fileList.closest('.file-list-container');
var hasFunctions = !container || !container.classList.contains('no-functions');
var hasBranches = !container || !container.classList.contains('no-branches');
var hasConditions = !container || !container.classList.contains('no-conditions');
var hasDecision = !container || !container.classList.contains('no-decisions');
var hasCalls = !container || !container.classList.contains('no-calls');
if (hasFunctions) {
var colFunc = document.createElement('div');
colFunc.className = 'col-functions';
var funcVal = document.createElement('span');
var funcCov = file.functionsCoverage || '';
var funcClass = file.functionsClass || '';
funcVal.className = 'stat-value ' + funcClass;
funcVal.textContent = (funcCov && funcCov !== '-') ? funcCov + '%' : '-';
colFunc.appendChild(funcVal);
row.appendChild(colFunc);
}
if (hasBranches) {
var colBr = document.createElement('div');
colBr.className = 'col-branches';
var brVal = document.createElement('span');
var brCov = file.branchesCoverage || '';
var brClass = file.branchesClass || '';
brVal.className = 'stat-value ' + brClass;
brVal.textContent = (brCov && brCov !== '-') ? brCov + '%' : '-';
colBr.appendChild(brVal);
row.appendChild(colBr);
}
if (hasConditions) {
var colCond = document.createElement('div');
colCond.className = 'col-conditions';
var condVal = document.createElement('span');
var condCov = file.conditionsCoverage || '';
var condClass = file.conditionsClass || '';
condVal.className = 'stat-value ' + condClass;
condVal.textContent = (condCov && condCov !== '-') ? condCov + '%' : '-';
colCond.appendChild(condVal);
row.appendChild(colCond);
}
if (hasDecision) {
var colDec = document.createElement('div');
colDec.className = 'col-decision';
var decVal = document.createElement('span');
var decCov = file.decisionCoverage || '';
var decClass = file.decisionClass || '';
decVal.className = 'stat-value ' + decClass;
decVal.textContent = (decCov && decCov !== '-') ? decCov + '%' : '-';
colDec.appendChild(decVal);
row.appendChild(colDec);
}
if (hasCalls) {
var colCalls = document.createElement('div');
colCalls.className = 'col-calls';
var callsVal = document.createElement('span');
var callsCov = file.callsCoverage || '';
var callsClass = file.callsClass || '';
callsVal.className = 'stat-value ' + callsClass;
callsVal.textContent = (callsCov && callsCov !== '-') ? callsCov + '%' : '-';
colCalls.appendChild(callsVal);
row.appendChild(colCalls);
}
return row;
}
function switchToFlat() {
if (!window.GCOVR_TREE_DATA) return;
// Stash original DOM nodes
if (originalNodes === null) {
originalNodes = document.createDocumentFragment();
while (fileList.firstChild) {
originalNodes.appendChild(fileList.firstChild);
}
}
var flatFiles = collectFlatFiles(window.GCOVR_TREE_DATA, '');
// Sort by coverage ascending (matching default)
flatFiles.sort(function(a, b) {
var aVal = parseFloat(a.coverage) || 0;
var bVal = parseFloat(b.coverage) || 0;
return aVal - bVal;
});
while (fileList.firstChild) {
fileList.removeChild(fileList.firstChild);
}
for (var i = 0; i < flatFiles.length; i++) {
fileList.appendChild(buildFlatRow(flatFiles[i]));
}
if (appContainer) appContainer.classList.add('flat-mode');
setActiveButton('flat');
localStorage.setItem('gcovr-view-mode', 'flat');
}
function switchToNested() {
if (originalNodes !== null) {
while (fileList.firstChild) {
fileList.removeChild(fileList.firstChild);
}
fileList.appendChild(originalNodes);
originalNodes = null;
}
if (appContainer) appContainer.classList.remove('flat-mode');
document.documentElement.classList.remove('early-flat-mode');
setActiveButton('nested');
localStorage.setItem('gcovr-view-mode', 'nested');
// Re-run sorting to maintain state
sortList(document.getElementById('file-list') || document.querySelector('.functions-body'), 'filename', true);
}
buttons.forEach(function(btn) {
btn.addEventListener('click', function() {
var view = this.dataset.view;
if (view === 'flat') {
switchToFlat();
} else {
switchToNested();
}
});
});
// Apply saved preference on load
if (savedView === 'flat') {
// Defer to ensure tree data is loaded
setTimeout(function() {
switchToFlat();
}, 0);
}
}
// ===========================================
// Settings Dropdown (mobile gear icon)
// ===========================================
function initSettingsDropdown() {
var btn = document.getElementById('settings-btn');
var dropdown = document.getElementById('settings-dropdown');
var header = document.querySelector('.main-header');
if (!btn || !dropdown || !header) return;
var viewToggle = document.getElementById('view-toggle');
var themeToggle = document.getElementById('theme-toggle');
var isMobile = false;
// Reference node: settings-btn, so we can insert before it when moving back
function moveToDropdown() {
if (viewToggle && viewToggle.parentNode !== dropdown) {
dropdown.appendChild(viewToggle);
}
if (themeToggle && themeToggle.parentNode !== dropdown) {
dropdown.appendChild(themeToggle);
}
}
function moveToHeader() {
// Insert before settings-btn so they appear in original order
if (viewToggle && viewToggle.parentNode !== header) {
header.insertBefore(viewToggle, btn);
}
if (themeToggle && themeToggle.parentNode !== header) {
header.insertBefore(themeToggle, btn);
}
}
function checkBreakpoint() {
var nowMobile = window.innerWidth <= 1024;
if (nowMobile === isMobile) return;
isMobile = nowMobile;
if (isMobile) {
moveToDropdown();
} else {
dropdown.classList.remove('open');
moveToHeader();
}
}
// Toggle dropdown on button click
btn.addEventListener('click', function(e) {
e.stopPropagation();
dropdown.classList.toggle('open');
});
// Close on outside click
document.addEventListener('click', function(e) {
if (!dropdown.contains(e.target) && e.target !== btn) {
dropdown.classList.remove('open');
}
});
// Close on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
dropdown.classList.remove('open');
}
});
// Respond to resize
window.addEventListener('resize', checkBreakpoint);
// Initial check
checkBreakpoint();
}
// ===========================================
// Popup Resize (only when overflowing)
// ===========================================
function initPopupResize() {
var details = document.querySelectorAll('.branch-details, .condition-details, .decision-details, .call-details');
if (details.length === 0) return;
details.forEach(function(det) {
det.addEventListener('toggle', function() {
if (!det.open) return;
var popup = det.querySelector('.branch-popup, .condition-popup, .decision-popup, .call-popup');
if (!popup) return;
// Check after render if content overflows
requestAnimationFrame(function() {
if (popup.scrollHeight > popup.clientHeight) {
popup.classList.add('is-overflowing');
} else {
popup.classList.remove('is-overflowing');
}
});
});
});
}
// ===========================================
// Nav Override (prev/next follows tree order)
// ===========================================
function initNavOverride() {
if (!window.GCOVR_TREE_DATA) return;
var navPrev = document.querySelectorAll('.nav-prev');
var navNext = document.querySelectorAll('.nav-next');
if (navPrev.length === 0 && navNext.length === 0) return;
// DFS-flatten tree to collect file links in sidebar order
function collectLinks(nodes) {
var links = [];
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.isDirectory && node.children && node.children.length > 0) {
links = links.concat(collectLinks(node.children));
} else if (!node.isDirectory && node.link) {
links.push(node.link);
}
}
return links;
}
var fileLinks = collectLinks(window.GCOVR_TREE_DATA);
if (fileLinks.length === 0) return;
var currentPage = window.location.pathname.split('/').pop() || 'index.html';
var idx = fileLinks.indexOf(currentPage);
if (idx === -1) return;
var prev = idx > 0 ? fileLinks[idx - 1] : null;
var next = idx < fileLinks.length - 1 ? fileLinks[idx + 1] : null;
function updateNavLinks(els, href) {
for (var i = 0; i < els.length; i++) {
var el = els[i];
if (href) {
// Enable: ensure it's an <a> with the correct href
if (el.tagName === 'A') {
el.setAttribute('href', href);
} else {
// Replace disabled <span> with an <a>
var a = document.createElement('a');
a.className = el.className.replace(/\bdisabled\b/, '').trim();
a.href = href;
a.title = el.title;
while (el.firstChild) a.appendChild(el.firstChild);
el.parentNode.replaceChild(a, el);
}
} else {
// Disable: ensure it's a <span> with disabled class
if (el.tagName === 'A') {
var span = document.createElement('span');
span.className = el.className + ' disabled';
span.title = el.title;
while (el.firstChild) span.appendChild(el.firstChild);
el.parentNode.replaceChild(span, el);
} else {
el.classList.add('disabled');
}
}
}
}
updateNavLinks(navPrev, prev);
updateNavLinks(navNext, next);
}
// ===========================================
// TLA Navigation (HIT/MIS/PAR links)
// ===========================================
function initTlaNavigation() {
var rows = document.querySelectorAll('.source-line');
if (rows.length === 0) return;
// Classify each row by coverage type
var COV_CLASSES = ['coveredLine', 'uncoveredLine', 'partialCoveredLine'];
var LABELS = { coveredLine: 'HIT', uncoveredLine: 'MIS', partialCoveredLine: 'PAR' };
var CSS_CLASSES = { coveredLine: 'tla-hit', uncoveredLine: 'tla-mis', partialCoveredLine: 'tla-par' };
// Build list of groups: contiguous runs of the same coverage class
var groups = []; // { type, firstRow }
var prevType = null;
for (var i = 0; i < rows.length; i++) {
var row = rows[i];
var type = null;
for (var j = 0; j < COV_CLASSES.length; j++) {
if (row.classList.contains(COV_CLASSES[j])) {
type = COV_CLASSES[j];
break;
}
}
if (type === null) {
prevType = null;
continue;
}
if (type !== prevType) {
groups.push({ type: type, firstRow: row });
prevType = type;
}
}
if (groups.length === 0) return;
// Determine the anchor prefix used in this page
var sampleAnchor = rows[0].querySelector('.col-lineno a');
var anchorPrefix = '';
if (sampleAnchor) {
var id = sampleAnchor.id;
var idx = id.indexOf('l');
if (idx > 0) {
anchorPrefix = id.substring(0, idx);
}
}
// For each group, find the line number from its first row
function getLineNo(row) {
var a = row.querySelector('.col-lineno a');
return a ? a.textContent.trim() : '';
}
// Build per-type lists for wrap-around linking
var byType = {};
for (var i = 0; i < groups.length; i++) {
var g = groups[i];
if (!byType[g.type]) byType[g.type] = [];
byType[g.type].push(i);
}
// For each group, compute next group index of same type
var nextGroupIdx = new Array(groups.length);
for (var type in byType) {
var indices = byType[type];
for (var k = 0; k < indices.length; k++) {
var nextK = (k + 1) % indices.length;
nextGroupIdx[indices[k]] = indices[nextK];
}
}
// Inject TLA links
for (var i = 0; i < groups.length; i++) {
var g = groups[i];
var cell = g.firstRow.querySelector('.col-tla');
if (!cell) continue;
var targetGroup = groups[nextGroupIdx[i]];
var targetLineNo = getLineNo(targetGroup.firstRow);
var targetId = anchorPrefix + 'l' + targetLineNo;
var a = document.createElement('a');
a.className = 'tla-link ' + CSS_CLASSES[g.type];
a.textContent = LABELS[g.type];
a.href = '#' + targetId;
a.addEventListener('click', function(e) {
var target = document.getElementById(this.getAttribute('href').substring(1));
if (target) {
e.preventDefault();
// Scroll within the source-table-container
var scrollBox = document.querySelector('.source-table-container');
var row = target.closest('tr');
if (scrollBox && row) {
var thead = scrollBox.querySelector('thead');
var theadHeight = thead ? thead.offsetHeight : 0;
scrollBox.scrollTo({ top: row.offsetTop - theadHeight - 8, behavior: 'instant' });
}
history.replaceState(null, '', this.getAttribute('href'));
// Highlight the target row (clear any previous highlight first)
var prev = document.querySelector('.highlight-target');
if (prev) prev.classList.remove('highlight-target');
if (row) row.classList.add('highlight-target');
}
});
cell.appendChild(a);
}
}
// ===========================================
// Line number click highlight
// ===========================================
function initLineHighlight() {
var clickedFnItem = null;
function highlightFromHash(scroll) {
var prev = document.querySelector('.highlight-target');
if (prev) prev.classList.remove('highlight-target');
var prevFn = document.querySelector('.source-function-item.selected');
if (prevFn) prevFn.classList.remove('selected');
var id = window.location.hash.slice(1);
if (!id) return;
var el = document.getElementById(id);
if (!el) return;
var fnItem = clickedFnItem || document.querySelector('.source-function-item[href="#' + id + '"]');
clickedFnItem = null;
if (fnItem) fnItem.classList.add('selected');
var row = el.closest('tr');
if (row) {
row.classList.add('highlight-target');
if (scroll) {
var scrollBox = document.querySelector('.source-table-container');
if (scrollBox) {
var thead = scrollBox.querySelector('thead');
var theadHeight = thead ? thead.offsetHeight : 0;
scrollBox.scrollTo({ top: row.offsetTop - theadHeight - 8, behavior: 'instant' });
} else {
row.scrollIntoView({ block: 'center' });
}
}
}
}
// Handle clicks on function list items directly
var fnList = document.querySelector('.source-functions-list');
if (fnList) {
fnList.addEventListener('click', function(e) {
var item = e.target.closest('.source-function-item');
if (!item) return;
e.preventDefault();
clickedFnItem = item;
var href = item.getAttribute('href');
if (href) history.replaceState(null, '', href);
highlightFromHash(true);
});
}
// Event delegation: single listener on the table container
var container = document.querySelector('.source-table-container');
if (container) {
container.addEventListener('click', function(e) {
var anchor = e.target.closest('.col-lineno a');
if (!anchor) return;
e.preventDefault();
if (anchor.id) history.replaceState(null, '', '#' + anchor.id);
highlightFromHash(false);
});
}
// Highlight + scroll on initial load and back/forward navigation
highlightFromHash(true);
window.addEventListener('hashchange', function() { highlightFromHash(true); });
}
// ===========================================
// Column Visibility Toggles
// ===========================================
function initColumnToggles() {
var buttons = document.querySelectorAll('.col-toggle');
if (buttons.length === 0) return;
var table = document.querySelector('.source-table');
if (!table) return;
// Restore saved state
var hidden = [];
try {
var saved = localStorage.getItem('gcovr-hidden-columns');
if (saved) {
hidden = JSON.parse(saved);
} else {
hidden = ['tla'];
}
} catch (e) {}
// Apply saved hidden columns
var fnList = document.querySelector('.source-functions-list');
for (var i = 0; i < hidden.length; i++) {
table.classList.add('hide-col-' + hidden[i]);
if (fnList) {
fnList.classList.add('hide-col-' + hidden[i]);
}
}
// Update button appearance to match state
buttons.forEach(function(btn) {
var col = btn.getAttribute('data-col');
if (hidden.indexOf(col) >= 0) {
btn.classList.remove('show-col');
}
});
// Handle clicks
buttons.forEach(function(btn) {
btn.addEventListener('click', function() {
var col = this.getAttribute('data-col');
var hideClass = 'hide-col-' + col;
var isHidden = table.classList.toggle(hideClass);
this.classList.toggle('show-col', !isHidden);
// Sync with function list sidebar
var fnList = document.querySelector('.source-functions-list');
if (fnList) {
fnList.classList.toggle(hideClass, isHidden);
}
// Save state
var current = [];
var allBtns = document.querySelectorAll('.col-toggle');
allBtns.forEach(function(b) {
if (!b.classList.contains('show-col')) {
current.push(b.getAttribute('data-col'));
}
});
localStorage.setItem('gcovr-hidden-columns', JSON.stringify(current));
});
});
}
// ===========================================
// File nav keyboard shortcuts ([ and ])
// ===========================================
function initFileNavKeys() {
var prevLink = document.querySelector('.source-nav-links .nav-prev') || document.querySelector('.nav-links .nav-prev');
var nextLink = document.querySelector('.source-nav-links .nav-next') || document.querySelector('.nav-links .nav-next');
if (!prevLink && !nextLink) return;
document.addEventListener('keydown', function(e) {
var tag = (e.target.tagName || '').toLowerCase();
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
// Re-query to pick up any DOM replacements by initNavOverride
var prev = document.querySelector('.source-nav-links a.nav-prev') || document.querySelector('.nav-links a.nav-prev');
var next = document.querySelector('.source-nav-links a.nav-next') || document.querySelector('.nav-links a.nav-next');
if (e.key === '[' && prev) {
window.location.href = prev.href;
}
if (e.key === ']' && next) {
window.location.href = next.href;
}
});
}
// ===========================================
// Enrich file nav tooltips with actual filenames
// ===========================================
function initFileNavTooltips() {
if (!window.GCOVR_TREE_DATA) return;
var links = document.querySelectorAll('.source-nav-links .nav-prev, .source-nav-links .nav-next, .nav-links .nav-prev, .nav-links .nav-next');
for (var i = 0; i < links.length; i++) {
var anchor = links[i];
var href = anchor.getAttribute('href');
if (!href || href === '#') continue;
var filename = href.replace(/^.*\//, '').replace(/#.*$/, '');
var node = findNodeInTree(window.GCOVR_TREE_DATA, filename);
if (node && node.name) {
var direction = anchor.classList.contains('nav-prev') ? 'Previous' : 'Next';
anchor.title = direction + ': ' + node.name;
}
}
}
function findNodeInTree(nodes, targetLink) {
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.link === targetLink) return node;
if (node.children) {
var found = findNodeInTree(node.children, targetLink);
if (found) return found;
}
}
return null;
}
// ===========================================
// Prefetch pages on hover for instant nav
// ===========================================
function initPrefetch() {
// Skip for file:// protocol (fetch won't work)
if (location.protocol === 'file:') return;
var prefetched = {};
document.addEventListener('mouseover', function(e) {
var link = e.target.closest('a[href]');
if (!link) return;
var href = link.getAttribute('href');
// Only prefetch local HTML pages
if (!href || href.charAt(0) === '#' || href.indexOf('://') !== -1) return;
if (prefetched[href]) return;
prefetched[href] = true;
var prefetchLink = document.createElement('link');
prefetchLink.rel = 'prefetch';
prefetchLink.href = href;
document.head.appendChild(prefetchLink);
});
}
function initFunctionListPersistence() {
var details = document.querySelector('details.source-functions');
if (!details) return;
var key = 'gcovr-fn-list-open';
if (sessionStorage.getItem(key) === 'true') {
details.setAttribute('open', '');
}
details.addEventListener('toggle', function() {
sessionStorage.setItem(key, details.open ? 'true' : 'false');
});
}
})();
window.GCOVR_TREE_DATA = [{"name": "include", "coverage": "89.9", "coverageClass": "coverage-medium", "linesTotal": "7108", "linesExec": "6391", "linesCoverage": "89.9", "linesClass": "coverage-medium", "functionsCoverage": "59.2", "functionsClass": "coverage-low", "branchesCoverage": "-", "branchesClass": "coverage-unknown", "isDirectory": true, "link": "index.include.d436eb0fd9de10b54a828ce6435f7e81.html", "children": [{"name": "boost", "coverage": "", "coverageClass": "coverage-unknown", "linesTotal": "", "linesExec": "", "linesCoverage": "", "linesClass": "", "functionsCoverage": "", "functionsClass": "", "branchesCoverage": "", "branchesClass": "", "isDirectory": true, "link": null, "children": [{"name": "regex", "coverage": "", "coverageClass": "coverage-unknown", "linesTotal": "", "linesExec": "", "linesCoverage": "", "linesClass": "", "functionsCoverage": "", "functionsClass": "", "branchesCoverage": "", "branchesClass": "", "isDirectory": true, "link": null, "children": [{"name": "v5", "coverage": "", "coverageClass": "coverage-unknown", "linesTotal": "", "linesExec": "", "linesCoverage": "", "linesClass": "", "functionsCoverage": "", "functionsClass": "", "branchesCoverage": "", "branchesClass": "", "isDirectory": true, "link": null, "children": [{"name": "basic_regex.hpp", "coverage": "97.6", "coverageClass": "coverage-high", "linesTotal": "205", "linesExec": "200", "linesCoverage": "97.6", "linesClass": "coverage-high", "functionsCoverage": "96.9", "functionsClass": "coverage-high", "branchesCoverage": "-", "branchesClass": "coverage-unknown", "isDirectory": false, "link": "index.basic_regex.hpp.fc7ae442ee1c3ba72f0c8efe27e0d4a0.html", "children": []}, {"name": "basic_regex_creator.hpp", "coverage": "98.4", "coverageClass": "coverage-high", "linesTotal": "806", "linesExec": "793", "linesCoverage": "98.4", "linesClass": "coverage-high", "functionsCoverage": "89.2", "functionsClass": "coverage-medium", "branchesCoverage": "-", "branchesClass": "coverage-unknown", "isDirectory": false, "link": "index.basic_regex_creator.hpp.ddc38b9ace9f1fce2474b7a8aa072758.html", "children": []}, {"name": "basic_regex_parser.hpp", "coverage": "88.1", "coverageClass": "coverage-medium", "linesTotal": "1796", "linesExec": "1582", "linesCoverage": "88.1", "linesClass": "coverage-medium", "functionsCoverage": "83.7", "functionsClass": "coverage-medium", "branchesCoverage": "-", "branchesClass": "coverage-unknown", "isDirectory": false, "link": "index.basic_regex_parser.hpp.3d312756fa15eb91f18ce74346527498.html", "children": []}, {"name": "c_regex_traits.hpp", "coverage": "80.2", "coverageClass": "coverage-medium", "linesTotal": "167", "linesExec": "134", "linesCoverage": "80.2", "linesClass": "coverage-medium", "functionsCoverage": "100.0", "functionsClass": "coverage-high", "branchesCoverage": "-", "branchesClass": "coverage-unknown", "isDirectory": false, "link": "index.c_regex_traits.hpp.4a312148ed0fde78796e7f563ab76c8c.html", "children": []}, {"name": "cpp_regex_traits.hpp", "coverage": "61.7", "coverageClass": "coverage-low", "linesTotal": "342", "linesExec": "211", "linesCoverage": "61.7", "linesClass": "coverage-low", "functionsCoverage": "88.9", "functionsClass": "coverage-medium", "branchesCoverage": "-", "branchesClass": "coverage-unknown", "isDirectory": false, "link": "index.cpp_regex_traits.hpp.065ad3525ef9ac006d863057e1e02b79.html", "children": []}, {"name": "icu.hpp", "coverage": "83.6", "coverageClass": "coverage-medium", "linesTotal": "371", "linesExec": "310", "linesCoverage": "83.6", "linesClass": "coverage-medium", "functionsCoverage": "50.5", "functionsClass": "coverage-low", "branchesCoverage": "-", "branchesClass": "coverage-unknown", "isDirectory": false, "link": "index.icu.hpp.85a0eeb0fabef296bfc4997ff2ba1c10.html", "children": []}, {"name": "match_flags.hpp", "coverage": "100.0", "coverageClass": "coverage-high", "linesTotal": "10", "linesExec": "10", "linesCoverage": "100.0", "linesClass": "coverage-high", "functionsCoverage": "100.0", "functionsClass": "coverage-high", "branchesCoverage": "-", "branchesCl