From 6131138e656a2c9f1ddabb1f906c398263352162 Mon Sep 17 00:00:00 2001 From: Pascal Fischer Date: Wed, 10 May 2023 00:46:38 +0200 Subject: [PATCH] add TOC --- mdx/rehype.mjs | 5 +- next.config.mjs | 2 + package.json | 1 + src/components/Footer.jsx | 2 +- src/components/Heading.jsx | 12 +- src/components/{Layout.jsx => LayoutAPI.jsx} | 6 +- src/components/LayoutDocs.jsx | 273 ++++++++++++++++++ src/components/MobileNavigation.jsx | 4 +- .../{Navigation.jsx => NavigationAPI.jsx} | 8 +- src/components/NavigationDocs.jsx | 125 ++++++++ src/components/mdx.jsx | 12 + src/pages/_app.jsx | 47 ++- .../netbird-vs-traditional-vpn.mdx | 4 +- 13 files changed, 476 insertions(+), 25 deletions(-) rename src/components/{Layout.jsx => LayoutAPI.jsx} (86%) create mode 100644 src/components/LayoutDocs.jsx rename src/components/{Navigation.jsx => NavigationAPI.jsx} (97%) create mode 100644 src/components/NavigationDocs.jsx diff --git a/mdx/rehype.mjs b/mdx/rehype.mjs index d13c65aa..b137262d 100644 --- a/mdx/rehype.mjs +++ b/mdx/rehype.mjs @@ -56,7 +56,7 @@ function rehypeSlugify() { return (tree) => { let slugify = slugifyWithCounter() visit(tree, 'element', (node) => { - if (node.tagName === 'h2' && !node.properties.id) { + if ( ['h2', 'h3', 'h4', 'h5', 'h6'].includes(node.tagName) && !node.properties.id) { node.properties.id = slugify(toString(node)) } }) @@ -97,10 +97,11 @@ function getSections(node) { let sections = [] for (let child of node.children ?? []) { - if (child.type === 'element' && child.tagName === 'h2') { + if (child.type === 'element' && ['h2', 'h3', 'h4', 'h5', 'h6'].includes(child.tagName)) { sections.push(`{ title: ${JSON.stringify(toString(child))}, id: ${JSON.stringify(child.properties.id)}, + tagName: ${JSON.stringify(child.tagName)}, ...${child.properties.annotation} }`) } else if (child.children) { diff --git a/next.config.mjs b/next.config.mjs index 15abe9e0..5ff9474b 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,10 +2,12 @@ import nextMDX from '@next/mdx' import { remarkPlugins } from './mdx/remark.mjs' import { rehypePlugins } from './mdx/rehype.mjs' import { recmaPlugins } from './mdx/recma.mjs' +import rehypeSlug from "rehype-slug"; const withMDX = nextMDX({ options: { remarkPlugins, + // rehypeSlug, rehypePlugins, recmaPlugins, providerImportSource: '@mdx-js/react', diff --git a/package.json b/package.json index b5ef0e64..bdf75426 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "react-dom": "18.2.0", "recma-nextjs-static-props": "^1.0.0", "rehype-mdx-title": "^2.0.0", + "rehype-slug": "^5.1.0", "remark-gfm": "^3.0.1", "shiki": "^0.14.2", "tailwindcss": "^3.3.0", diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 27512729..debd880d 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/router' import { Transition } from '@headlessui/react' import { Button } from '@/components/Button' -import {apiNavigation, docsNavigation, navigation} from '@/components/Navigation' +import {apiNavigation, docsNavigation, navigation} from '@/components/NavigationAPI' function CheckIcon(props) { return ( diff --git a/src/components/Heading.jsx b/src/components/Heading.jsx index e3dd28ef..997e273b 100644 --- a/src/components/Heading.jsx +++ b/src/components/Heading.jsx @@ -67,18 +67,18 @@ export function Heading({ }) { let Component = `h${level}` let ref = useRef() - let registerHeading = useSectionStore((s) => s.registerHeading) + // let registerHeading = useSectionStore((s) => s.registerHeading) let inView = useInView(ref, { margin: `${remToPx(-3.5)}px 0px 0px 0px`, amount: 'all', }) - useEffect(() => { - if (level === 2) { - registerHeading({ id, ref, offsetRem: tag || label ? 8 : 6 }) - } - }) + // useEffect(() => { + // if (level === 2) { + // registerHeading({ id, ref, offsetRem: tag || label ? 8 : 6 }) + // } + // }) return ( <> diff --git a/src/components/Layout.jsx b/src/components/LayoutAPI.jsx similarity index 86% rename from src/components/Layout.jsx rename to src/components/LayoutAPI.jsx index 46df45ce..07ee9d07 100644 --- a/src/components/Layout.jsx +++ b/src/components/LayoutAPI.jsx @@ -4,11 +4,11 @@ import { motion } from 'framer-motion' import { Footer } from '@/components/Footer' import { Header } from '@/components/Header' import { Logo } from '@/components/Logo' -import { Navigation } from '@/components/Navigation' +import { NavigationAPI } from '@/components/NavigationAPI' import { Prose } from '@/components/Prose' import { SectionProvider } from '@/components/SectionProvider' -export function Layout({ children, sections = [] }) { +export function LayoutAPI({ children, sections = [] }) { return (
@@ -23,7 +23,7 @@ export function Layout({ children, sections = [] }) {
- +
diff --git a/src/components/LayoutDocs.jsx b/src/components/LayoutDocs.jsx new file mode 100644 index 00000000..9f8ba6bb --- /dev/null +++ b/src/components/LayoutDocs.jsx @@ -0,0 +1,273 @@ +import { useCallback, useEffect, useState } from 'react' +import Link from 'next/link' +import { useRouter } from 'next/router' +import clsx from 'clsx' + +import { Logo, Logomark } from '@/components/Logo' +import { MobileNavigation } from '@/components/MobileNavigation' +import { Prose } from '@/components/Prose' +import { Search } from '@/components/Search' +import {HeroPattern} from "@/components/HeroPattern"; +import {NavigationDocs} from "@/components/NavigationDocs"; +import {Header} from "@/components/Header"; +import {NavigationAPI} from "@/components/NavigationAPI"; +import {motion} from "framer-motion"; +import {Footer} from "@/components/Footer"; + +const navigation = [ + { + title: 'Introduction', + links: [ + { title: 'Getting started', href: '/' }, + { title: 'Installation', href: '/docs/installation' }, + ], + }, + { + title: 'Core concepts', + links: [ + { title: 'Understanding caching', href: '/docs/understanding-caching' }, + { + title: 'Predicting user behavior', + href: '/docs/predicting-user-behavior', + }, + { title: 'Basics of time-travel', href: '/docs/basics-of-time-travel' }, + { + title: 'Introduction to string theory', + href: '/docs/introduction-to-string-theory', + }, + { title: 'The butterfly effect', href: '/docs/the-butterfly-effect' }, + ], + }, + { + title: 'Advanced guides', + links: [ + { title: 'Writing plugins', href: '/docs/writing-plugins' }, + { title: 'Neuralink integration', href: '/docs/neuralink-integration' }, + { title: 'Temporal paradoxes', href: '/docs/temporal-paradoxes' }, + { title: 'Testing', href: '/docs/testing' }, + { title: 'Compile-time caching', href: '/docs/compile-time-caching' }, + { + title: 'Predictive data generation', + href: '/docs/predictive-data-generation', + }, + ], + }, + { + title: 'API reference', + links: [ + { title: 'CacheAdvance.predict()', href: '/docs/cacheadvance-predict' }, + { title: 'CacheAdvance.flush()', href: '/docs/cacheadvance-flush' }, + { title: 'CacheAdvance.revert()', href: '/docs/cacheadvance-revert' }, + { title: 'CacheAdvance.regret()', href: '/docs/cacheadvance-regret' }, + ], + }, + { + title: 'Contributing', + links: [ + { title: 'How to contribute', href: '/docs/how-to-contribute' }, + { title: 'Architecture guide', href: '/docs/architecture-guide' }, + { title: 'Design principles', href: '/docs/design-principles' }, + ], + }, +] + +function GitHubIcon(props) { + return ( + + ) +} + +// function Header({ navigation }) { +// let [isScrolled, setIsScrolled] = useState(false) +// +// useEffect(() => { +// function onScroll() { +// setIsScrolled(window.scrollY > 0) +// } +// onScroll() +// window.addEventListener('scroll', onScroll, { passive: true }) +// return () => { +// window.removeEventListener('scroll', onScroll) +// } +// }, []) +// +// return ( +//
+//
+// +//
+//
+// +// +// +// +//
+//
+// +//
+//
+// +// +// +//
+//
+// ) +// } + +function useTableOfContents(tableOfContents) { + let [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id) + + let getHeadings = useCallback((tableOfContents) => { + return tableOfContents + .flatMap((node) => [node.id, ...node.children.map((child) => child.id)]) + .map((id) => { + let el = document.getElementById(id) + if (!el) return + + let style = window.getComputedStyle(el) + let scrollMt = parseFloat(style.scrollMarginTop) + + let top = window.scrollY + el.getBoundingClientRect().top - scrollMt + return { id, top } + }) + }, []) + + useEffect(() => { + if (tableOfContents.length === 0) return + let headings = getHeadings(tableOfContents) + function onScroll() { + let top = window.scrollY + let current = headings[0].id + for (let heading of headings) { + if (top >= heading.top) { + current = heading.id + } else { + break + } + } + setCurrentSection(current) + } + window.addEventListener('scroll', onScroll, { passive: true }) + onScroll() + return () => { + window.removeEventListener('scroll', onScroll) + } + }, [getHeadings, tableOfContents]) + + return currentSection +} + +export function LayoutDocs({ children, title, tableOfContents }) { + let router = useRouter() + let isHomePage = router.pathname === '/' + let allLinks = navigation.flatMap((section) => section.links) + let linkIndex = allLinks.findIndex((link) => link.href === router.pathname) + let previousPage = allLinks[linkIndex - 1] + let nextPage = allLinks[linkIndex + 1] + let section = navigation.find((section) => + section.links.find((link) => link.href === router.pathname) + ) + let currentSection = useTableOfContents(tableOfContents) + + function isActive(section) { + if (section.id === currentSection) { + return true + } + if (!section.children) { + return false + } + return section.children.findIndex(isActive) > -1 + } + + return ( + <> + {/*
*/} + + +
+ +
+
+ + + +
+
+ +
+
+
+
+ {children} +
+
+
+
+ +
+
+ + ) +} diff --git a/src/components/MobileNavigation.jsx b/src/components/MobileNavigation.jsx index d37f0f2d..5293929f 100644 --- a/src/components/MobileNavigation.jsx +++ b/src/components/MobileNavigation.jsx @@ -4,7 +4,7 @@ import { motion } from 'framer-motion' import { create } from 'zustand' import { Header } from '@/components/Header' -import { Navigation } from '@/components/Navigation' +import { NavigationAPI } from '@/components/NavigationAPI' function MenuIcon(props) { return ( @@ -103,7 +103,7 @@ export function MobileNavigation() { layoutScroll className="fixed bottom-0 left-0 top-14 w-full overflow-y-auto bg-white px-4 pb-4 pt-6 shadow-lg shadow-zinc-900/10 ring-1 ring-zinc-900/7.5 dark:bg-zinc-900 dark:ring-zinc-800 min-[416px]:max-w-sm sm:px-6 sm:pb-10" > - + diff --git a/src/components/Navigation.jsx b/src/components/NavigationAPI.jsx similarity index 97% rename from src/components/Navigation.jsx rename to src/components/NavigationAPI.jsx index b6a4919c..ff677bd1 100644 --- a/src/components/Navigation.jsx +++ b/src/components/NavigationAPI.jsx @@ -15,7 +15,7 @@ function useInitialValue(value, condition = true) { return condition ? initialValue : value } -function TopLevelNavItem({ href, children }) { +export function TopLevelNavItem({ href, children }) { return (
  • link.href === pathname) @@ -248,7 +248,7 @@ export const apiNavigation = [ }, ] -export function Navigation(props) { +export function NavigationAPI(props) { let router = useRouter() return (