import { siteDataRef } from './data' import { inBrowser, EXTERNAL_URL_RE, sanitizeFileName, type Awaitable } from '../shared' import { h, onMounted, onUnmounted, shallowRef, type AsyncComponentLoader } from 'vue' export { inBrowser } from '../shared' /** * Join two paths by resolving the slash collision. */ export function joinPath(base: string, path: string) { return `${base}${path}`.replace(/\/+/g, '/') } /** * Append base to internal (non-relative) urls */ export function withBase(path: string) { return EXTERNAL_URL_RE.test(path) || !path.startsWith('/') ? path : joinPath(siteDataRef.value.base, path) } /** * Converts a url path to the corresponding js chunk filename. */ export function pathToFile(path: string) { let pagePath = path.replace(/\.html$/, '') pagePath = decodeURIComponent(pagePath) pagePath = pagePath.replace(/\/$/, '/index') // /foo/ -> /foo/index if (import.meta.env.DEV) { // always force re-fetch content in dev pagePath += `.md?t=${Date.now()}` } else { // in production, each .md file is built into a .md.js file following // the path conversion scheme. // /foo/bar.html -> ./foo_bar.md if (inBrowser) { const base = import.meta.env.BASE_URL pagePath = sanitizeFileName( pagePath.slice(base.length).replace(/\//g, '_') || 'index' ) + '.md' // client production build needs to account for page hash, which is // injected directly in the page's html let pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()] if (!pageHash) { pagePath = pagePath.endsWith('_index.md') ? pagePath.slice(0, -9) + '.md' : pagePath.slice(0, -3) + '_index.md' pageHash = __VP_HASH_MAP__[pagePath.toLowerCase()] } if (!pageHash) return null pagePath = `${base}${__ASSETS_DIR__}/${pagePath}.${pageHash}.js` } else { // ssr build uses much simpler name mapping pagePath = `./${sanitizeFileName( pagePath.slice(1).replace(/\//g, '_') )}.md.js` } } return pagePath } export let contentUpdatedCallbacks: (() => any)[] = [] /** * Register callback that is called every time the markdown content is updated * in the DOM. */ export function onContentUpdated(fn: () => any) { contentUpdatedCallbacks.push(fn) onUnmounted(() => { contentUpdatedCallbacks = contentUpdatedCallbacks.filter((f) => f !== fn) }) } export function defineClientComponent( loader: AsyncComponentLoader, args?: any[], cb?: () => Awaitable<void> ) { return { setup() { const comp = shallowRef() onMounted(async () => { let res = await loader() // interop module default if (res && (res.__esModule || res[Symbol.toStringTag] === 'Module')) { res = res.default } comp.value = res await cb?.() }) return () => (comp.value ? h(comp.value, ...(args ?? [])) : null) } } } export function getScrollOffset() { let scrollOffset = siteDataRef.value.scrollOffset let offset = 0 let padding = 24 if (typeof scrollOffset === 'object' && 'padding' in scrollOffset) { padding = scrollOffset.padding scrollOffset = scrollOffset.selector } if (typeof scrollOffset === 'number') { offset = scrollOffset } else if (typeof scrollOffset === 'string') { offset = tryOffsetSelector(scrollOffset, padding) } else if (Array.isArray(scrollOffset)) { for (const selector of scrollOffset) { const res = tryOffsetSelector(selector, padding) if (res) { offset = res break } } } return offset } function tryOffsetSelector(selector: string, padding: number): number { const el = document.querySelector(selector) if (!el) return 0 const bot = el.getBoundingClientRect().bottom if (bot < 0) return 0 return bot + padding }