Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: some progress but honestly it's funky

+130 -48
+18 -19
crates/maudit/js/prefetch.ts
··· 1 - const preloadedResources = new Set<string>(); 1 + const preloadedUrls = new Set<string>(); 2 2 3 3 interface PreloadConfig { 4 4 skipConnectionCheck?: boolean; 5 5 } 6 6 7 7 export function prefetch(url: string, config?: PreloadConfig) { 8 - url = url.replace(/#.*/, ""); 8 + let urlObj: URL; 9 + try { 10 + urlObj = new URL(url, window.location.href); 11 + urlObj.hash = ""; 12 + } catch { 13 + throw new Error(`Invalid URL provided to prefetch: ${url}`); 14 + } 9 15 10 - const bypassConnectionCheck = config?.skipConnectionCheck ?? false; 16 + const skipConnectionCheck = config?.skipConnectionCheck ?? false; 11 17 12 - if (!canPrefetchUrl(url, bypassConnectionCheck)) { 18 + if (!canPrefetchUrl(urlObj, skipConnectionCheck)) { 13 19 return; 14 20 } 15 21 ··· 20 26 linkElement.rel = "prefetch"; 21 27 linkElement.href = url; 22 28 document.head.appendChild(linkElement); 23 - preloadedResources.add(url); 29 + preloadedUrls.add(urlObj.href); 24 30 } 25 31 } 26 32 27 - function canPrefetchUrl(url: string, bypassConnectionCheck: boolean): boolean { 33 + function canPrefetchUrl(url: URL, skipConnectionCheck: boolean): boolean { 28 34 if (!navigator.onLine) return false; 29 - if (!bypassConnectionCheck && hasLimitedBandwidth()) return false; 35 + if (!skipConnectionCheck && hasLimitedBandwidth()) return false; 30 36 31 - try { 32 - const destination = new URL(url, window.location.href); 33 - 34 - return ( 35 - (window.location.origin === destination.origin && 36 - window.location.pathname !== destination.pathname) || 37 - (window.location.search !== destination.search && !preloadedResources.has(url)) 38 - ); 39 - } catch { 40 - return false; 41 - } 37 + return ( 38 + (window.location.origin === url.origin && window.location.pathname !== url.pathname) || 39 + (window.location.search !== url.search && !preloadedUrls.has(url.href)) 40 + ); 42 41 } 43 42 44 43 function hasLimitedBandwidth(): boolean { ··· 46 45 // https://caniuse.com/?search=navigator.connection 47 46 if ("connection" in navigator) { 48 47 const networkInfo = (navigator as any).connection; 49 - return networkInfo.saveData || /2g/.test(networkInfo.effectiveType); 48 + return networkInfo.saveData || networkInfo.effectiveType.endsWith("2g"); 50 49 } 51 50 52 51 return false;
+73
crates/maudit/js/prefetch/hover.ts
··· 1 + import { prefetch } from "../prefetch.ts"; 2 + 3 + const listenedAnchors = new WeakSet<HTMLAnchorElement>(); 4 + 5 + function init() { 6 + let timeout: ReturnType<typeof setTimeout> | null = null; 7 + 8 + // Handle focus listeners for keyboard navigation accessibility 9 + document.body.addEventListener( 10 + "focusin", 11 + (e) => { 12 + if (e.target instanceof HTMLAnchorElement) { 13 + handleHoverIn(e); 14 + } 15 + }, 16 + { passive: true }, 17 + ); 18 + document.body.addEventListener("focusout", handleHoverOut, { passive: true }); 19 + 20 + // Attach hover listeners to all anchors 21 + const attachListeners = () => { 22 + const anchors = document.getElementsByTagName("a"); 23 + for (const anchor of anchors) { 24 + if (listenedAnchors.has(anchor)) continue; 25 + 26 + listenedAnchors.add(anchor); 27 + anchor.addEventListener("mouseenter", handleHoverIn, { passive: true }); 28 + anchor.addEventListener("mouseleave", handleHoverOut, { passive: true }); 29 + } 30 + }; 31 + 32 + document.addEventListener("DOMContentLoaded", attachListeners); 33 + 34 + // Re-attach listeners for dynamically added content 35 + const observer = new MutationObserver((mutations) => { 36 + for (const mutation of mutations) { 37 + if (mutation.type === "childList" && mutation.addedNodes.length > 0) { 38 + attachListeners(); 39 + break; 40 + } 41 + } 42 + }); 43 + 44 + observer.observe(document.body, { 45 + childList: true, 46 + subtree: true, 47 + }); 48 + 49 + function handleHoverIn(e: Event) { 50 + const target = e.target as HTMLAnchorElement; 51 + 52 + if (!target.href) { 53 + return; 54 + } 55 + 56 + if (timeout !== null) { 57 + clearTimeout(timeout); 58 + } 59 + timeout = setTimeout(() => { 60 + prefetch(target.href); 61 + timeout = null; 62 + }, 80); 63 + } 64 + 65 + function handleHoverOut() { 66 + if (timeout !== null) { 67 + clearTimeout(timeout); 68 + timeout = null; 69 + } 70 + } 71 + } 72 + 73 + init();
+24 -24
crates/maudit/src/assets/prefetch.rs
··· 1 - use rolldown::{ 2 - ModuleType, 3 - plugin::{HookUsage, Plugin}, 4 - }; 1 + use std::path::PathBuf; 5 2 6 - const PREFETCH_CODE: &str = include_str!("../../js/prefetch.ts"); 3 + use rolldown::plugin::{HookUsage, Plugin}; 7 4 8 - /// Rolldown plugin to expose the prefetch module as a virtual module. 5 + /// Rolldown plugin to resolve prefetch modules to their actual file paths. 9 6 #[derive(Debug)] 10 7 pub struct PrefetchPlugin; 11 8 9 + impl PrefetchPlugin { 10 + /// Get the base directory where the prefetch files are located. 11 + fn get_base_dir() -> PathBuf { 12 + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("js") 13 + } 14 + 15 + /// Resolve a maudit:prefetch specifier to its actual file path. 16 + fn resolve_prefetch_path(specifier: &str) -> Option<PathBuf> { 17 + let base_dir = Self::get_base_dir(); 18 + 19 + match specifier { 20 + "maudit:prefetch" => Some(base_dir.join("prefetch.ts")), 21 + "maudit:prefetch:hover" => Some(base_dir.join("prefetch").join("hover.ts")), 22 + _ => None, 23 + } 24 + } 25 + } 26 + 12 27 impl Plugin for PrefetchPlugin { 13 28 fn name(&self) -> std::borrow::Cow<'static, str> { 14 29 "builtin:prefetch".into() 15 30 } 16 31 17 32 fn register_hook_usage(&self) -> HookUsage { 18 - HookUsage::ResolveId | HookUsage::Load 33 + HookUsage::ResolveId 19 34 } 20 35 21 36 async fn resolve_id( ··· 23 38 _ctx: &rolldown::plugin::PluginContext, 24 39 args: &rolldown::plugin::HookResolveIdArgs<'_>, 25 40 ) -> rolldown::plugin::HookResolveIdReturn { 26 - if args.specifier == "maudit:prefetch" { 41 + if let Some(path) = Self::resolve_prefetch_path(args.specifier) { 27 42 return Ok(Some(rolldown::plugin::HookResolveIdOutput { 28 - id: "maudit:prefetch".to_string().into(), 29 - ..Default::default() 30 - })); 31 - } 32 - Ok(None) 33 - } 34 - 35 - async fn load( 36 - &self, 37 - _ctx: &rolldown::plugin::PluginContext, 38 - args: &rolldown::plugin::HookLoadArgs<'_>, 39 - ) -> rolldown::plugin::HookLoadReturn { 40 - if args.id == "maudit:prefetch" { 41 - return Ok(Some(rolldown::plugin::HookLoadOutput { 42 - code: PREFETCH_CODE.to_string().into(), 43 - module_type: Some(ModuleType::Ts), 43 + id: path.to_string_lossy().to_string().into(), 44 44 ..Default::default() 45 45 })); 46 46 }
+1 -1
crates/maudit/src/build.rs
··· 134 134 135 135 if options.prefetch { 136 136 let prefetch_script = Script::new( 137 - PathBuf::from("maudit:prefetch"), 137 + PathBuf::from("maudit:prefetch:hover"), 138 138 true, 139 139 { 140 140 use rapidhash::fast::RapidHasher;
+14 -4
xtask/src/main.rs
··· 102 102 fs::create_dir_all(&js_dist_dir)?; 103 103 104 104 // Configure Rolldown bundler input 105 - let input_items = vec![InputItem { 106 - name: Some("prefetch".to_string()), 107 - import: js_src_dir.join("prefetch.ts").to_string_lossy().to_string(), 108 - }]; 105 + let input_items = vec![ 106 + InputItem { 107 + name: Some("prefetch".to_string()), 108 + import: js_src_dir.join("prefetch.ts").to_string_lossy().to_string(), 109 + }, 110 + InputItem { 111 + name: Some("hover".to_string()), 112 + import: js_src_dir 113 + .join("prefetch") 114 + .join("hover.ts") 115 + .to_string_lossy() 116 + .to_string(), 117 + }, 118 + ]; 109 119 110 120 let bundler_options = BundlerOptions { 111 121 input: Some(input_items),