import { reactive , inject , markRaw , nextTick , readonly } from 'vue' ; import type { Component , InjectionKey } from 'vue' ; import { PageData } from '../shared' ; import { inBrowser } from './utils' ; import { siteDataRef } from './data' ; export interface Route { path : string ; data : PageData ; component : Component | null ; } export interface Router { route : Route ; go : ( href ? : string ) => Promise < void > ; } export const RouterSymbol = Symbol ( ) ; const fakeHost = `http://a.com` ; const getDefaultRoute = ( ) : Route => ( { path : '/' , component : null , data : { frontmatter : { } } as any } ) ; interface PageModule { __pageData : string ; default : Component ; } export function createRouter ( loadPageModule , fallbackComponent ? ) { const route = reactive ( getDefaultRoute ( ) ) ; function go ( href = inBrowser ? location . href : '/' ) { const url = new URL ( href , fakeHost ) ; if ( ! url . pathname . endsWith ( '/' ) && ! url . pathname . endsWith ( '.html' ) ) { url . pathname += '.html' ; href = url . pathname + url . search + url . hash ; } if ( inBrowser ) { history . replaceState ( { scrollPosition : window . scrollY } , document . title ) ; history . pushState ( null , '' , href ) ; } return loadPage ( href ) ; } let latestPendingPath = null ; async function loadPage ( href , scrollPosition = 0 ) { const targetLoc = new URL ( href , fakeHost ) ; const pendingPath = ( latestPendingPath = targetLoc . pathname ) ; try { let page = loadPageModule ( pendingPath ) ; if ( 'then' in page && typeof page . then === 'function' ) { page = await page ; } if ( latestPendingPath === pendingPath ) { latestPendingPath = null ; const { default : comp , __pageData } = page as PageModule ; if ( ! comp ) { throw new Error ( `Invalid route component: ${ comp }` ) ; } route . path = pendingPath ; route . component = markRaw ( comp ) ; route . data = import . meta . env . PROD ? markRaw ( JSON . parse ( __pageData ) ) : ( readonly ( JSON . parse ( __pageData ) ) as PageData ) ; if ( inBrowser ) { nextTick ( ( ) => { if ( targetLoc . hash && ! scrollPosition ) { let target = null ; try { target = document . querySelector ( decodeURIComponent ( targetLoc . hash ) ) as HTMLElement ; } catch ( e ) { console . warn ( e ) ; } if ( target ) { scrollTo ( target , targetLoc . hash ) ; return ; } } window . scrollTo ( 0 , scrollPosition ) ; } ) ; } } } catch ( err ) { if ( ! err . message . match ( /fetch/ ) ) { console . error ( err ) ; } if ( latestPendingPath === pendingPath ) { latestPendingPath = null ; route . path = pendingPath ; route . component = fallbackComponent ? markRaw ( fallbackComponent ) : null ; } } } if ( inBrowser ) { window . addEventListener ( 'click' , ( e ) => { const link = ( e . target as Element ) . closest ( 'a' ) ; if ( link ) { const { href , protocol , hostname , pathname , hash , target } = link ; const currentUrl = window . location ; const extMatch = pathname . match ( /\.\w+$/ ) ; if ( ! e . ctrlKey && ! e . shiftKey && ! e . altKey && ! e . metaKey && target !== `_blank` && protocol === currentUrl . protocol && hostname === currentUrl . hostname && ! ( extMatch && extMatch [ 0 ] !== '.html' ) ) { e . preventDefault ( ) ; if ( pathname === currentUrl . pathname ) { if ( hash && hash !== currentUrl . hash ) { history . pushState ( null , '' , hash ) ; window . dispatchEvent ( new Event ( 'hashchange' ) ) ; scrollTo ( link , hash , link . classList . contains ( 'header-anchor' ) ) ; } } else { go ( href ) ; } } } } , { capture : true } ) ; window . addEventListener ( 'popstate' , ( e ) => { loadPage ( location . href , ( e . state && e . state . scrollPosition ) || 0 ) ; } ) ; window . addEventListener ( 'hashchange' , ( e ) => { e . preventDefault ( ) ; } ) ; } return { route , go } ; } export function useRouter ( ) { const router = inject ( RouterSymbol ) ; if ( ! router ) { throw new Error ( 'useRouter() is called without provider.' ) ; } return router ; } export function useRoute ( ) { return useRouter ( ) . route ; } function scrollTo ( el , hash , smooth = false ) { let target = null ; try { target = el . classList . contains ( '.header-anchor' ) ? el : document . querySelector ( decodeURIComponent ( hash ) ) ; } catch ( e ) { console . warn ( e ) ; } if ( target ) { let offset = siteDataRef . value . scrollOffset ; if ( typeof offset === 'string' ) { offset = document . querySelector ( offset ) ! . getBoundingClientRect ( ) . bottom + 24 ; } const targetPadding = parseInt ( window . getComputedStyle ( target as HTMLElement ) . paddingTop , 10 ) ; const targetTop = window . scrollY + ( target as HTMLElement ) . getBoundingClientRect ( ) . top - offset + targetPadding ; if ( ! smooth || Math . abs ( targetTop - window . scrollY ) > window . innerHeight ) { window . scrollTo ( 0 , targetTop ) ; } else { window . scrollTo ( { left : 0 , top : targetTop , behavior : 'smooth' } ) ; } } }