diff --git a/components/GithubFileReaderDisplay.tsx b/components/GithubFileReaderDisplay.tsx index f01618e..1cca621 100644 --- a/components/GithubFileReaderDisplay.tsx +++ b/components/GithubFileReaderDisplay.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import { FaGithub, FaLink, FaSpinner } from "react-icons/fa"; -import { getHighlighter, type Highlighter, bundledLanguages } from "shiki"; import { useTheme } from "next-themes"; +import { dedentCode, getLanguage, getShikiHighlighter } from "@/pages/shiki"; interface GithubFileReaderDisplayProps { url: string; @@ -10,42 +10,12 @@ interface GithubFileReaderDisplayProps { title?: string; } -// Create a singleton promise for the highlighter -let highlighterPromise: Promise | null = null; - -// Singleton function to get or create the highlighter -const getShikiHighlighter = () => { - if (!highlighterPromise) { - highlighterPromise = getHighlighter({ - themes: ["github-dark", "github-light"], - langs: Object.keys(bundledLanguages), - }); - } - return highlighterPromise; -}; - -// Language detection utility -const getLanguage = (url: string) => { - const extension = url.split(".").pop()?.toLowerCase(); - switch (extension) { - case "rs": - return "rust"; - case "ts": - case "tsx": - return "typescript"; - case "js": - case "jsx": - return "javascript"; - default: - return "plaintext"; - } -}; - const GithubFileReaderDisplay: React.FC = ({ url, fromLine = 1, toLine, title, + dedent = true, }) => { const [content, setContent] = useState(""); const [loading, setLoading] = useState(true); @@ -71,7 +41,12 @@ const GithubFileReaderDisplay: React.FC = ({ const text = await response.text(); const lines = text.split("\n"); const selectedLines = lines.slice(fromLine - 1, toLine || lines.length); - const codeContent = selectedLines.join("\n"); + let codeContent = selectedLines.join("\n"); + + // Apply dedentation if enabled + if (dedent) { + codeContent = dedentCode(codeContent); + } // Set the theme based on current theme const theme = currentTheme === "dark" ? "github-dark" : "github-light"; @@ -80,10 +55,6 @@ const GithubFileReaderDisplay: React.FC = ({ const highlightedCode = highlighter.codeToHtml(codeContent, { lang: getLanguage(url), theme: theme, - lineOptions: Array.from({ length: selectedLines.length }, (_, i) => ({ - line: i + 1, - classes: [`line-${i + fromLine}`], - })), }); // Wrap the highlighted code with a div that sets the starting line number diff --git a/pages/_app.tsx b/pages/_app.tsx index f8f475f..808bef4 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -7,6 +7,7 @@ import type { ReactElement, ReactNode } from "react"; import "../globals.css"; import { GoogleAnalytics } from "@next/third-parties/google"; import { AskCookbook } from "../components/AskCookbook"; +import { getShikiHighlighter } from "./shiki"; export type NextPageWithLayout

= NextPage & { getLayout?: (page: ReactElement) => ReactNode; @@ -24,6 +25,9 @@ if (typeof window !== "undefined" && !("requestIdleCallback" in window)) { window.cancelIdleCallback = (id) => clearTimeout(id); } +// Initialize the highlighter when the app starts +getShikiHighlighter(); + export default function Nextra({ Component, pageProps }: NextraAppProps) { // Define a layout if it doesn't exist in the page component const getLayout = Component.getLayout || ((page) => page); diff --git a/pages/shiki.ts b/pages/shiki.ts new file mode 100644 index 0000000..1368b7d --- /dev/null +++ b/pages/shiki.ts @@ -0,0 +1,56 @@ +import { createHighlighter, type Highlighter, bundledLanguages } from "shiki"; + +// Create a singleton promise for the highlighter +let highlighterPromise: Promise | null = null; + +// Singleton function to get or create the highlighter +export const getShikiHighlighter = () => { + if (!highlighterPromise) { + highlighterPromise = createHighlighter({ + themes: ["github-dark", "github-light"], + langs: Object.keys(bundledLanguages), + }); + } + return highlighterPromise; +}; + +// Language detection utility +export const getLanguage = (url: string) => { + const extension = url.split(".").pop()?.toLowerCase(); + switch (extension) { + case "rs": + return "rust"; + case "ts": + case "tsx": + return "typescript"; + case "js": + case "jsx": + return "javascript"; + default: + return "plaintext"; + } +}; + +export const dedentCode = (code: string): string => { + const lines = code.split("\n"); + + // Filter out empty lines for calculating minimum indentation + const nonEmptyLines = lines.filter((line) => line.trim().length > 0); + + if (nonEmptyLines.length === 0) return code; + + // Find the minimum indentation level across all non-empty lines + const minIndent = Math.min( + ...nonEmptyLines.map((line) => { + const match = line.match(/^[ \t]*/); + return match ? match[0].length : 0; + }), + ); + + // Remove the common indentation from all lines + if (minIndent > 0) { + return lines.map((line) => line.slice(minIndent)).join("\n"); + } + + return code; +};