rework/lightweight #12
12
app/bun.lock
12
app/bun.lock
@ -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=="],
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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" }
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
9
app/components/Search.telefunc.ts
Normal file
9
app/components/Search.telefunc.ts
Normal 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);
|
||||||
|
};
|
||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
© 2022 - {new Date().getFullYear()} Memento Dev. Tous droits
|
© 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">
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
185
app/services/DocCache.ts
Normal 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();
|
||||||
73
app/services/FlexSearchService.ts
Normal file
73
app/services/FlexSearchService.ts
Normal 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,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user