diff --git a/docs/zh/guide/markdown.md b/docs/zh/guide/markdown.md index 93db97e0..674b7c2e 100644 --- a/docs/zh/guide/markdown.md +++ b/docs/zh/guide/markdown.md @@ -625,11 +625,11 @@ const line4 = 'This is line 4' ```md <<< @/snippets/snippet.cs{c#} - + <<< @/snippets/snippet.cs{1,2,4-6 c#} - + <<< @/snippets/snippet.cs{1,2,4-6 c#:line-numbers} ``` diff --git a/src/client/app/data.ts b/src/client/app/data.ts index ccca8123..c75f79e7 100644 --- a/src/client/app/data.ts +++ b/src/client/app/data.ts @@ -44,9 +44,9 @@ export interface VitePressData { title: Ref description: Ref lang: Ref - isDark: Ref dir: Ref localeIndex: Ref + isDark: Ref } // site data is a singleton @@ -89,14 +89,12 @@ export function initData(route: Route): VitePressData { frontmatter: computed(() => route.data.frontmatter), params: computed(() => route.data.params), lang: computed(() => site.value.lang), - dir: computed(() => route.data.frontmatter.dir || site.value.dir || 'ltr'), + dir: computed(() => route.data.frontmatter.dir || site.value.dir), localeIndex: computed(() => site.value.localeIndex || 'root'), - title: computed(() => { - return createTitle(site.value, route.data) - }), - description: computed(() => { - return route.data.description || site.value.description - }), + title: computed(() => createTitle(site.value, route.data)), + description: computed( + () => route.data.description || site.value.description + ), isDark } } diff --git a/src/client/app/index.ts b/src/client/app/index.ts index a7db0c23..57cfe15a 100644 --- a/src/client/app/index.ts +++ b/src/client/app/index.ts @@ -38,13 +38,13 @@ const Theme = resolveThemeExtends(RawTheme) const VitePressApp = defineComponent({ name: 'VitePressApp', setup() { - const { site } = useData() + const { site, lang, dir } = useData() // change the language on the HTML element based on the current lang onMounted(() => { watchEffect(() => { - document.documentElement.lang = site.value.lang - document.documentElement.dir = site.value.dir + document.documentElement.lang = lang.value + document.documentElement.dir = dir.value }) }) diff --git a/src/client/theme-default/composables/outline.ts b/src/client/theme-default/composables/outline.ts index 59ee7447..8ec4f3e5 100644 --- a/src/client/theme-default/composables/outline.ts +++ b/src/client/theme-default/composables/outline.ts @@ -4,10 +4,11 @@ import type { Header } from '../../shared' import { useAside } from './aside' import { throttleAndDebounce } from '../support/utils' -// magic number to avoid repeated retrieval -const PAGE_OFFSET = 71 +// cached list of anchor elements from resolveHeaders +const resolvedHeaders: { element: HTMLHeadElement; link: string }[] = [] export type MenuItem = Omit & { + element: HTMLHeadElement children?: MenuItem[] } @@ -29,6 +30,7 @@ export function getHeaders(range: DefaultTheme.Config['outline']) { .map((el) => { const level = Number(el.tagName[1]) return { + element: el as HTMLHeadElement, title: serializeHeader(el), link: '#' + el.id, level @@ -78,6 +80,12 @@ export function resolveHeaders( : levelsRange headers = headers.filter((h) => h.level >= high && h.level <= low) + // clear previous caches + resolvedHeaders.length = 0 + // update global header list for active link rendering + for (const { element, link } of headers) { + resolvedHeaders.push({ element, link }) + } const ret: MenuItem[] = [] outer: for (let i = 0; i < headers.length; i++) { @@ -128,40 +136,55 @@ export function useActiveAnchor( return } - const links = [].slice.call( - container.value.querySelectorAll('.outline-link') - ) as HTMLAnchorElement[] - - const anchors = [].slice - .call(document.querySelectorAll('.content .header-anchor')) - .filter((anchor: HTMLAnchorElement) => { - return links.some((link) => { - return link.hash === anchor.hash && anchor.offsetParent !== null - }) - }) as HTMLAnchorElement[] + // pixel offset, start of main content + const offsetDocTop = (() => { + const container = + document.querySelector('#VPContent .VPDoc')?.firstElementChild + if (container) return getAbsoluteTop(container as HTMLElement) + else return 78 + })() const scrollY = window.scrollY const innerHeight = window.innerHeight const offsetHeight = document.body.offsetHeight const isBottom = Math.abs(scrollY + innerHeight - offsetHeight) < 1 - // page bottom - highlight last one - if (anchors.length && isBottom) { - activateLink(anchors[anchors.length - 1].hash) + // resolvedHeaders may be repositioned, hidden or fix positioned + const headers = resolvedHeaders + .map(({ element, link }) => ({ + link, + top: getAbsoluteTop(element) + })) + .filter(({ top }) => !Number.isNaN(top)) + .sort((a, b) => a.top - b.top) + + // no headers available for active link + if (!headers.length) { + activateLink(null) return } - for (let i = 0; i < anchors.length; i++) { - const anchor = anchors[i] - const nextAnchor = anchors[i + 1] + // page top + if (scrollY < 1) { + activateLink(null) + return + } - const [isActive, hash] = isAnchorActive(i, anchor, nextAnchor) + // page bottom - highlight last link + if (isBottom) { + activateLink(headers[headers.length - 1].link) + return + } - if (isActive) { - activateLink(hash) - return + // find the last header above the top of viewport + let activeLink: string | null = null + for (const { link, top } of headers) { + if (top > scrollY + offsetDocTop) { + break } + activeLink = link } + activateLink(activeLink) } function activateLink(hash: string | null) { @@ -190,28 +213,18 @@ export function useActiveAnchor( } } -function getAnchorTop(anchor: HTMLAnchorElement): number { - return anchor.parentElement!.offsetTop - PAGE_OFFSET -} - -function isAnchorActive( - index: number, - anchor: HTMLAnchorElement, - nextAnchor: HTMLAnchorElement | undefined -): [boolean, string | null] { - const scrollTop = window.scrollY - - if (index === 0 && scrollTop === 0) { - return [true, null] - } - - if (scrollTop < getAnchorTop(anchor)) { - return [false, null] - } - - if (!nextAnchor || scrollTop < getAnchorTop(nextAnchor)) { - return [true, anchor.hash] +function getAbsoluteTop(element: HTMLElement): number { + let offsetTop = 0 + while (element !== document.body) { + if (element === null) { + // child element is: + // - not attached to the DOM (display: none) + // - set to fixed position (not scrollable) + // - body or html element (null offsetParent) + return NaN + } + offsetTop += element.offsetTop + element = element.offsetParent as HTMLElement } - - return [false, null] + return offsetTop } diff --git a/src/node/build/render.ts b/src/node/build/render.ts index 082b4898..df8fb5e1 100644 --- a/src/node/build/render.ts +++ b/src/node/build/render.ts @@ -168,9 +168,11 @@ export async function renderPage( } } + const dir = pageData.frontmatter.dir || siteData.dir || 'ltr' + const html = [ ``, - ``, + ``, ``, ``, isMetaViewportOverridden(head)