Compare commits
No commits in common. "364c548aa806ac62121ca64d18c958764cd2f88d" and "3c08fe0b078d3b75923403ca2ac3788c67b9792f" have entirely different histories.
364c548aa8
...
3c08fe0b07
@ -15,14 +15,14 @@ type DefaultLayoutProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function DefaultLayout(props: DefaultLayoutProps) {
|
export default function DefaultLayout(props: DefaultLayoutProps) {
|
||||||
const pageContext = usePageContext();
|
const { urlPathname } = usePageContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="flex w-full flex-col font-sans">
|
<div class="flex w-full flex-col font-sans">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
{pageContext.urlPathname === "/" && <HeroSection />}
|
{urlPathname === "/" && <HeroSection />}
|
||||||
|
|
||||||
<div class="relative mx-auto w-full flex max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12">
|
<div class="relative mx-auto w-full flex max-w-8xl flex-auto justify-center sm:px-2 lg:px-8 xl:px-12">
|
||||||
<div class="hidden lg:relative lg:block lg:flex-none">
|
<div class="hidden lg:relative lg:block lg:flex-none">
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun ./fastify-entry.ts",
|
"dev": "bun ./fastify-entry.ts",
|
||||||
"build": "cross-env DEBUG=vike:error,vike:log vike build",
|
"build": "cross-env DEBUG=vike:error,vike:log vike build",
|
||||||
"preview": "cross-env NODE_ENV=production bun ./fastify-entry.ts",
|
"preview": "cross-env NODE_ENV=production bun ./fastify-server.ts",
|
||||||
"production": "bun run build && bun run preview",
|
"production": "bun run build && bun run preview",
|
||||||
"lint": "biome lint --write .",
|
"lint": "biome lint --write .",
|
||||||
"format": "biome format --write ."
|
"format": "biome format --write ."
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import type { SectionCache } from "@/services/DocCache";
|
|
||||||
import type { PageContext } from "vike/types";
|
import type { PageContext } from "vike/types";
|
||||||
|
|
||||||
import { useConfig } from "vike-solid/useConfig";
|
import { useConfig } from "vike-solid/useConfig";
|
||||||
import { docCache } from "@/services/DocCache";
|
|
||||||
import buildTitle from "./buildTitle";
|
import buildTitle from "./buildTitle";
|
||||||
|
|
||||||
export type Data = Awaited<ReturnType<typeof data>>;
|
export type Data = Awaited<ReturnType<typeof data>>;
|
||||||
@ -11,7 +9,7 @@ export async function data(pageContext: PageContext) {
|
|||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
exports: { frontmatter },
|
exports: { tableOfContents, frontmatter },
|
||||||
urlParsed,
|
urlParsed,
|
||||||
} = pageContext;
|
} = pageContext;
|
||||||
const isRoot = urlParsed.pathname === "/";
|
const isRoot = urlParsed.pathname === "/";
|
||||||
@ -21,15 +19,7 @@ export async function data(pageContext: PageContext) {
|
|||||||
description: frontmatter?.description,
|
description: frontmatter?.description,
|
||||||
});
|
});
|
||||||
|
|
||||||
let cachePathname = urlParsed.pathname.replace(/\/$/, "").replace(/^\//, "");
|
|
||||||
if (cachePathname === "") {
|
|
||||||
cachePathname = "index";
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc = docCache.get(cachePathname);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sections: doc?.sections || [],
|
tableOfContents,
|
||||||
frontmatter,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,15 +12,13 @@ type NavigationItemProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function NavigationItem(props: NavigationItemProps) {
|
function NavigationItem(props: NavigationItemProps) {
|
||||||
const pageContext = usePageContext();
|
const { urlPathname } = usePageContext();
|
||||||
|
|
||||||
const [isOpened, setIsOpened] = createSignal(
|
const [isOpened, setIsOpened] = createSignal(
|
||||||
props.section.links.some(
|
props.section.links.some(
|
||||||
(link) =>
|
(link) =>
|
||||||
link.href === pageContext.urlPathname ||
|
link.href === urlPathname ||
|
||||||
link.subitems?.some(
|
link.subitems?.some((subitem) => subitem.href === urlPathname),
|
||||||
(subitem) => subitem.href === pageContext.urlPathname,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -66,9 +64,9 @@ function NavigationItem(props: NavigationItemProps) {
|
|||||||
link={link}
|
link={link}
|
||||||
onLinkClick={props.onLinkClick}
|
onLinkClick={props.onLinkClick}
|
||||||
isOpened={
|
isOpened={
|
||||||
link.href === pageContext.urlPathname ||
|
link.href === urlPathname ||
|
||||||
link.subitems?.some(
|
link.subitems?.some(
|
||||||
(subitem) => subitem.href === pageContext.urlPathname,
|
(subitem) => subitem.href === urlPathname,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -89,16 +87,14 @@ type NavigationSubItemProps = {
|
|||||||
|
|
||||||
function NavigationSubItem(props: NavigationSubItemProps) {
|
function NavigationSubItem(props: NavigationSubItemProps) {
|
||||||
const [isOpened, setIsOpened] = createSignal(props.isOpened);
|
const [isOpened, setIsOpened] = createSignal(props.isOpened);
|
||||||
const pageContext = usePageContext();
|
const { urlPathname } = usePageContext();
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setIsOpened(
|
setIsOpened(
|
||||||
props.link.href === pageContext.urlPathname ||
|
props.link.href === urlPathname ||
|
||||||
props.link.subitems?.some(
|
props.link.subitems?.some((subitem) => subitem.href === urlPathname),
|
||||||
(subitem) => subitem.href === pageContext.urlPathname,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}, [pageContext.urlPathname, props.link]);
|
}, [urlPathname, props.link]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -135,7 +131,7 @@ function NavigationSubItem(props: NavigationSubItemProps) {
|
|||||||
"before:top-3 before:-translate-y-1/2 font-semibold":
|
"before:top-3 before:-translate-y-1/2 font-semibold":
|
||||||
props.link.subitems,
|
props.link.subitems,
|
||||||
},
|
},
|
||||||
props.link.href !== pageContext.urlPathname && "before:hidden",
|
props.link.href !== urlPathname && "before:hidden",
|
||||||
isOpened()
|
isOpened()
|
||||||
? "text-violet-500 before:bg-violet-500"
|
? "text-violet-500 before:bg-violet-500"
|
||||||
: "text-slate-500 before:bg-slate-300 hover:text-slate-600 hover:before:block",
|
: "text-slate-500 before:bg-slate-300 hover:text-slate-600 hover:before:block",
|
||||||
@ -157,7 +153,7 @@ function NavigationSubItem(props: NavigationSubItemProps) {
|
|||||||
onClick={props.onLinkClick}
|
onClick={props.onLinkClick}
|
||||||
class={clsx(
|
class={clsx(
|
||||||
"block w-full pl-3.5 before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full",
|
"block w-full pl-3.5 before:pointer-events-none before:absolute before:top-1/2 before:-left-1 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full",
|
||||||
subitem.href === pageContext.urlPathname
|
subitem.href === urlPathname
|
||||||
? "font-semibold text-violet-500 before:bg-violet-500"
|
? "font-semibold text-violet-500 before:bg-violet-500"
|
||||||
: "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block",
|
: "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { DocSection } from "@/services/DocCache";
|
import type { TableOfContents as TableOfContentsType } from "@/remarkHeadingId";
|
||||||
|
import type { Section } from "@/libs/sections";
|
||||||
import type { Data } from "@/pages/+data";
|
import type { Data } from "@/pages/+data";
|
||||||
|
|
||||||
import { createSignal, createEffect, For } from "solid-js";
|
import { createSignal, createEffect, For } from "solid-js";
|
||||||
@ -7,32 +8,31 @@ import { Link } from "@/components/Link";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export function TableOfContents() {
|
export function TableOfContents() {
|
||||||
const data = useData<Data>();
|
const { tableOfContents } = useData<Data>();
|
||||||
if (!data.sections) return null;
|
|
||||||
|
if (!tableOfContents) return null;
|
||||||
|
|
||||||
const [currentSection, setCurrentSection] = createSignal(
|
const [currentSection, setCurrentSection] = createSignal(
|
||||||
data.sections[0]?.hash,
|
tableOfContents[0]?.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getHeadings = () => {
|
const getHeadings = () => {
|
||||||
return data.sections
|
return tableOfContents
|
||||||
.map((section) => {
|
.map((section) => {
|
||||||
if (!section.hash) return null;
|
const el = document.getElementById(section.id);
|
||||||
|
|
||||||
const el = document.getElementById(section.hash);
|
|
||||||
if (!el) return null;
|
if (!el) return null;
|
||||||
|
|
||||||
const style = window.getComputedStyle(el);
|
const style = window.getComputedStyle(el);
|
||||||
const scrollMt = Number.parseFloat(style.scrollMarginTop);
|
const scrollMt = Number.parseFloat(style.scrollMarginTop);
|
||||||
|
|
||||||
const top = window.scrollY + el.getBoundingClientRect().top - scrollMt;
|
const top = window.scrollY + el.getBoundingClientRect().top - scrollMt;
|
||||||
return { id: section.hash, top };
|
return { id: section.id, top };
|
||||||
})
|
})
|
||||||
.filter((x): x is { id: string; top: number } => x !== null);
|
.filter((x): x is { id: string; top: number } => x !== null);
|
||||||
};
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (data.sections.length === 0) return;
|
if (tableOfContents.length === 0) return;
|
||||||
const headings = getHeadings();
|
const headings = getHeadings();
|
||||||
|
|
||||||
function onScroll() {
|
function onScroll() {
|
||||||
@ -51,10 +51,10 @@ export function TableOfContents() {
|
|||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("scroll", onScroll);
|
window.removeEventListener("scroll", onScroll);
|
||||||
};
|
};
|
||||||
}, [getHeadings, data.sections]);
|
}, [getHeadings, tableOfContents]);
|
||||||
|
|
||||||
function isActive(section: DocSection) {
|
function isActive(section: Section) {
|
||||||
if (section.hash === currentSection()) return true;
|
if (section.id === currentSection()) return true;
|
||||||
return false;
|
return false;
|
||||||
// if (!section.children) return false;
|
// if (!section.children) return false;
|
||||||
|
|
||||||
@ -72,19 +72,19 @@ export function TableOfContents() {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<ol class="mt-4 space-y-3 text-sm">
|
<ol class="mt-4 space-y-3 text-sm">
|
||||||
<For each={data.sections}>
|
<For each={tableOfContents}>
|
||||||
{(section) => (
|
{(section) => (
|
||||||
<li>
|
<li>
|
||||||
<h3>
|
<h3>
|
||||||
<Link
|
<Link
|
||||||
href={`#${section.hash}`}
|
href={`#${section.id}`}
|
||||||
class={clsx(
|
class={clsx(
|
||||||
isActive(section)
|
isActive(section)
|
||||||
? "text-violet-500"
|
? "text-violet-500"
|
||||||
: "font-normal text-slate-500 hover:text-slate-700",
|
: "font-normal text-slate-500 hover:text-slate-700",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{section.content}
|
{section.title}
|
||||||
</Link>
|
</Link>
|
||||||
</h3>
|
</h3>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import type { FlexSearchData } from "./FlexSearchService";
|
import type { FlexSearchData } from "./FlexSearchService";
|
||||||
import type { TableOfContents } from "@/remarkHeadingId";
|
|
||||||
import type { Node } from "@markdoc/markdoc";
|
import type { Node } from "@markdoc/markdoc";
|
||||||
|
|
||||||
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
||||||
@ -8,6 +7,7 @@ import { hrtime } from "node:process";
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
import { copyFile } from "node:fs";
|
||||||
|
|
||||||
const __dirname = path.resolve();
|
const __dirname = path.resolve();
|
||||||
|
|
||||||
@ -15,14 +15,12 @@ export type SectionCache = {
|
|||||||
content: string;
|
content: string;
|
||||||
frontmatter?: SectionFrontmatter;
|
frontmatter?: SectionFrontmatter;
|
||||||
markdocNode: Node;
|
markdocNode: Node;
|
||||||
sections: DocSection[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocSection = {
|
export type DocSection = {
|
||||||
content: string;
|
content: string;
|
||||||
hash?: string;
|
hash?: string;
|
||||||
subsections: string[];
|
subsections: string[];
|
||||||
level?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type SectionFrontmatter = {
|
type SectionFrontmatter = {
|
||||||
@ -101,12 +99,7 @@ class DocCache {
|
|||||||
if (node.type === "heading" && node.attributes?.level <= 2) {
|
if (node.type === "heading" && node.attributes?.level <= 2) {
|
||||||
const hash = (node.attributes?.id as string) ?? this.slugify(content);
|
const hash = (node.attributes?.id as string) ?? this.slugify(content);
|
||||||
const subsections: string[] = [];
|
const subsections: string[] = [];
|
||||||
sections.push({
|
sections.push({ content, hash, subsections });
|
||||||
content,
|
|
||||||
hash,
|
|
||||||
subsections,
|
|
||||||
level: node.attributes?.level,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
sections.at(-1)?.subsections.push(content);
|
sections.at(-1)?.subsections.push(content);
|
||||||
}
|
}
|
||||||
@ -131,26 +124,9 @@ class DocCache {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTableOfContents(markdocNode: Node) {
|
|
||||||
const sections: DocSection[] = [];
|
|
||||||
this.extractSections(markdocNode, sections);
|
|
||||||
|
|
||||||
return sections;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async cacheFile(file: string): Promise<void> {
|
private async cacheFile(file: string): Promise<void> {
|
||||||
const [content, frontmatter, markdocNode] = await this.getFileContent(file);
|
const [content, frontmatter, markdocNode] = await this.getFileContent(file);
|
||||||
const filePath = file
|
this.cache.set(file, { content, frontmatter, markdocNode });
|
||||||
.replace(/\+Page\.md(x)?$/, "")
|
|
||||||
.replace(/\/+/, "/")
|
|
||||||
.replace(/\/$/, "");
|
|
||||||
|
|
||||||
this.cache.set(filePath, {
|
|
||||||
content,
|
|
||||||
frontmatter,
|
|
||||||
markdocNode,
|
|
||||||
sections: this.getTableOfContents(markdocNode),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async populateCache(): Promise<void> {
|
private async populateCache(): Promise<void> {
|
||||||
@ -181,10 +157,6 @@ class DocCache {
|
|||||||
return DocCache.getInstance().cache;
|
return DocCache.getInstance().cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCache(): Map<string, SectionCache> {
|
|
||||||
return this.cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get(file: string): SectionCache | undefined {
|
public get(file: string): SectionCache | undefined {
|
||||||
return this.cache.get(file);
|
return this.cache.get(file);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user