From 5f66cfd2344437baf806f502fc3ee91e43c122ae Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sun, 2 Mar 2025 16:12:26 -0600 Subject: [PATCH] feat: add syntax highlighting and code formatting for blog posts --- .../website/app/[locale]/blog/[slug]/page.tsx | 66 ++++------------ .../[locale]/blog/components/CodeBlock.tsx | 75 ++++++++----------- apps/website/package.json | 2 + pnpm-lock.yaml | 23 ++++++ 4 files changed, 71 insertions(+), 95 deletions(-) diff --git a/apps/website/app/[locale]/blog/[slug]/page.tsx b/apps/website/app/[locale]/blog/[slug]/page.tsx index f9a8531..d9c045a 100644 --- a/apps/website/app/[locale]/blog/[slug]/page.tsx +++ b/apps/website/app/[locale]/blog/[slug]/page.tsx @@ -11,6 +11,7 @@ import ReactMarkdown from "react-markdown"; import type { Components } from "react-markdown"; import rehypeRaw from "rehype-raw"; import remarkGfm from "remark-gfm"; +import TurndownService from "turndown"; import { CodeBlock } from "../components/CodeBlock"; import { ZoomableImage } from "./components/ZoomableImage"; @@ -76,6 +77,13 @@ export default async function BlogPostPage({ params }: Props) { notFound(); } + // Convertir HTML a Markdown + const turndownService = new TurndownService({ + headingStyle: "atx", + codeBlockStyle: "fenced", + }); + const markdown = turndownService.turndown(post.html); + const formattedDate = new Date(post.published_at).toLocaleDateString(locale, { year: "numeric", month: "long", @@ -152,60 +160,14 @@ export default async function BlogPostPage({ params }: Props) { ), code: ({ inline, className, children, ...props }: CodeProps) => { - if (inline) { - return ( - - {children} - - ); - } - const match = /language-(\w+)/.exec(className || ""); - // Extraer el contenido del código de la estructura anidada - const extractCodeContent = (children: React.ReactNode): string => { - if (typeof children === "string") { - return children; - } - if (Array.isArray(children)) { - return children - .map((child) => { - if (typeof child === "string") { - return child; - } - if (child && typeof child === "object" && "props" in child) { - return extractCodeContent(child.props.children); - } - return ""; - }) - .join(""); - } - if (children && typeof children === "object" && "props" in children) { - return extractCodeContent(children.props.children); - } - return ""; - }; - - const codeContent = extractCodeContent(children) - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/&/g, "&") - .trim(); - - // Wrap CodeBlock in a div to prevent it from being inside a p tag return ( -
- -
+ ); }, }; @@ -298,7 +260,7 @@ export default async function BlogPostPage({ params }: Props) { rehypePlugins={[rehypeRaw]} components={components} > - {post.html} + {markdown} diff --git a/apps/website/app/[locale]/blog/components/CodeBlock.tsx b/apps/website/app/[locale]/blog/components/CodeBlock.tsx index e51ed33..f65b3d8 100644 --- a/apps/website/app/[locale]/blog/components/CodeBlock.tsx +++ b/apps/website/app/[locale]/blog/components/CodeBlock.tsx @@ -75,18 +75,10 @@ export function CodeBlock({ code, language, className = "" }: CodeBlockProps) { formatCode(); }, [code, language]); - // Ensure we're working with a string and remove any potential duplicate line breaks - const processedCode = - typeof formattedCode === "string" - ? formattedCode.replace(/\n+/g, "\n") - : typeof formattedCode === "object" - ? JSON.stringify(formattedCode, null, 2) - : String(formattedCode); - return ( {({ @@ -96,43 +88,40 @@ export function CodeBlock({ code, language, className = "" }: CodeBlockProps) { getLineProps, getTokenProps, }) => ( -
-
-						
- +
+ {tokens.map((line, i) => ( +
+ - Copy - + {i + 1} + + + {line.map((token, key) => ( + + ))} +
- {tokens.map((line, i) => ( -
- - {i + 1} - - - {line.map((token, key) => ( - - ))} - -
- ))} -
+ ))}
)}
diff --git a/apps/website/package.json b/apps/website/package.json index 93bc21a..d7f0873 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -25,6 +25,7 @@ "@tabler/icons-react": "3.21.0", "@tryghost/content-api": "^1.11.21", "@types/node": "20.4.6", + "@types/turndown": "^5.0.5", "autoprefixer": "^10.4.12", "axios": "^1.8.1", "class-variance-authority": "^0.7.0", @@ -40,6 +41,7 @@ "tailwind-merge": "^2.2.2", "tailwindcss": "^3.4.1", "tailwindcss-animate": "^1.0.7", + "turndown": "^7.2.0", "typescript": "5.1.6" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9ba566..b06e7dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,6 +143,9 @@ importers: '@types/node': specifier: 20.4.6 version: 20.4.6 + '@types/turndown': + specifier: ^5.0.5 + version: 5.0.5 autoprefixer: specifier: ^10.4.12 version: 10.4.19(postcss@8.4.47) @@ -188,6 +191,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.7) + turndown: + specifier: ^7.2.0 + version: 7.2.0 typescript: specifier: 5.1.6 version: 5.1.6 @@ -968,6 +974,9 @@ packages: '@mdx-js/mdx@3.1.0': resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@next/env@14.2.2': resolution: {integrity: sha512-sk72qRfM1Q90XZWYRoJKu/UWlTgihrASiYw/scb15u+tyzcze3bOuJ/UV6TBOQEeUaxOkRqGeuGUdiiuxc5oqw==} @@ -1660,6 +1669,9 @@ packages: '@types/react@18.3.5': resolution: {integrity: sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==} + '@types/turndown@5.0.5': + resolution: {integrity: sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==} + '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} @@ -3517,6 +3529,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + turndown@7.2.0: + resolution: {integrity: sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==} + typescript@5.1.6: resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} engines: {node: '>=14.17'} @@ -4322,6 +4337,8 @@ snapshots: - acorn - supports-color + '@mixmark-io/domino@2.2.0': {} + '@next/env@14.2.2': {} '@next/env@15.0.3': {} @@ -5279,6 +5296,8 @@ snapshots: '@types/prop-types': 15.7.12 csstype: 3.1.3 + '@types/turndown@5.0.5': {} + '@types/unist@2.0.10': {} '@types/unist@3.0.2': {} @@ -7576,6 +7595,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + turndown@7.2.0: + dependencies: + '@mixmark-io/domino': 2.2.0 + typescript@5.1.6: {} typescript@5.6.3: {}