From 3dd7ff1ee8a45112454ca3c08f4faccfef0f9ca6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 2 Mar 2025 20:28:42 -0600 Subject: [PATCH] feat: enhance code block rendering with dynamic formatting and highlighting --- .../blog/[slug]/components/CodeBlock.tsx | 116 +++++++++++------- .../[locale]/blog/[slug]/components/shared.ts | 19 +++ .../website/app/[locale]/blog/[slug]/page.tsx | 7 +- apps/website/package.json | 3 +- pnpm-lock.yaml | 30 ++++- 5 files changed, 126 insertions(+), 49 deletions(-) create mode 100644 apps/website/app/[locale]/blog/[slug]/components/shared.ts diff --git a/apps/website/app/[locale]/blog/[slug]/components/CodeBlock.tsx b/apps/website/app/[locale]/blog/[slug]/components/CodeBlock.tsx index 960dad8..4db0fb0 100644 --- a/apps/website/app/[locale]/blog/[slug]/components/CodeBlock.tsx +++ b/apps/website/app/[locale]/blog/[slug]/components/CodeBlock.tsx @@ -1,55 +1,85 @@ -import { CopyButton } from "@/components/ui/copy-button"; -import prettier from "prettier"; -import { codeToHtml } from "shiki"; -import type { BundledLanguage } from "shiki/bundle/web"; +"use client"; -interface LanguageProps { - children: string; +import { CopyButton } from "@/components/ui/copy-button"; +import * as babel from "prettier/plugins/babel"; +import * as estree from "prettier/plugins/estree"; +import * as prettier from "prettier/standalone"; +import { type JSX, useLayoutEffect, useState } from "react"; +import type { BundledLanguage } from "shiki/bundle/web"; +import { highlight } from "./shared"; + +interface CodeBlockProps { + code: string; lang: BundledLanguage; + initial?: JSX.Element; } -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", - }; +async function formatCode(code: string, lang: string) { + try { + // Configuración básica para JavaScript/TypeScript + const plugins = [babel, estree]; + console.log("Formatting with plugins:", plugins); - return languageMap[language.toLowerCase()] || "babel"; -}; + const formatted = await prettier.format(code, { + parser: "babel-ts", + plugins, + semi: true, + singleQuote: true, + tabWidth: 2, + useTabs: false, + printWidth: 120, + }); -export 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", - }); + console.log("Formatted code:", formatted); + return formatted; + } catch (error) { + console.error("Error formatting code:", error); + return code; // Retorna el código original si hay error + } +} + +export function CodeBlock({ code, lang, initial }: CodeBlockProps) { + const [nodes, setNodes] = useState(initial); + const [formattedCode, setFormattedCode] = useState(code); + + useLayoutEffect(() => { + async function formatAndHighlight() { + try { + console.log("Original code:", code); + const formatted = await formatCode(code, lang); + setFormattedCode(formatted); + + // Then highlight the formatted code + const highlighted = await highlight(formatted, lang); + setNodes(highlighted); + } catch (error) { + console.error("Error in formatAndHighlight:", error); + // If formatting fails, try to highlight the original code + const highlighted = await highlight(code, lang); + setNodes(highlighted); + } + } + + void formatAndHighlight(); + }, [code, lang]); + + if (!nodes) { + return ( +
+
+
+
+
+
+ ); + } return (
- -
+ +
+ {nodes} +
); } diff --git a/apps/website/app/[locale]/blog/[slug]/components/shared.ts b/apps/website/app/[locale]/blog/[slug]/components/shared.ts new file mode 100644 index 0000000..377f2ca --- /dev/null +++ b/apps/website/app/[locale]/blog/[slug]/components/shared.ts @@ -0,0 +1,19 @@ +import { toJsxRuntime } from "hast-util-to-jsx-runtime"; +import type { JSX } from "react"; +import { Fragment } from "react"; +import { jsx, jsxs } from "react/jsx-runtime"; +import type { BundledLanguage } from "shiki/bundle/web"; +import { codeToHast } from "shiki/bundle/web"; + +export async function highlight(code: string, lang: BundledLanguage) { + const out = await codeToHast(code, { + lang, + theme: "houston", + }); + + return toJsxRuntime(out, { + Fragment, + jsx, + jsxs, + }) as JSX.Element; +} diff --git a/apps/website/app/[locale]/blog/[slug]/page.tsx b/apps/website/app/[locale]/blog/[slug]/page.tsx index 962a302..5c18b75 100644 --- a/apps/website/app/[locale]/blog/[slug]/page.tsx +++ b/apps/website/app/[locale]/blog/[slug]/page.tsx @@ -177,9 +177,10 @@ export default async function BlogPostPage({ params }: Props) { code: ({ className, children }) => { const match = /language-(\w+)/.exec(className || ""); return ( - - {children?.toString() || ""} - + ); }, }; diff --git a/apps/website/package.json b/apps/website/package.json index 7fb39cd..3dae541 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -6,12 +6,13 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start", + "start": "next start -p 3001", "lint": "next lint", "typecheck": "tsc --noEmit" }, "browserslist": "defaults, not ie <= 11", "dependencies": { + "hast-util-to-jsx-runtime": "2.3.5", "@headlessui/react": "^2.2.0", "@headlessui/tailwindcss": "^0.2.0", "@radix-ui/react-accordion": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec3bda0..5b73d4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,6 +151,9 @@ importers: framer-motion: specifier: ^11.3.19 version: 11.3.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + hast-util-to-jsx-runtime: + specifier: 2.3.5 + version: 2.3.5 lucide-react: specifier: 0.364.0 version: 0.364.0(react@18.2.0) @@ -2458,6 +2461,9 @@ packages: hast-util-to-jsx-runtime@2.3.2: resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==} + hast-util-to-jsx-runtime@2.3.5: + resolution: {integrity: sha512-gHD+HoFxOMmmXLuq9f2dZDMQHVcplCVpMfBNRpJsF03yyLZvJGzsFORe8orVuYDX9k2w0VH0uF8oryFd1whqKQ==} + hast-util-to-parse5@8.0.0: resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} @@ -4454,7 +4460,7 @@ snapshots: estree-util-is-identifier-name: 3.0.0 estree-util-scope: 1.0.0 estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.2 + hast-util-to-jsx-runtime: 2.3.5 markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 recma-jsx: 1.0.0(acorn@8.12.1) @@ -6331,6 +6337,26 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-jsx-runtime@2.3.5: + dependencies: + '@types/estree': 1.0.5 + '@types/hast': 3.0.4 + '@types/unist': 3.0.2 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.0 + mdast-util-mdx-jsx: 3.1.2 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.0.0 + space-separated-tokens: 2.0.2 + style-to-object: 1.0.6 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + hast-util-to-parse5@8.0.0: dependencies: '@types/hast': 3.0.4 @@ -7319,7 +7345,7 @@ snapshots: '@types/mdast': 4.0.4 '@types/react': 18.3.5 devlop: 1.1.0 - hast-util-to-jsx-runtime: 2.3.2 + hast-util-to-jsx-runtime: 2.3.5 html-url-attributes: 3.0.1 mdast-util-to-hast: 13.2.0 react: 18.2.0