/** * Build the filetree * @param {HTMLElement} parent Root element of the filetree * @param {{name: string, is_dir: boolean, children: any[]}} data FileNode * @param {string} location Current location, used for links creation */ const buildFileTree = (parent, data, location) => { const ul = document.createElement("ul"); data.forEach((item) => { const li = document.createElement("li"); li.classList.add(item.is_dir ? "directory" : "file"); if (item.is_dir) { // Directory li.textContent = item.name; li.classList.add("collapsed"); // Toggle collapsing on click li.addEventListener("click", function (e) { if (e.target === li) { li.classList.toggle("collapsed"); } }); } else { // File const url = window.location.href.split("?")[0]; const a = document.createElement("a"); a.text = item.name; a.href = `${url}?q=${location}${item.name}`; li.appendChild(a); } ul.appendChild(li); if (item.children && item.children.length > 0) { buildFileTree( li, item.children, item.is_dir ? location + `${item.name}/` : location ); } }); parent.appendChild(ul); }; /** * Uncollapse elements from the deepest element * @param {HTMLLIElement} element Element to uncollapse */ const uncollapse = (element) => { if (element) { element.classList.remove("collapsed"); uncollapse(element.parentElement.closest("li")); } }; /** * Find the deepest opened directory * @param {string[]} path Current path we are looking at, init with fullpath * @param {NodeListOf} options Options we have, init with list root * @returns */ const deepestNodeOpened = (path, options) => { // Iterate over possible options for (let i = 0; i < options.length; ++i) { // If the directory and the current path match if (decodeURI(path[0]) === options[i].firstChild.nodeValue) { if (path.length === 1) { // We found it return options[i]; } // Continue the search return deepestNodeOpened( path.slice(1), options[i].querySelector("ul").childNodes ); } } }; const svgDarkTheme = () => { for (const item of document.getElementsByTagName("img")) { if (!item.src.startsWith("data:image/svg+xml;base64,")) { // Exclude image who aren't SVG and base64 encoded break; } /** Convert to grayscale */ const colorToGrayscale = (color) => { return 0.3 * color.r + 0.59 * color.g + 0.11 * color.b; }; /** Extract color using canvas2d */ const extractColors = (image) => { const canvas = document.createElement("canvas"); canvas.width = image.width; canvas.height = image.height; const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0); const imageData = ctx.getImageData( 0, 0, Math.max(1, canvas.width), Math.max(1, canvas.height) ); const pixelData = imageData.data; const colors = []; for (let i = 0; i < pixelData.length; i += 4) { if (pixelData[i + 3] > 0) { colors.push({ r: pixelData[i], g: pixelData[i + 1], b: pixelData[i + 2], }); } } return colors; }; // Extract colors const colors = extractColors(item); // Calculate the average grayscale value const grayscaleValues = colors.map(colorToGrayscale); const totalGrayscale = grayscaleValues.reduce((acc, val) => acc + val, 0); const averageGrayscale = totalGrayscale / grayscaleValues.length; if (averageGrayscale < 128) { item.style = "filter: invert(1);"; } } }; window.addEventListener("load", () => { // Build the filetree const fileTreeElement = document.getElementsByTagName("aside")[0]; const dataElement = fileTreeElement.getElementsByTagName("span")[0]; buildFileTree( fileTreeElement, JSON.parse(dataElement.getAttribute("data-json")).children, "" ); dataElement.remove(); // Open nested openeded directories const infoURL = window.location.href.split("?"); if (infoURL.length > 1) { const fullpath = infoURL[1].substring(2); const path = fullpath.substring(0, fullpath.lastIndexOf("/")); const last_openeded = deepestNodeOpened( path.split("/"), fileTreeElement.querySelector("ul").childNodes ); uncollapse(last_openeded); } // Fix SVG images in dark mode if (window.matchMedia("(prefers-color-scheme: dark)").matches) { svgDarkTheme(); } });