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 { Highlight } from "prism-react-renderer";
|
||||||
import { useTheme } from "@/hooks/useTheme";
|
import { useTheme } from "@/hooks/useTheme";
|
||||||
import { Fragment, useMemo } from "react";
|
import { Fragment, useMemo } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import { Button } from "./Button";
|
import { Button } from "./Button";
|
||||||
import Prism from "prismjs";
|
import Prism from "prismjs";
|
||||||
import { toast } from "react-toastify";
|
|
||||||
|
|
||||||
export default function CSRFence({ children, language }: { children: string; language: string }) {
|
export default function CSRFence({ children, language }: { children: string; language: string }) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@ -16,7 +16,7 @@ export default function CSRFence({ children, language }: { children: string; lan
|
|||||||
|
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
navigator.clipboard.writeText(children.trimEnd());
|
navigator.clipboard.writeText(children.trimEnd());
|
||||||
toast.success("Code copied to clipboard!");
|
toast.success("Code copié dans le presse-papier");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -39,6 +39,7 @@ export default function CSRFence({ children, language }: { children: string; lan
|
|||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="absolute top-2 right-2 w-8 h-8 aspect-square opacity-0 group-hover:opacity-50 hover:opacity-100 transition-opacity"
|
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"
|
size="sm"
|
||||||
@ -5,7 +5,7 @@ import { Fragment, useMemo } from "react";
|
|||||||
|
|
||||||
import { clientOnly } from "vike-react/clientOnly";
|
import { clientOnly } from "vike-react/clientOnly";
|
||||||
|
|
||||||
const CSRFence = clientOnly(() => import("./CSRFence"));
|
const CSRSnippet = clientOnly(() => import("./CSRSnippet"));
|
||||||
|
|
||||||
function SSRFence({ children, language }: { children: string; language: string }) {
|
function SSRFence({ children, language }: { children: string; language: string }) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@ -38,9 +38,9 @@ function SSRFence({ children, language }: { children: string; language: string }
|
|||||||
export function Fence({ children, language }: { children: string; language: string }) {
|
export function Fence({ children, language }: { children: string; language: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<CSRFence language={language} fallback={<SSRFence language={language} children={children} />}>
|
<CSRSnippet language={language} fallback={<SSRFence language={language} children={children} />}>
|
||||||
{children}
|
{children}
|
||||||
</CSRFence>
|
</CSRSnippet>
|
||||||
</div>
|
</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" %}
|
{% tab value="jsx" label="JSX" %}
|
||||||
|
|
||||||
```jsx
|
{% snippet path="data/docs/react/usereducer/reducer-example.jsx" language="jsx" label="test" /%}
|
||||||
const reducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case "TYPE_1":
|
|
||||||
return { ...state /* Nouvel état */ };
|
|
||||||
case "TYPE_2":
|
|
||||||
return { ...state /* Nouvel état */ };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
{% /tab %}
|
{% /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 { dirname } from "node:path";
|
||||||
import Fastify from "fastify";
|
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 __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import "./style.css";
|
|||||||
import "./tailwind.css";
|
import "./tailwind.css";
|
||||||
import "./prism.css";
|
import "./prism.css";
|
||||||
import "unfonts.css";
|
import "unfonts.css";
|
||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||||
return (
|
return (
|
||||||
@ -93,6 +94,7 @@ export default function DefaultLayout({ children }: { children: React.ReactNode
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ToastContainer />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import { QuickLink, QuickLinks } from "@syntax/QuickLinks";
|
import { QuickLink, QuickLinks } from "@syntax/QuickLinks";
|
||||||
import { TabContent, Tabs } from "@/components/md/Tabs";
|
import { TabContent, Tabs } from "@/components/md/Tabs";
|
||||||
|
// import { Fence2 } from "@/components/syntax/Fence2";
|
||||||
import { Callout } from "@syntax/Callout";
|
import { Callout } from "@syntax/Callout";
|
||||||
|
// import fs from "fs/promises";
|
||||||
|
// import { Tag } from "./Tag";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Snippet } from "@/components/syntax/Snippet";
|
||||||
|
// import path from "path";
|
||||||
|
|
||||||
|
// const __dirname = path.resolve();
|
||||||
|
|
||||||
const tags = {
|
const tags = {
|
||||||
callout: {
|
callout: {
|
||||||
@ -67,6 +74,53 @@ const tags = {
|
|||||||
value: { type: String },
|
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;
|
export default tags;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { PageContext } from "vike/types";
|
import type { PageContext } from "vike/types";
|
||||||
|
|
||||||
|
import { snippetsService } from "@/services/SnippetsService";
|
||||||
import { docsService } from "@/services/DocsService";
|
import { docsService } from "@/services/DocsService";
|
||||||
import { readingTime } from "reading-time-estimator";
|
import { readingTime } from "reading-time-estimator";
|
||||||
import { useConfig } from "vike-react/useConfig";
|
import { useConfig } from "vike-react/useConfig";
|
||||||
@ -28,5 +29,10 @@ export async function data(pageContext: PageContext) {
|
|||||||
|
|
||||||
docsService.transform(doc);
|
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 type { Node } from "@markdoc/markdoc";
|
||||||
|
|
||||||
|
import { snippetsService } from "@/services/SnippetsService";
|
||||||
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
||||||
import { buildFlexSearch } from "./FlexSearchService";
|
import { buildFlexSearch } from "./FlexSearchService";
|
||||||
import Markdoc from "@markdoc/markdoc";
|
import Markdoc from "@markdoc/markdoc";
|
||||||
@ -9,19 +10,22 @@ import glob from "fast-glob";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
|
const __dirname = path.resolve();
|
||||||
|
|
||||||
export type FlexSearchData = { key: string; sections: DocSection[] }[];
|
export type FlexSearchData = { key: string; sections: DocSection[] }[];
|
||||||
|
|
||||||
type DocsCache = Map<string, DocData>;
|
type DocsCache = Map<string, DocData>;
|
||||||
type DocSection = [string, string | null, string[]];
|
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";
|
type DocExtension = "mdx" | "md";
|
||||||
|
|
||||||
class DocsService {
|
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 readonly DOCS_EXTS: DocExtension[] = ["mdx", "md"]; // Order matters
|
||||||
private static instance: DocsService;
|
private static instance: DocsService;
|
||||||
|
|
||||||
public search: ReturnType<typeof buildFlexSearch> = buildFlexSearch([]);
|
public search: ReturnType<typeof buildFlexSearch> = buildFlexSearch([]);
|
||||||
|
|
||||||
private slugify = slugifyWithCounter();
|
private slugify = slugifyWithCounter();
|
||||||
private cache: DocsCache = new Map();
|
private cache: DocsCache = new Map();
|
||||||
|
|
||||||
@ -78,9 +82,25 @@ 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() {
|
public async fetchDocs() {
|
||||||
const docs = glob.sync(DocsService.DOCS_PATH + `/**/*.{${DocsService.DOCS_EXTS.join(",")}}`);
|
const docs = glob.sync(DocsService.DOCS_PATH + `/**/*.{${DocsService.DOCS_EXTS.join(",")}}`);
|
||||||
const data = docs.map((doc) => {
|
const data = await Promise.all(
|
||||||
|
docs.map((doc) => {
|
||||||
const content = fs.readFileSync(doc, "utf-8");
|
const content = fs.readFileSync(doc, "utf-8");
|
||||||
const extension = path.extname(doc).slice(1) as DocExtension;
|
const extension = path.extname(doc).slice(1) as DocExtension;
|
||||||
const key = doc
|
const key = doc
|
||||||
@ -89,16 +109,19 @@ class DocsService {
|
|||||||
.replace(`.${extension}`, "")
|
.replace(`.${extension}`, "")
|
||||||
.replace(/\/$/g, "");
|
.replace(/\/$/g, "");
|
||||||
|
|
||||||
|
const allSnippets = this.fetchSnippets(content);
|
||||||
|
|
||||||
const ast = Markdoc.parse(content);
|
const ast = Markdoc.parse(content);
|
||||||
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
|
const title = ast.attributes?.frontmatter?.match(/^title:\s*(.*?)\s*$/m)?.[1];
|
||||||
const description = ast.attributes?.frontmatter?.match(/^description:\s*(.*?)\s*$/m)?.[1]?.replaceAll('"', "");
|
const description = ast.attributes?.frontmatter?.match(/^description:\s*(.*?)\s*$/m)?.[1]?.replaceAll('"', "");
|
||||||
const sections: DocSection[] = [[title, null, []]];
|
const sections: DocSection[] = [[title, null, []]];
|
||||||
|
|
||||||
this.extractSections(ast, sections);
|
this.extractSections(ast, sections);
|
||||||
this.setToCache(key, { title, description, content, sections });
|
this.setToCache(key, { title, description, content, sections, snippets: allSnippets || [] });
|
||||||
|
|
||||||
return { key, sections };
|
return { key, sections };
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return data;
|
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