chore: Rename CSRFence to CSRSnippet and add toast in Fence
This commit is contained in:
parent
7c83d3b37e
commit
3f6d324980
@ -3,9 +3,9 @@ import { prismThemes } from "@/data/themes/prism";
|
||||
import { Highlight } from "prism-react-renderer";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { Button } from "./Button";
|
||||
import Prism from "prismjs";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export default function CSRFence({ children, language }: { children: string; language: string }) {
|
||||
const { theme } = useTheme();
|
||||
@ -16,7 +16,7 @@ export default function CSRFence({ children, language }: { children: string; lan
|
||||
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(children.trimEnd());
|
||||
toast.success("Code copied to clipboard!");
|
||||
toast.success("Code copié dans le presse-papier");
|
||||
};
|
||||
|
||||
return (
|
||||
@ -39,6 +39,7 @@ export default function CSRFence({ children, language }: { children: string; lan
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
|
||||
<Button
|
||||
className="absolute top-2 right-2 w-8 h-8 aspect-square opacity-0 group-hover:opacity-50 hover:opacity-100 transition-opacity"
|
||||
size="sm"
|
||||
@ -5,7 +5,7 @@ import { Fragment, useMemo } from "react";
|
||||
|
||||
import { clientOnly } from "vike-react/clientOnly";
|
||||
|
||||
const CSRFence = clientOnly(() => import("./CSRFence"));
|
||||
const CSRSnippet = clientOnly(() => import("./CSRSnippet"));
|
||||
|
||||
function SSRFence({ children, language }: { children: string; language: string }) {
|
||||
const { theme } = useTheme();
|
||||
@ -38,9 +38,9 @@ function SSRFence({ children, language }: { children: string; language: string }
|
||||
export function Fence({ children, language }: { children: string; language: string }) {
|
||||
return (
|
||||
<div className="relative group">
|
||||
<CSRFence language={language} fallback={<SSRFence language={language} children={children} />}>
|
||||
<CSRSnippet language={language} fallback={<SSRFence language={language} children={children} />}>
|
||||
{children}
|
||||
</CSRFence>
|
||||
</CSRSnippet>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
58
app/components/syntax/Snippet.tsx
Normal file
58
app/components/syntax/Snippet.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import type { Data } from "@/pages/docs/+data";
|
||||
|
||||
import { snippetsService } from "@/services/SnippetsService";
|
||||
import { Highlight, Prism } from "prism-react-renderer";
|
||||
import { clientOnly } from "vike-react/clientOnly";
|
||||
import { prismThemes } from "@/data/themes/prism";
|
||||
import { useData } from "vike-react/useData";
|
||||
import { useTheme } from "@/hooks/useTheme";
|
||||
import { Fragment, useMemo } from "react";
|
||||
|
||||
const CSRSnippet = clientOnly(() => import("./CSRSnippet"));
|
||||
|
||||
function SSRSnippet({ language, children }: { language: string; children: string }) {
|
||||
const { theme } = useTheme();
|
||||
const prismTheme = useMemo(() => {
|
||||
return prismThemes[theme];
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<Highlight code={children.trimEnd()} language={language} theme={prismTheme} prism={Prism}>
|
||||
{({ className, style, tokens, getTokenProps }) => (
|
||||
<pre className={className} style={style}>
|
||||
<code>
|
||||
{tokens.map((line, lineIndex) => (
|
||||
<Fragment key={lineIndex}>
|
||||
{line
|
||||
.filter((token) => !token.empty)
|
||||
.map((token, tokenIndex) => (
|
||||
<span key={tokenIndex} {...getTokenProps({ token })} />
|
||||
))}
|
||||
{"\n"}
|
||||
</Fragment>
|
||||
))}
|
||||
</code>
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
);
|
||||
}
|
||||
|
||||
export function Snippet({ path, language, label }: { path: string; language: string; label?: string }) {
|
||||
const { snippets } = useData<Data>();
|
||||
|
||||
const snippet = snippets.find((snippet) => snippet.path === path);
|
||||
if (!snippet || !snippet.content) return null;
|
||||
|
||||
const props = {
|
||||
language,
|
||||
label,
|
||||
children: snippet.content,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative group">
|
||||
<CSRSnippet {...props} fallback={<SSRSnippet {...props} />} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -50,18 +50,7 @@ Parlons dans un premier temps de la signature d'un reducer :
|
||||
|
||||
{% tab value="jsx" label="JSX" %}
|
||||
|
||||
```jsx
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case "TYPE_1":
|
||||
return { ...state /* Nouvel état */ };
|
||||
case "TYPE_2":
|
||||
return { ...state /* Nouvel état */ };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
```
|
||||
{% snippet path="data/docs/react/usereducer/reducer-example.jsx" language="jsx" label="test" /%}
|
||||
|
||||
{% /tab %}
|
||||
|
||||
|
||||
10
app/data/docs/react/usereducer/reducer-example.jsx
Normal file
10
app/data/docs/react/usereducer/reducer-example.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case "TYPE_1":
|
||||
return { ...state /* Nouvel état */ };
|
||||
case "TYPE_2":
|
||||
return { ...state /* Nouvel état */ };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@ -5,10 +5,6 @@ import { fileURLToPath } from "node:url";
|
||||
import { dirname } from "node:path";
|
||||
import Fastify from "fastify";
|
||||
|
||||
import { Prism } from "prism-react-renderer";
|
||||
(typeof global !== "undefined" ? global : window).Prism = Prism;
|
||||
require("prismjs/components/prism-bash");
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import "./style.css";
|
||||
import "./tailwind.css";
|
||||
import "./prism.css";
|
||||
import "unfonts.css";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
@ -93,6 +94,7 @@ export default function DefaultLayout({ children }: { children: React.ReactNode
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { QuickLink, QuickLinks } from "@syntax/QuickLinks";
|
||||
import { TabContent, Tabs } from "@/components/md/Tabs";
|
||||
// import { Fence2 } from "@/components/syntax/Fence2";
|
||||
import { Callout } from "@syntax/Callout";
|
||||
// import fs from "fs/promises";
|
||||
// import { Tag } from "./Tag";
|
||||
import React from "react";
|
||||
import { Snippet } from "@/components/syntax/Snippet";
|
||||
// import path from "path";
|
||||
|
||||
// const __dirname = path.resolve();
|
||||
|
||||
const tags = {
|
||||
callout: {
|
||||
@ -67,6 +74,53 @@ const tags = {
|
||||
value: { type: String },
|
||||
},
|
||||
},
|
||||
snippet: {
|
||||
render: Snippet,
|
||||
attributes: {
|
||||
language: {
|
||||
type: String,
|
||||
default: "auto",
|
||||
},
|
||||
label: { type: String },
|
||||
path: { type: String },
|
||||
},
|
||||
},
|
||||
// snippet: {
|
||||
// // render: Fence2,
|
||||
// attributes: {
|
||||
// language: {
|
||||
// type: String,
|
||||
// default: "auto",
|
||||
// },
|
||||
// label: { type: String },
|
||||
// description: { type: String },
|
||||
// path: { type: String },
|
||||
// },
|
||||
// async transform(node: any, config: any) {
|
||||
// const attributes = node.transformAttributes(config);
|
||||
|
||||
// const pathValue = attributes.path;
|
||||
|
||||
// let language = attributes.language ?? "auto";
|
||||
// let content = "";
|
||||
|
||||
// if (!pathValue) {
|
||||
// console.warn("No path provided for snippet tag");
|
||||
// } else {
|
||||
// const absolutePath = path.resolve(__dirname, pathValue);
|
||||
// // Read the file content
|
||||
// try {
|
||||
// content = await fs.readFile(absolutePath, "utf-8");
|
||||
// } catch (error) {
|
||||
// console.error("Error reading file:", error);
|
||||
// content = `Error reading file: ${absolutePath}`;
|
||||
// language = "plain";
|
||||
// }
|
||||
|
||||
// // return new Tag("fence2", { ...attributes, language, content, label: "Temp" });
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
};
|
||||
|
||||
export default tags;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { PageContext } from "vike/types";
|
||||
|
||||
import { snippetsService } from "@/services/SnippetsService";
|
||||
import { docsService } from "@/services/DocsService";
|
||||
import { readingTime } from "reading-time-estimator";
|
||||
import { useConfig } from "vike-react/useConfig";
|
||||
@ -28,5 +29,10 @@ export async function data(pageContext: PageContext) {
|
||||
|
||||
docsService.transform(doc);
|
||||
|
||||
return { doc, estimatedReadingTime: readingTimeObject.text };
|
||||
const snippets = Array.from(doc.snippets).map((snippetPath) => ({
|
||||
path: snippetPath,
|
||||
content: snippetsService.getFromCache(snippetPath),
|
||||
}));
|
||||
|
||||
return { doc, estimatedReadingTime: readingTimeObject.text, snippets };
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Node } from "@markdoc/markdoc";
|
||||
|
||||
import { snippetsService } from "@/services/SnippetsService";
|
||||
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
||||
import { buildFlexSearch } from "./FlexSearchService";
|
||||
import Markdoc from "@markdoc/markdoc";
|
||||
@ -9,19 +10,22 @@ import glob from "fast-glob";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
const __dirname = path.resolve();
|
||||
|
||||
export type FlexSearchData = { key: string; sections: DocSection[] }[];
|
||||
|
||||
type DocsCache = Map<string, DocData>;
|
||||
type DocSection = [string, string | null, string[]];
|
||||
type DocData = { title: string; description: string; content: string; sections: DocSection[] };
|
||||
type DocData = { title: string; description: string; content: string; sections: DocSection[]; snippets: string[] };
|
||||
type DocExtension = "mdx" | "md";
|
||||
|
||||
class DocsService {
|
||||
private static readonly DOCS_PATH = path.resolve("../../app/data");
|
||||
private static readonly DOCS_PATH = path.resolve(path.join(__dirname, "data"));
|
||||
private static readonly DOCS_EXTS: DocExtension[] = ["mdx", "md"]; // Order matters
|
||||
private static instance: DocsService;
|
||||
|
||||
public search: ReturnType<typeof buildFlexSearch> = buildFlexSearch([]);
|
||||
|
||||
private slugify = slugifyWithCounter();
|
||||
private cache: DocsCache = new Map();
|
||||
|
||||
@ -78,27 +82,46 @@ class DocsService {
|
||||
}
|
||||
}
|
||||
|
||||
public fetchSnippets(content: string) {
|
||||
const identifierResults = snippetsService.identifyNewSnippets(content);
|
||||
if (!identifierResults) return;
|
||||
|
||||
const [snippetsToFetch, allSnippets] = identifierResults;
|
||||
|
||||
for (const snippet of snippetsToFetch) {
|
||||
const absolutePath = path.resolve(__dirname, snippet);
|
||||
const content = fs.readFileSync(absolutePath, "utf-8");
|
||||
snippetsService.setToCache(snippet, content);
|
||||
}
|
||||
|
||||
return allSnippets;
|
||||
}
|
||||
|
||||
public async fetchDocs() {
|
||||
const docs = glob.sync(DocsService.DOCS_PATH + `/**/*.{${DocsService.DOCS_EXTS.join(",")}}`);
|
||||
const data = docs.map((doc) => {
|
||||
const content = fs.readFileSync(doc, "utf-8");
|
||||
const extension = path.extname(doc).slice(1) as DocExtension;
|
||||
const key = doc
|
||||
.replace(DocsService.DOCS_PATH, "")
|
||||
.replace(`page.${extension}`, "")
|
||||
.replace(`.${extension}`, "")
|
||||
.replace(/\/$/g, "");
|
||||
const data = await Promise.all(
|
||||
docs.map((doc) => {
|
||||
const content = fs.readFileSync(doc, "utf-8");
|
||||
const extension = path.extname(doc).slice(1) as DocExtension;
|
||||
const key = doc
|
||||
.replace(DocsService.DOCS_PATH, "")
|
||||
.replace(`page.${extension}`, "")
|
||||
.replace(`.${extension}`, "")
|
||||
.replace(/\/$/g, "");
|
||||
|
||||
const ast = Markdoc.parse(content);
|
||||
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
|
||||
const description = ast.attributes?.frontmatter?.match(/^description:\s*(.*?)\s*$/m)?.[1]?.replaceAll('"', "");
|
||||
const sections: DocSection[] = [[title, null, []]];
|
||||
const allSnippets = this.fetchSnippets(content);
|
||||
|
||||
this.extractSections(ast, sections);
|
||||
this.setToCache(key, { title, description, content, sections });
|
||||
const ast = Markdoc.parse(content);
|
||||
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
|
||||
const description = ast.attributes?.frontmatter?.match(/^description:\s*(.*?)\s*$/m)?.[1]?.replaceAll('"', "");
|
||||
const sections: DocSection[] = [[title, null, []]];
|
||||
|
||||
return { key, sections };
|
||||
});
|
||||
this.extractSections(ast, sections);
|
||||
this.setToCache(key, { title, description, content, sections, snippets: allSnippets || [] });
|
||||
|
||||
return { key, sections };
|
||||
}),
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
61
app/services/SnippetsService.ts
Normal file
61
app/services/SnippetsService.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import path from "path";
|
||||
|
||||
type SnippetsCache = Map<string, string>;
|
||||
|
||||
class SnippetsService {
|
||||
private static instance: SnippetsService;
|
||||
|
||||
private cache: SnippetsCache = new Map();
|
||||
|
||||
public static getInstance(): SnippetsService {
|
||||
if (!SnippetsService.instance) {
|
||||
SnippetsService.instance = new SnippetsService();
|
||||
}
|
||||
|
||||
return SnippetsService.instance;
|
||||
}
|
||||
|
||||
public getFromCache(key: string): string | undefined {
|
||||
return this.cache.get(key);
|
||||
}
|
||||
|
||||
private doesCacheHas(key: string): boolean {
|
||||
return this.cache.has(key);
|
||||
}
|
||||
|
||||
public setToCache(key: string, value: string): void {
|
||||
this.cache.set(key, value);
|
||||
}
|
||||
|
||||
public showCache(): SnippetsCache {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
public identifyNewSnippets(content: string): [string[], string[]] | undefined {
|
||||
const regex = /{%\s?snippet.*(path="\S*").*\/%}/g;
|
||||
const snippets = content.match(regex);
|
||||
if (!snippets) return;
|
||||
|
||||
const snippetsPath = snippets
|
||||
.map((snippet) => {
|
||||
const pathMatch = snippet.match(/path="([^"]+)"/);
|
||||
|
||||
const path = pathMatch?.[1];
|
||||
if (!path) return null;
|
||||
|
||||
return path;
|
||||
})
|
||||
.filter((path) => path !== null) as string[];
|
||||
|
||||
const snippetsPathSet = new Set(snippetsPath);
|
||||
|
||||
return [
|
||||
Array.from(snippetsPathSet).filter((snippetPath) => {
|
||||
return this.doesCacheHas(snippetPath) === false;
|
||||
}),
|
||||
Array.from(snippetsPathSet),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const snippetsService = SnippetsService.getInstance();
|
||||
Loading…
Reference in New Issue
Block a user