feat: Update Highlight component props structure

This commit is contained in:
Gauthier Daniels 2025-04-20 02:36:59 +02:00
parent 9569049e61
commit 0d7afbd9cf
12 changed files with 296 additions and 14 deletions

View File

@ -5,6 +5,7 @@
"dependencies": { "dependencies": {
"@fastify/middie": "^9.0.3", "@fastify/middie": "^9.0.3",
"@fastify/static": "^8.1.1", "@fastify/static": "^8.1.1",
"@markdoc/markdoc": "^0.5.1",
"@mdx-js/rollup": "^3.1.0", "@mdx-js/rollup": "^3.1.0",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
@ -12,6 +13,7 @@
"@universal-middleware/fastify": "^0.5.16", "@universal-middleware/fastify": "^0.5.16",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"fastify": "^5.3.0", "fastify": "^5.3.0",
"flexsearch": "^0.8.158",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"reading-time-estimator": "^1.14.0", "reading-time-estimator": "^1.14.0",
@ -226,6 +228,8 @@
"@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="], "@lukeed/ms": ["@lukeed/ms@2.0.2", "", {}, "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA=="],
"@markdoc/markdoc": ["@markdoc/markdoc@0.5.1", "", { "optionalDependencies": { "@types/linkify-it": "^3.0.1", "@types/markdown-it": "12.2.3" }, "peerDependencies": { "@types/react": "*", "react": "*" }, "optionalPeers": ["@types/react", "react"] }, "sha512-W2apYOglq0hOnvWbhE70yl6V9++FG+YPFKNHmgiSjv0HTmdJaMLt+NA1LMqoH5LasSiTI7R0yVc5ofjaFh39Pg=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
"@mdx-js/rollup": ["@mdx-js/rollup@3.1.0", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@rollup/pluginutils": "^5.0.0", "source-map": "^0.7.0", "vfile": "^6.0.0" }, "peerDependencies": { "rollup": ">=2" } }, "sha512-q4xOtUXpCzeouE8GaJ8StT4rDxm/U5j6lkMHL2srb2Q3Y7cobE0aXyPzXVVlbeIMBi+5R5MpbiaVE5/vJUdnHg=="], "@mdx-js/rollup": ["@mdx-js/rollup@3.1.0", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@rollup/pluginutils": "^5.0.0", "source-map": "^0.7.0", "vfile": "^6.0.0" }, "peerDependencies": { "rollup": ">=2" } }, "sha512-q4xOtUXpCzeouE8GaJ8StT4rDxm/U5j6lkMHL2srb2Q3Y7cobE0aXyPzXVVlbeIMBi+5R5MpbiaVE5/vJUdnHg=="],
@ -340,8 +344,14 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/linkify-it": ["@types/linkify-it@3.0.5", "", {}, "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw=="],
"@types/markdown-it": ["@types/markdown-it@12.2.3", "", { "dependencies": { "@types/linkify-it": "*", "@types/mdurl": "*" } }, "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
@ -596,6 +606,8 @@
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
"flexsearch": ["flexsearch@0.8.158", "", {}, "sha512-UBOzX2rxIrhAeSSCesTI0qB2Q+75n66rofJx5ppZm5tjXV2P6BxOS3VHKsoSdJhIPg9IMzQl3qkVeSFyq3BUdw=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],

View File

@ -313,10 +313,7 @@ export type Language = (typeof Language)[keyof typeof Language];
type Props = { type Props = {
language: string; language: string;
} & ( } & ComponentProps<"code">;
| (ComponentProps<"code"> & { code?: never })
| (Omit<JSX.IntrinsicElements["code"], "children"> & { code: string })
);
export const Highlight: ParentComponent<Props> = (_props) => { export const Highlight: ParentComponent<Props> = (_props) => {
const props = mergeProps({ language: "javascript" }, _props); const props = mergeProps({ language: "javascript" }, _props);
@ -354,7 +351,7 @@ export const Highlight: ParentComponent<Props> = (_props) => {
innerHTML={highlightedCode()} innerHTML={highlightedCode()}
{...rest} {...rest}
> >
{props.code || props.children} {props.children}
</code> </code>
</pre> </pre>
); );

View File

@ -9,7 +9,7 @@ export function Image(props: ImageProps) {
<img <img
{...props} {...props}
src={props.src} src={props.src}
role={isDecorationImage ? "presentation" : "img"} role={isDecorationImage ? "presentation" : undefined}
aria-hidden={isDecorationImage ? "true" : undefined} aria-hidden={isDecorationImage ? "true" : undefined}
alt={isDecorationImage ? undefined : props.alt} alt={isDecorationImage ? undefined : props.alt}
loading="lazy" loading="lazy"

View File

@ -25,7 +25,7 @@ export function Link(props: LinkProps) {
return ( return (
<a <a
{...props} {...props}
{...(isActive && { ariaCurrent: "page" })} {...(isActive && { "aria-current": "page" })}
{...(isDownload && { download: true })} {...(isDownload && { download: true })}
{...(!isSameDomain || isDownload {...(!isSameDomain || isDownload
? { target: "_blank", rel: "noopener noreferrer" } ? { target: "_blank", rel: "noopener noreferrer" }

View File

@ -30,7 +30,7 @@ function PageLink(props: PageLinkProps) {
}); });
return ( return (
<div {...cleanProps(props, "dir", "title")}> <div {...cleanProps(props, "dir", "title", "href", "subitems")}>
<dt class="font-display text-sm font-medium text-slate-900"> <dt class="font-display text-sm font-medium text-slate-900">
{props.dir === "next" ? "Suivant" : "Précédent"} {props.dir === "next" ? "Suivant" : "Précédent"}
</dt> </dt>

View File

@ -0,0 +1,9 @@
import { buildFlexSearch } from "@/services/FlexSearchService";
import { docCache } from "@/services/DocCache";
export const onSearch = async (query: string) => {
const docs = docCache.fetchDocs();
const search = buildFlexSearch(docs);
return search(query, 5);
};

View File

@ -5,10 +5,12 @@ import { createHandler } from "@universal-middleware/fastify";
import { telefuncHandler } from "./server/telefunc-handler"; import { telefuncHandler } from "./server/telefunc-handler";
import { vikeHandler } from "./server/vike-handler"; import { vikeHandler } from "./server/vike-handler";
import { createDevMiddleware } from "vike/server"; import { createDevMiddleware } from "vike/server";
import { docCache } from "./services/DocCache";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { dirname } from "node:path"; import { dirname } from "node:path";
import { config } from "./config"; import { config } from "./config";
import Fastify from "fastify"; import Fastify from "fastify";
import { buildFlexSearch } from "./services/FlexSearchService";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
@ -32,6 +34,8 @@ declare global {
} }
async function startServer() { async function startServer() {
await docCache.waitingForCache(20000);
const app = Fastify(); const app = Fastify();
// Avoid pre-parsing body, otherwise it will cause issue with universal handlers // Avoid pre-parsing body, otherwise it will cause issue with universal handlers

View File

@ -85,10 +85,10 @@ function Footer() {
<footer class="bg-slate-50 text-slate-700"> <footer class="bg-slate-50 text-slate-700">
<div class="mx-auto w-full flex flex-col max-w-8xl sm:px-2 lg:px-8 xl:px-12 py-8"> <div class="mx-auto w-full flex flex-col max-w-8xl sm:px-2 lg:px-8 xl:px-12 py-8">
<section> <section>
<header class="flex items-center gap-2 mb-2"> <div class="flex items-center gap-2 mb-2">
<Logo class="h-8 w-auto" /> <Logo class="h-8 w-auto" />
<h2 class="font-display text-2xl">Memento Dev</h2> <strong class="font-display text-2xl">Memento Dev</strong>
</header> </div>
<p> <p>
Plateforme de ressources et documentations synthétiques et concises, Plateforme de ressources et documentations synthétiques et concises,
@ -101,10 +101,10 @@ function Footer() {
<section> <section>
<header class="flex items-center gap-2"> <header class="flex items-center gap-2">
<h2 class="font-display"> <strong class="font-display">
&copy; 2022 - {new Date().getFullYear()} Memento Dev. Tous droits &copy; 2022 - {new Date().getFullYear()} Memento Dev. Tous droits
réservés réservés
</h2> </strong>
</header> </header>
<p class="text-sm text-slate-500"> <p class="text-sm text-slate-500">

View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@fastify/middie": "^9.0.3", "@fastify/middie": "^9.0.3",
"@fastify/static": "^8.1.1", "@fastify/static": "^8.1.1",
"@markdoc/markdoc": "^0.5.1",
"@mdx-js/rollup": "^3.1.0", "@mdx-js/rollup": "^3.1.0",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
@ -17,6 +18,7 @@
"@universal-middleware/fastify": "^0.5.16", "@universal-middleware/fastify": "^0.5.16",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"fastify": "^5.3.0", "fastify": "^5.3.0",
"flexsearch": "^0.8.158",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
"reading-time-estimator": "^1.14.0", "reading-time-estimator": "^1.14.0",

View File

@ -132,7 +132,7 @@ export function HeroSection() {
</For> </For>
</div> </div>
<Highlight language={codeLanguage} code={code} /> <Highlight language={codeLanguage}>{code}</Highlight>
</div> </div>
</div> </div>
</div> </div>

185
app/services/DocCache.ts Normal file
View File

@ -0,0 +1,185 @@
import type { FlexSearchData } from "./FlexSearchService";
import type { Node } from "@markdoc/markdoc";
import { slugifyWithCounter } from "@sindresorhus/slugify";
import Markdoc from "@markdoc/markdoc";
import { hrtime } from "node:process";
import fs from "node:fs/promises";
import path from "node:path";
import yaml from "js-yaml";
import { copyFile } from "node:fs";
const __dirname = path.resolve();
export type SectionCache = {
content: string;
frontmatter?: SectionFrontmatter;
markdocNode: Node;
};
export type DocSection = {
content: string;
hash?: string;
subsections: string[];
};
type SectionFrontmatter = {
title: string;
description: string;
tags: string[];
};
class DocCache {
private static readonly pagesDir = path.join(__dirname, "pages");
private static instance: DocCache | null = null;
private cache: Map<string, SectionCache> = new Map();
private slugify = slugifyWithCounter();
private cacheReady = false;
private constructor() {
this.populateCache();
}
private async getFiles(): Promise<string[]> {
const files = await fs.readdir(DocCache.pagesDir, { recursive: true });
return files.filter(
(file) => file.endsWith(".md") || file.endsWith(".mdx"),
);
}
private removeFrontmatter(content: string): string {
const frontmatterRegex = /^---\n[\s\S]*?\n---/;
return content.replace(frontmatterRegex, "").trim();
}
private async getFileContent(
file: string,
): Promise<[string, SectionFrontmatter | undefined, Node]> {
const filePath = path.join(DocCache.pagesDir, file);
try {
const content = await fs.readFile(filePath, "utf-8");
const frontmatter = yaml.load(
content.split("---")[1] || "",
) as SectionFrontmatter;
return [
this.removeFrontmatter(content),
frontmatter,
Markdoc.parse(content),
];
} catch (error) {
console.error(`Error reading file ${filePath}:`, error);
throw error;
}
}
private nodeToString(node: Node): string {
let string = "";
if (node.type === "text" && typeof node.attributes?.content === "string") {
string = node.attributes.content;
}
if (node.children) {
for (const child of node.children) {
string += this.nodeToString(child);
}
}
return string;
}
private extractSections(node: Node, sections: DocSection[], isRoot = true) {
if (isRoot) this.slugify.reset();
if (["heading", "paragraph"].includes(node.type)) {
const content = this.nodeToString(node).trim();
if (node.type === "heading" && node.attributes?.level <= 2) {
const hash = (node.attributes?.id as string) ?? this.slugify(content);
const subsections: string[] = [];
sections.push({ content, hash, subsections });
} else {
sections.at(-1)?.subsections.push(content);
}
return;
}
if (node.children) {
for (const child of node.children) {
this.extractSections(child, sections, false);
}
}
}
public fetchDocs() {
const data = Array.from(this.cache.entries()).map(([key, section]) => {
const sections: DocSection[] = [];
this.extractSections(section.markdocNode, sections);
return { key, sections };
});
return data;
}
private async cacheFile(file: string): Promise<void> {
const [content, frontmatter, markdocNode] = await this.getFileContent(file);
this.cache.set(file, { content, frontmatter, markdocNode });
}
private async populateCache(): Promise<void> {
try {
const files = await this.getFiles();
console.log(`Found ${files.length} files to cache`);
const cachePromises = files.map(async (file) => {
return await this.cacheFile(file);
});
await Promise.all(cachePromises);
this.cacheReady = true;
} catch (error) {
console.error("Error populating cache:", error);
}
}
public static getInstance(): DocCache {
if (!DocCache.instance) {
DocCache.instance = new DocCache();
}
return DocCache.instance;
}
public static getCache(): Map<string, SectionCache> {
return DocCache.getInstance().cache;
}
public get(file: string): SectionCache | undefined {
return this.cache.get(file);
}
public waitingForCache(timeout: 20000): Promise<void> {
const timer = hrtime();
return new Promise((resolve, reject) => {
const interval = setInterval(() => {
if (this.cacheReady) {
const elapsed = hrtime(timer);
console.log(`Cache ready in ${elapsed[0]}s ${elapsed[1] / 1e6}ms`);
clearInterval(interval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(interval);
reject(new Error("Cache not ready within timeout"));
}, timeout);
});
}
}
export const docCache = DocCache.getInstance();

View File

@ -0,0 +1,73 @@
import type { DocSection } from "./DocCache";
export type FlexSearchData = { key: string; sections: DocSection[] }[];
import FlexSearch from "flexsearch";
interface NativeSearchResult {
id: string;
doc: {
title: string;
pageTitle?: string;
};
}
export type SearchResult = {
url: string;
title: string;
pageTitle?: string;
};
export function buildFlexSearch(data: FlexSearchData) {
const sectionIndex = new FlexSearch.Document({
tokenize: "full",
document: {
id: "url",
index: "content",
store: ["title", "pageTitle"],
},
context: {
resolution: 9,
depth: 2,
bidirectional: true,
},
});
for (const { key, sections } of data) {
for (const section of sections) {
const { content, hash, subsections } = section;
sectionIndex.add({
url: key + (hash ? `#${hash}` : ""),
title: content,
content: [content, ...subsections].join("\n"),
// @ts-ignore
pageTitle: hash ? sections[0]?.content : undefined,
});
}
}
return function search(
query: string,
limit?: number,
): Promise<SearchResult[]> {
const result = sectionIndex.search(query, 5, {
enrich: true,
limit,
});
// @ts-ignore
if (result.length === 0) return [];
// @ts-ignore
return result[0].result.map((rawItem) => {
const item = rawItem as unknown as NativeSearchResult;
return {
url: item.id,
title: item.doc.title,
pageTitle: item.doc.pageTitle,
};
});
};
}