/** * Pure helpers for the navigation page facets / filters (#158). * * No DOM, no imports — usable straight from Node tests. * Mirrors the search page's facet mechanism (Vaults / Tags / Extensions) * over a plain directory listing instead of a search result set. */ /** Strip the leading dot and lowercase a file extension. */ function normExt(extension) { return String(extension || "").toLowerCase().replace(/^\./, ""); } /** Display name used for labels and alphabetical sorting (.md removed). */ export function navDisplayName(file) { const name = file.name || String(file.path || "").split("/").pop() || ""; return name.endsWith(".md") ? name.slice(0, -3) : name; } /** * Count vaults / tags / extensions over a file listing. * @param {Array} files * @returns {{vaults: object, tags: object, extensions: object}} counts sorted desc */ export function buildNavFacets(files) { const facets = { vaults: {}, tags: {}, extensions: {} }; for (const f of files || []) { if (f.vault) facets.vaults[f.vault] = (facets.vaults[f.vault] || 0) + 1; const ext = normExt(f.extension); if (ext) facets.extensions[ext] = (facets.extensions[ext] || 0) + 1; for (const tag of f.tags || []) { facets.tags[tag] = (facets.tags[tag] || 0) + 1; } } for (const key of Object.keys(facets)) { facets[key] = Object.fromEntries( Object.entries(facets[key]).sort((a, b) => b[1] - a[1]), ); } return facets; } /** * Keep only the files matching the active tag / extension filters. * @param {Array} files * @param {{tag?: string, ext?: string}} filters */ export function filterNavFiles(files, filters) { const tag = (filters && filters.tag) || ""; const ext = normExt((filters && filters.ext) || ""); if (!tag && !ext) return files || []; return (files || []).filter((f) => { if (ext && normExt(f.extension) !== ext) return false; if (tag && !(f.tags || []).includes(tag)) return false; return true; }); } /** * Sort the listing: `modified` (newest first, server default) or * `relevance` (A→Z). * * ponytail: a directory listing carries no relevance signal, `relevance` is an * alphabetical stand-in — upgrade path: sort by scoped search score once the * navigation page owns a query. */ export function sortNavFiles(files, mode) { const out = (files || []).slice(); if (mode === "relevance") { out.sort((a, b) => navDisplayName(a).localeCompare(navDisplayName(b), undefined, { sensitivity: "base" }), ); } else { out.sort((a, b) => (b.modified || 0) - (a.modified || 0)); } return out; }