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