From ebab22b2634ffaa2afcde646d3a047f98c9b24eb Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 2 Mar 2025 16:53:42 -0600 Subject: [PATCH] refactor: migrate code block rendering to Shiki with Prettier formatting --- .../website/app/[locale]/blog/[slug]/page.tsx | 60 +++++++-- .../[locale]/blog/components/CodeBlock.tsx | 119 ------------------ apps/website/package.json | 1 + pnpm-lock.yaml | 3 + 4 files changed, 56 insertions(+), 127 deletions(-) delete mode 100644 apps/website/app/[locale]/blog/components/CodeBlock.tsx diff --git a/apps/website/app/[locale]/blog/[slug]/page.tsx b/apps/website/app/[locale]/blog/[slug]/page.tsx index e20aec0..1acfb44 100644 --- a/apps/website/app/[locale]/blog/[slug]/page.tsx +++ b/apps/website/app/[locale]/blog/[slug]/page.tsx @@ -1,20 +1,20 @@ import { getPost, getPosts } from "@/lib/ghost"; -import type { Post } from "@/lib/ghost"; import type { Metadata, ResolvingMetadata } from "next"; import { getTranslations } from "next-intl/server"; import Image from "next/image"; import Link from "next/link"; import { notFound } from "next/navigation"; +import * as prettier from "prettier"; import type { DetailedHTMLProps, HTMLAttributes } from "react"; import type React from "react"; import ReactMarkdown from "react-markdown"; import type { Components } from "react-markdown"; import rehypeRaw from "rehype-raw"; import remarkGfm from "remark-gfm"; +import { codeToHtml } from "shiki"; +import type { BundledLanguage } from "shiki/bundle/web"; import TurndownService from "turndown"; -import { CodeBlock } from "../components/CodeBlock"; import { ZoomableImage } from "./components/ZoomableImage"; - type Props = { params: { locale: string; slug: string }; }; @@ -63,7 +63,53 @@ interface CodeProps className?: string; 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 ( +
+ ); +} export default async function BlogPostPage({ params }: Props) { const { locale, slug } = params; const t = await getTranslations({ locale, namespace: "blog" }); @@ -163,11 +209,9 @@ export default async function BlogPostPage({ params }: Props) { const match = /language-(\w+)/.exec(className || ""); return ( - + + {children?.toString() || ""} + ); }, }; diff --git a/apps/website/app/[locale]/blog/components/CodeBlock.tsx b/apps/website/app/[locale]/blog/components/CodeBlock.tsx deleted file mode 100644 index 76d1b6b..0000000 --- a/apps/website/app/[locale]/blog/components/CodeBlock.tsx +++ /dev/null @@ -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 ( - - {({ - className: preClassName, - style, - tokens, - getLineProps, - getTokenProps, - }) => ( -
-
- -
- {tokens.map((line, i) => ( -
- - {i + 1} - - - {line.map((token, key) => ( - - ))} - -
- ))} -
- )} -
- ); -} diff --git a/apps/website/package.json b/apps/website/package.json index d7f0873..dc118f1 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -38,6 +38,7 @@ "react-dom": "18.2.0", "react-ga4": "^2.1.0", "react-photo-view": "^1.2.7", + "shiki": "1.22.2", "tailwind-merge": "^2.2.2", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b06e7dc..d771ab9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,6 +182,9 @@ importers: react-photo-view: specifier: ^1.2.7 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: specifier: ^2.2.2 version: 2.4.0