Loading...
Syntax Highlighting
Author Cloudapp
E.G.

Next.js 14 - Erweiterte Syntax-/Code-Hervorhebung

10. Juli 2024
Inhaltsverzeichnis

Sie können verschiedene Ansätze verwenden, um Codeblöcke in Ihren Blog oder Ihre Website zu integrieren (TailwindCSS, usw.). Ich stieß auf einen modernen Ansatz zur Syntaxhervorhebung und entdeckte Shiki, einen relativ neuen Syntaxhervorhefter, der von Pine Wu entwickelt und von Anthony Fu gepflegt wird. Bedeutende Unternehmen wie Vercel nutzen Shiki für ihre Next.js-Dokumentation und Astro für die Syntaxhervorhebung.

Hier ist das GitHub Repo mit dem gesamten Code und darunter der Link zur Beispielwebsite.

Example page hosted on Vercel -> https://nextjs14-contentful-syntax-highlighting.vercel.app/

Warum Shiki?

Syntaxhervorhebung in Next.js-Anwendungen kann für Techblogs sehr interessant sein. Shiki basiert auf der TextMate-Grammatik, dem gleichen System, das auch von Visual Studio Code verwendet wird, und gewährleistet so eine genaue und visuell ansprechende Codehervorhebung. Noch wichtiger für die Leistung unserer Website ist, dass Shiki den hervorgehobenen Code bereits vor der Zeit rendert. Das bedeutet, dass kein zusätzliches JavaScript für die Syntaxhervorhebung an den Client gesendet wird, was dem Bestreben von Next.js nach Serverkomponenten und minimalem clientseitigem JavaScript entspricht.

Verwendeter Stack

Ich werde mit meinem Standard-Stack beginnen:

  • Next.js 14 als Web-Framework, und ich werde die mitgelieferte Middleware Edge-Funktion verwenden

  • NPM-Paket Shiki für die Syntaxhervorhebung und das entsprechende Transformers-Paket

  • Vercel für das Hosting

Inspiration und ursprüngliche Quelle

Danke an Nikolai Lehbrink für die Inspiration und die ursprüngliche Codequelle, die ich zur Integration in mein Next.js 14-Projekt verwendet habe. Wenn Sie an einer detaillierten Anleitung zur Integration in Next.js 13 interessiert sind -> https://www.nikolailehbr.ink/blog/syntax-highlighting-shiki-next-js

Zwei neue NPM Pakete

npm install shiki
npm install @shikijs/transformers

Neue Komponente für Syntax Hervorhebung

Jetzt werde ich eine neue Komponente erstellen, die ich auf der Startseite wiederverwenden kann.

//src/components/tools/syntaxhighlight/syntaxhighlight.component.tsx
import { codeToHtml } from "shiki";

export default async function SyntaxHighlight() {
  const html = await codeToHtml("const a = 1 + 3", {
    lang: "javascript",
    theme: "nord",
  });

  return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
}

Integration der neuen Komponente in die Datei page.tsx

Unterhalb ist die Integration ersichtlich

//src/app/[locale]/page.tsx
import SyntaxHighlight from "@/components/tools/syntaxhighlight/syntaxhighlight.component";

export default function Home() {
  return (
    <>
      <SyntaxHighlight />
    </>
  );
}

Nun machen wir es hübsch

Im Moment ist alles fest kodiert, also lassen Sie uns unsere Komponente erweitern, damit sie den Code, die Sprache (lang) und das Theme als Eigenschaften (props) übernimmt.

import { codeToHtml } from "shiki";
import type { BundledLanguage, BundledTheme } from "shiki";

type Props = {
  code: string;
  lang?: BundledLanguage;
  theme?: BundledTheme;
};

export default async function SyntaxHighlight({
  code,
  lang = "javascript",
  theme = "nord",
}: Props) {
  
  const html = await codeToHtml(code, {
    lang,
    theme,
  });

  return (
    <div
      dangerouslySetInnerHTML={{ __html: html }}
    ></div>
  );
}

Übergeben der Parameter an die Datei Page.tsx

//src/app/[locale]/page.tsx
import SyntaxHighlight from "@/components/tools/syntaxhighlight/syntaxhighlight.component";

export default function Home() {
  return (
    <>
      <SyntaxHighlight code="let a = 1 + 4" />
      <SyntaxHighlight code="console.log('Hello, world!')" lang="typescript" />
      <SyntaxHighlight
        code={`fn main() { println!(\"Hello, world!\"); }`}
        lang="rust"
        theme="github-dark"
      />
    </>
  );
}

Erst schön, jetzt verbessern wir es.

Die Grundfunktionalität ist erledigt. Lassen Sie uns einige Verbesserungen zu unserem Codeblock hinzufügen.

Hervorheben von bestimmten Code-Zeilen

Ich wollte die Implementierung der Zeilenhervorhebung nicht verpassen. Shiki hat ein paar Transformer, die uns dies schnell einrichten lassen. Ich habe bereits das Transformer-Paket und das Shiki-Basispaket installiert. 1. Neuer Import in Zeile 3:

„import { transformerNotationHighlight } from '@shikijs/transformers';“

2. Aktivierung der Transformer unter „lang“ und „theme“:

transformers: [transformerNotationHighlight()],

//syntaxhighlight Component
import { codeToHtml } from "shiki";
import type { BundledLanguage, BundledTheme } from "shiki";
import { 
    transformerNotationHighlight,
  } from "@shikijs/transformers";

type Props = {
  code: string;
  lang?: BundledLanguage;
  theme?: BundledTheme;
};

export default async function SyntaxHighlight({
  code,
  lang = "javascript",
  theme = "nord",
}: Props) {
  const html = await codeToHtml(code, {
    lang,
    theme,
    transformers: [transformerNotationHighlight()],
  });

  return <div 
    className="[&_.highlighted]:bg-blue-500"
    dangerouslySetInnerHTML={{ __html: html }}>
  </div>;
}

Anpassen des Code Parameters in der Datei Page.tsx

Ich habe diesen Abschnitt “ // [!code highlight]” am Ende der Code-Zeile hinzugefügt.

<SyntaxHighlight
   code={`fn main() { println!(\"Hello, world!\"); }`}
   lang="rust"
   theme="github-dark"
/>

Dies fügt die Klasse „has-highlighted“ zum <pre> und die Klasse „highlighted“ zum <span> hinzu. Jetzt ziele ich mit CSS auf die Klasse und style sie entsprechend. Ich werde TailwindCSS (mit seinen beliebigen Werten) verwenden.

<div className="[&_.highlighted]:bg-blue-500"
    dangerouslySetInnerHTML={{ __html: html }}>
</div>

Code Anpassungen anzeigen

Um Klassen für hinzugefügte und entfernte Zeilen hinzuzufügen, folgen wir den gleichen Schritten wie oben, jedoch mit einem anderen Transformer namens transformerNotationDiff. Zuerst importierte ich den neuen Transformer und fügte dann den Kommentar // [!code ++] für hinzugefügte Zeilen und den entsprechenden Kommentar // [!code --] für entfernte Zeilen zu Ihrer code-Eigenschaft hinzu.

import { codeToHtml } from "shiki";
import type { BundledLanguage, BundledTheme } from "shiki";
import {
  transformerNotationHighlight,
  transformerNotationDiff,
} from "@shikijs/transformers";
import CopyToClipboard from "./copyToClipboard.component";

type Props = {
  code: string;
  lang?: BundledLanguage;
  theme?: BundledTheme;
};

export default async function SyntaxHighlight({
  code,
  lang = "javascript",
  theme = "nord",
  filename,
}: Props) {
  const html = await codeToHtml(code, {
    lang,
    theme,
    transformers: [transformerNotationHighlight(), transformerNotationDiff()],
  });

  return (
         <div
          className="border-t-2 border-neutral-700 text-sm [&>pre]:overflow-x-auto [&>pre]:!bg-neutral-900 [&>pre]:py-3 [&>pre]:pl-4 [&>pre]:pr-5 [&>pre]:leading-snug [&_code]:block [&_code]:w-fit [&_code]:min-w-full"
          dangerouslySetInnerHTML={{ __html: html }}
        ></div>
  );
}    <SyntaxHighlight
          code="console.log('Hello, world!')"
          lang="typescript"         
    />     
    <SyntaxHighlight
          code="console.log('Hello, world!')"
          lang="typescript"
    />

Neue Style-Klassen “diff remove” und “diff add”

Dies fügt erneut die diff remove und diff add hinzu. Wir können Klassen auf das gerenderte HTML anwenden, um die Komponente später zu stylen.

<pre class="shiki nord has-diff" style="background-color:#2e3440ff;color:#d8dee9ff" tabindex="0">
    <code>
        <span class="line diff remove">...</span>
        <span class="line diff add">...</span>
    </code>
</pre>

Hinzufügen von "Dateinamen" und zusätzlichen Styles

Neben der Hervorhebung wollte ich meinen Codekomponenten Dateinamen hinzufügen. Auf diese Weise weiß der Leser immer, zu welcher Datei der angegebene Code gehört. Lassen Sie uns also eine filename-Eigenschaft zu unserer Komponente hinzufügen und einige anfängliche Stile anwenden, um den Effekt zu sehen.

import { codeToHtml } from "shiki";
import type { BundledLanguage, BundledTheme } from "shiki";
import {
  transformerNotationHighlight,
  transformerNotationDiff,
} from "@shikijs/transformers";
import CopyToClipboard from "./copyToClipboard.component";

type Props = {
  code: string;
  lang?: BundledLanguage;
  theme?: BundledTheme;
  filename?: string;
};

export default async function SyntaxHighlight({
  code,
  lang = "javascript",
  theme = "nord",
  filename,
}: Props) {
  const html = await codeToHtml(code, {
    lang,
    theme,
    transformers: [transformerNotationHighlight(), transformerNotationDiff()],
  });

  return (
    <div className="mt-4 rounded-lg bg-gradient-to-r from-sky-200 to-sky-400 p-4 !pr-0 md:p-8 lg:p-12 [&>pre]:rounded-none max-w-4xl">
      <div className="overflow-hidden rounded-s-lg">
        <div className="flex items-center justify-between bg-gradient-to-r from-neutral-900 to-neutral-800 py-2 pl-2 pr-4 text-sm">
          <span className="-mb-[calc(0.5rem+2px)] rounded-t-lg border-2 border-white/5 border-b-neutral-700 bg-neutral-800 px-4 py-2 ">
            {filename}
          </span>
          <CopyToClipboard code={code} />
        </div>
        <div
          className="border-t-2 border-neutral-700 text-sm [&>pre]:overflow-x-auto [&>pre]:!bg-neutral-900 [&>pre]:py-3 [&>pre]:pl-4 [&>pre]:pr-5 [&>pre]:leading-snug [&_code]:block [&_code]:w-fit [&_code]:min-w-full"
          dangerouslySetInnerHTML={{ __html: html }}
        ></div>
      </div>
    </div>
  );
}

In unserer page.tsx fügen wir das Attribut "filename" hinzu, damit wir es als "Prop" an die Komponente

<Code code="let a = 1 + 4" filename="index.js" />

Integration von Zeilennummern mit TailwindCSS

Eine häufige Ergänzung in einem Codeblock sind Zeilennummern, die die Komponente optisch ansprechender machen und dem Leser gleichzeitig eine Referenz für bestimmte Zeilen im Code geben.

Stylen der Code-Highlights und der Codeunterschiede (Diffs)

Um das Design der einzelnen hervorgehobenen Zeilen zu verbessern, habe ich außerdem die folgenden Stile hinzugefügt.

//app/global.css
.shiki {
  counter-reset: step;
  counter-increment: step 0;
  .line {
    @apply border-l-4 border-transparent;
    &::before {
      counter-increment: step;
      @apply mr-6 inline-block w-4 border-transparent text-right text-neutral-600 content-[counter(step)];
    }
    &.highlighted,
    &.diff {
      @apply -ml-4 -mr-5 inline-block w-[calc(100%+(theme(spacing.5)+theme(spacing.4)))] pl-4 pr-5;
    }
    &.highlighted {
      @apply border-neutral-500 bg-neutral-800;
    }
    &.diff {
      &.add,
      &.remove {
        span:first-child::before {
          @apply -ml-4 inline-flex w-4;
        }
      }
      &.add {
        @apply border-blue-500 bg-blue-500/25 before:text-blue-500;
        span:first-child::before {
          @apply text-blue-500 content-["+"];
        }
      }
      &.remove {
        @apply border-orange-500 bg-orange-500/30 opacity-70 *:!text-neutral-400 before:text-orange-500;
        span:first-child::before {
          @apply text-orange-500 content-["-"];
        }
      }
    }
  }
}

Anpassen der Datei post.config.js

Sie müssen das Plugin nicht installieren, da es bereits mit TailwindCSS enthalten ist, das bereits installiert wurde.

//post.config.js
module.exports = {
  plugins: {
    "tailwindcss/nesting": {},
    tailwindcss: {},
    autoprefixer: {},
  },
};

Endergebnis

Zuletzt habe ich einige Stile hinzugefügt, um die gesamte Syntax-Hervorhebungskomponente optisch ansprechender zu gestalten, und einen Hintergrundverlauf hinzugefügt. Außerdem habe ich eine Funktion zum Kopieren in die Zwischenablage hinzugefügt. Den vollständigen Code finden Sie am Anfang dieser Geschichte unter dem angegebenen Link.

return (
    <div className="mt-4 rounded-lg bg-gradient-to-r from-sky-200 to-sky-400 p-4 !pr-0 md:p-8 lg:p-12 [&>pre]:rounded-none max-w-4xl">
      <div className="overflow-hidden rounded-s-lg">
        <div className="flex items-center justify-between bg-gradient-to-r from-neutral-900 to-neutral-800 py-2 pl-2 pr-4 text-sm">
          <span className="-mb-[calc(0.5rem+2px)] rounded-t-lg border-2 border-white/5 border-b-neutral-700 bg-neutral-800 px-4 py-2 ">
            {filename}
          </span>
          <CopyToClipboard code={code} />
        </div>
        <div
          className="border-t-2 border-neutral-700 text-sm [&>pre]:overflow-x-auto [&>pre]:!bg-neutral-900 [&>pre]:py-3 [&>pre]:pl-4 [&>pre]:pr-5 [&>pre]:leading-snug [&_code]:block [&_code]:w-fit [&_code]:min-w-full"
          dangerouslySetInnerHTML={{ __html: html }}
        ></div>
      </div>
    </div>
  );

Cloudapp-dev und bevor Sie uns verlassen

Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:

Wenn Ihnen gefallen hat was Sie gelesen haben oder wenn es Ihnen sogar geholfen hat, dann würden wir uns über einen "Clap" 👏 oder einen neuen Follower auf unseren Medium Account sehr freuen.

Oder folgen Sie uns auf Twitter -> Cloudapp.dev

Verwandte Artikel