mirror of
https://github.com/Dokploy/website
synced 2025-06-26 18:16:01 +00:00
refactor: migrate code block rendering to Shiki with Prettier formatting
This commit is contained in:
@@ -1,20 +1,20 @@
|
|||||||
import { getPost, getPosts } from "@/lib/ghost";
|
import { getPost, getPosts } from "@/lib/ghost";
|
||||||
import type { Post } from "@/lib/ghost";
|
|
||||||
import type { Metadata, ResolvingMetadata } from "next";
|
import type { Metadata, ResolvingMetadata } from "next";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import * as prettier from "prettier";
|
||||||
import type { DetailedHTMLProps, HTMLAttributes } from "react";
|
import type { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import type { Components } from "react-markdown";
|
import type { Components } from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
import { codeToHtml } from "shiki";
|
||||||
|
import type { BundledLanguage } from "shiki/bundle/web";
|
||||||
import TurndownService from "turndown";
|
import TurndownService from "turndown";
|
||||||
import { CodeBlock } from "../components/CodeBlock";
|
|
||||||
import { ZoomableImage } from "./components/ZoomableImage";
|
import { ZoomableImage } from "./components/ZoomableImage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: { locale: string; slug: string };
|
params: { locale: string; slug: string };
|
||||||
};
|
};
|
||||||
@@ -63,7 +63,53 @@ interface CodeProps
|
|||||||
className?: string;
|
className?: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
interface LanguageProps {
|
||||||
|
children: string;
|
||||||
|
lang: BundledLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getParserForLanguage = (language: string): string => {
|
||||||
|
const languageMap: { [key: string]: string } = {
|
||||||
|
js: "babel",
|
||||||
|
jsx: "babel",
|
||||||
|
ts: "typescript",
|
||||||
|
tsx: "typescript",
|
||||||
|
json: "json",
|
||||||
|
css: "css",
|
||||||
|
scss: "scss",
|
||||||
|
less: "less",
|
||||||
|
html: "html",
|
||||||
|
xml: "xml",
|
||||||
|
markdown: "markdown",
|
||||||
|
md: "markdown",
|
||||||
|
yaml: "yaml",
|
||||||
|
yml: "yaml",
|
||||||
|
};
|
||||||
|
|
||||||
|
return languageMap[language.toLowerCase()] || "babel";
|
||||||
|
};
|
||||||
|
|
||||||
|
async function CodeBlock(props: LanguageProps) {
|
||||||
|
const format = await prettier.format(props.children, {
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
tabWidth: 2,
|
||||||
|
useTabs: false,
|
||||||
|
printWidth: 120,
|
||||||
|
parser: getParserForLanguage(props.lang),
|
||||||
|
});
|
||||||
|
const out = await codeToHtml(format, {
|
||||||
|
lang: props.lang,
|
||||||
|
theme: "houston",
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{ __html: out }}
|
||||||
|
className="text-sm p-4 rounded-lg bg-[#18191F]"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
export default async function BlogPostPage({ params }: Props) {
|
export default async function BlogPostPage({ params }: Props) {
|
||||||
const { locale, slug } = params;
|
const { locale, slug } = params;
|
||||||
const t = await getTranslations({ locale, namespace: "blog" });
|
const t = await getTranslations({ locale, namespace: "blog" });
|
||||||
@@ -163,11 +209,9 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
const match = /language-(\w+)/.exec(className || "");
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CodeBlock
|
<CodeBlock lang={match ? (match[1] as BundledLanguage) : "ts"}>
|
||||||
code={children?.toString() || ""}
|
{children?.toString() || ""}
|
||||||
language={match ? match[1] : "text"}
|
</CodeBlock>
|
||||||
className="my-6"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import * as prettier from "prettier";
|
|
||||||
import * as prettierPluginBabel from "prettier/plugins/babel";
|
|
||||||
import * as prettierPluginEstree from "prettier/plugins/estree";
|
|
||||||
import { Highlight, themes } from "prism-react-renderer";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
interface CodeBlockProps {
|
|
||||||
code: string;
|
|
||||||
language: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getParserForLanguage = (language: string): string => {
|
|
||||||
const languageMap: { [key: string]: string } = {
|
|
||||||
js: "babel",
|
|
||||||
jsx: "babel",
|
|
||||||
ts: "typescript",
|
|
||||||
tsx: "typescript",
|
|
||||||
json: "json",
|
|
||||||
css: "css",
|
|
||||||
scss: "scss",
|
|
||||||
less: "less",
|
|
||||||
html: "html",
|
|
||||||
xml: "xml",
|
|
||||||
markdown: "markdown",
|
|
||||||
md: "markdown",
|
|
||||||
yaml: "yaml",
|
|
||||||
yml: "yaml",
|
|
||||||
};
|
|
||||||
|
|
||||||
return languageMap[language.toLowerCase()] || "babel";
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CodeBlock({ code, language, className = "" }: CodeBlockProps) {
|
|
||||||
const [formattedCode, setFormattedCode] = useState(code);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const formatCode = async () => {
|
|
||||||
try {
|
|
||||||
const parser = getParserForLanguage(language);
|
|
||||||
|
|
||||||
// Eliminar espacios en blanco al inicio y final, pero mantener la indentación interna
|
|
||||||
|
|
||||||
const formatted = await prettier.format(formattedCode, {
|
|
||||||
semi: true,
|
|
||||||
singleQuote: false,
|
|
||||||
tabWidth: 2,
|
|
||||||
useTabs: false,
|
|
||||||
printWidth: 80,
|
|
||||||
parser,
|
|
||||||
plugins: [prettierPluginBabel, prettierPluginEstree],
|
|
||||||
});
|
|
||||||
|
|
||||||
setFormattedCode(formatted);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Error formatting code:", error);
|
|
||||||
// Si falla el formateo, al menos limpiamos los espacios en blanco extra
|
|
||||||
const cleanCode = code.replace(/^\s+|\s+$/g, "");
|
|
||||||
setFormattedCode(cleanCode);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
formatCode();
|
|
||||||
}, [code, language]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Highlight
|
|
||||||
theme={themes.dracula}
|
|
||||||
code={formattedCode}
|
|
||||||
language={language.toLowerCase()}
|
|
||||||
>
|
|
||||||
{({
|
|
||||||
className: preClassName,
|
|
||||||
style,
|
|
||||||
tokens,
|
|
||||||
getLineProps,
|
|
||||||
getTokenProps,
|
|
||||||
}) => (
|
|
||||||
<div
|
|
||||||
className={`${preClassName} ${className} overflow-x-auto p-4 rounded-lg text-[14px] leading-[1.5] font-mono relative group`}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
navigator.clipboard.writeText(formattedCode);
|
|
||||||
}}
|
|
||||||
className="px-2 py-1 text-xs rounded bg-primary/10 text-primary hover:bg-primary/20 transition-colors"
|
|
||||||
>
|
|
||||||
Copy
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{tokens.map((line, i) => (
|
|
||||||
<div key={i} {...getLineProps({ line })} className="table-row">
|
|
||||||
<span
|
|
||||||
className="table-cell text-right pr-4 select-none w-[2.5em] text-zinc-500 text-xs"
|
|
||||||
style={{
|
|
||||||
color: "rgb(98, 114, 164)", // Dracula comment color
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{i + 1}
|
|
||||||
</span>
|
|
||||||
<span className="table-cell">
|
|
||||||
{line.map((token, key) => (
|
|
||||||
<span key={key} {...getTokenProps({ token })} />
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Highlight>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-ga4": "^2.1.0",
|
"react-ga4": "^2.1.0",
|
||||||
"react-photo-view": "^1.2.7",
|
"react-photo-view": "^1.2.7",
|
||||||
|
"shiki": "1.22.2",
|
||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -182,6 +182,9 @@ importers:
|
|||||||
react-photo-view:
|
react-photo-view:
|
||||||
specifier: ^1.2.7
|
specifier: ^1.2.7
|
||||||
version: 1.2.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.2.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
shiki:
|
||||||
|
specifier: 1.22.2
|
||||||
|
version: 1.22.2
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^2.2.2
|
specifier: ^2.2.2
|
||||||
version: 2.4.0
|
version: 2.4.0
|
||||||
|
|||||||
Reference in New Issue
Block a user