mirror of
https://github.com/boostorg/regex.git
synced 2026-07-05 07:51:00 +02:00
2124 lines
89 KiB
JavaScript
2124 lines
89 KiB
JavaScript
|
|
/* 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
|