/* * Per-tab wiki page history. * * BreadcrumbTrail owns the ordered page slugs and their session-storage * representation. It deliberately knows nothing about page titles, routes or * DOM elements; the application supplies the current set of valid page slugs. */ const storageKey = "racket-wiki-breadcrumb-trail"; const trailLimit = 8; class BreadcrumbTrail { /** * goal : Create an empty breadcrumb history backed by session storage. * pre : storage implements the Web Storage getItem/setItem interface. * post : No stored value is read until load is called with valid pages. * result : A directly usable breadcrumb history. */ constructor(storage) { this.storage = storage; this.pageSlugs = []; } /** Return a copy of the ordered non-home page slugs. */ get slugs() { return this.pageSlugs.slice(); } /** * goal : Restore the trail and discard references to missing pages. * pre : validSlugs contains the current readable page slugs. * post : The normalized trail is stored again for this browser tab. * result : A copy of the restored trail. * internals: JSON is decoded first, then filtered through validSlugs so stale * browser state cannot become application navigation. */ load(validSlugs) { let stored = []; try { stored = JSON.parse(this.storage.getItem(storageKey) || "[]"); } catch (_error) { stored = []; } const valid = new Set(validSlugs); if (Array.isArray(stored)) { this.pageSlugs = stored.filter((slug) => typeof slug === "string" && valid.has(slug)); } else { this.pageSlugs = []; } this.#save(); return this.slugs; } /** Empty and persist the breadcrumb history. */ clear() { this.pageSlugs = []; this.#save(); } /** * goal : Record navigation to one wiki page. * pre : pageSlug identifies the visited page; homeSlug may be null. * post : Home clears the trail, revisits truncate it, and new visits append. * result : A copy of the updated trail. * internals: The trail contains only non-home pages and retains at most the * eight most recent entries, matching the visible breadcrumb. */ record(pageSlug, homeSlug = null) { if (!pageSlug) return this.slugs; if (homeSlug && pageSlug === homeSlug) { this.clear(); return this.slugs; } const existingIndex = this.pageSlugs.indexOf(pageSlug); if (existingIndex >= 0) { this.pageSlugs = this.pageSlugs.slice(0, existingIndex + 1); } else { this.pageSlugs.push(pageSlug); if (this.pageSlugs.length > trailLimit) { this.pageSlugs = this.pageSlugs.slice(-trailLimit); } } this.#save(); return this.slugs; } /** * goal : Discard breadcrumb entries that follow a selected trail page. * pre : pageSlug is a breadcrumb destination; homeSlug may be null. * post : Home clears the trail; an existing page becomes its final entry. * result : A copy of the updated trail. */ truncate(pageSlug, homeSlug = null) { if (homeSlug && pageSlug === homeSlug) { this.clear(); return this.slugs; } const index = this.pageSlugs.indexOf(pageSlug); if (index >= 0) { this.pageSlugs = this.pageSlugs.slice(0, index + 1); this.#save(); } return this.slugs; } /** Persist the current representation for this browser tab. */ #save() { this.storage.setItem(storageKey, JSON.stringify(this.pageSlugs)); } } export { BreadcrumbTrail };