Menambahkan Syntax Highlighting pada Static Site dengan Prism.js

Pada kali ini, saya akan menjelaskan proses menambahkan fitur syntax highlighting pada static site dengan Prism.js. Static site atau situs statis adalah sebuah situs web yang hanya terdiri dari HTML, CSS, JavaScript. Nah, untuk contoh static site yang akan digunakan adalah milik saya sendiri yang sedang saya kembangkan dengan menggunakan Next.js dan ChakraUI dan dihosting pada Vercel.

Ketika sedang dalam proses pemindahan artikel-artikel dari website lama ke yang baru, saya menemukan beberapa artikel terutama yang menjelaskan hal teknis terdapat potongan baris kode di dalamnya dan hanya ter-render sebagai plain text saja.

Kemudian mencoba untuk mencari tau bagaimana cara untuk menambahkan styling pada potongan baris kode tersebut supaya terlihat seperti ketika dibuka menggunakan kode editor, Visual Studio Code contohnya.

Akhirnya menemukan caranya yaitu dengan mengimplementasikan syntax highlighting dengan menggunakan Prism.js. Supaya tidak lupa dan mungkin dibutuhkan suatu saat nanti, saya akan coba mendokumentasikan caranya pada tulisan saya kali ini.

Sebagai catatan, disini saya menggunakan Next.js blog starter sebagai base dari static site yang sedang saya kembangkan. Repo tersebut dibuat menggunakan Next.js, TypeScript, TailwindCSS dan juga menggunakan Remark yang digunakan untuk mengkonversi Markdown menjadi Markup Language yaitu HTML. Kemudian untuk membuat fitur syntax highlighting saya menggunakan Prism.js dan salah satu pluginnya yaitu remark-prism.

Integrasi Prism.js ke dalam Proyek

Langkah pertama yang dilakukan adalah memasang Prism.js dan remark-prism ke dalam proyek target dengan menggunakan npm.

npm install prismjs remark-prism

Setelah itu, buka file markdownToHtml di dalam folder lib dan menambahkan beberapa baris kode dibawah ini.

import remarkPrism from "remark-prism";

// baris kode lain...

.use(html, { sanitize: false })
.use(remarkPrism, { plugins: ["line-numbers"] })

Dibawah ini adalah isi dari file markdownToHtml setelah menambahkan kode di atas.

import { remark } from "remark";
import html from "remark-html";
import remarkPrism from "remark-prism";

export default async function markdownToHtml(markdown) {
  const result = await remark()
    .use(html, { sanitize: false })
    .use(remarkPrism, { plugins: ["line-numbers"] })
    .process(markdown);

  return result.toString();
}

Menambahkan styles dan tema dari Prism.js

Setelah mengintegrasikan Prism.js ke dalam proyek, langkah selanjutnya adalah menambahkan styles dan tema dari Prism.js ke dalam proyek. Pertama-tama, buka file _app.tsx yang ada di dalam folder pages.

Pada file ini, saya mengimport beberapa styles yaitu main style dari Prism.js, theme style Prism.js yang dipilih (disini saya memilih tema Tomorrow Night) dan sebuah file untuk meng-override styling dari Prism.js yang saya beri nama prism-overrides.css dan disimpan di dalam folder styles. File ini nantinya akan diisi oleh beberapa style rules, untuk saat ini saya biarkan kosong saja dulu.

import "prismjs/themes/prism-tomorrow.css";
import "prismjs/plugins/line-numbers/prism-line-numbers.css";
import "../styles/prism-overrides.css";

Menampilkan baris kode yang di highlight

Langkah selanjutnya adalah membuat metode untuk menampilkan baris kode yang dihighlight Prism.js.

Buka file post-body.tsx yang ada di dalam folder components. Lalu kita akan mengimport beberapa hal yang akan digunakan dalam membuat metode ini.

import { useEffect, useRef } from "react";

Lalu menambahkan ref pada komponen post-body.

const rootRef = useRef < HTMLDivElement > null;

Kemudian menerapkan pada root element dari komponen post-body.

<div ref={rootRef} className="max-w-2xl mx-auto">

Setelah itu menambahkan beberapa baris kode di dalam useEffect pada komponen post-body.

useEffect(() => {
  const allPres = rootRef.current.querySelectorAll("pre");
  const cleanup: (() => void)[] = [];

  for (const pre of allPres) {
    const code = pre.firstElementChild;
    if (!code || !/code/i.test(code.tagName)) {
      continue;
    }

    const highlightRanges = pre.dataset.line;
    const lineNumbersContainer = pre.querySelector(".line-numbers-rows");

    if (!highlightRanges || !lineNumbersContainer) {
      continue;
    }

    const runHighlight = () =>
      highlightCode(pre, highlightRanges, lineNumbersContainer);
    runHighlight();

    const ro = new ResizeObserver(runHighlight);
    ro.observe(pre);

    cleanup.push(() => ro.disconnect());
  }

  return () => cleanup.forEach((f) => f());
}, []);

Apakah ada error yang muncul dari kode editor?

Yaps betul, karena saya belum membuat fungsi highlightCode yang dipanggil pada baris kode di atas.

Oke, sekarang saya akan buat fungsi highlightCode-nya.

function highlightCode(pre, highlightRanges, lineNumberRowsContainer) {
  const ranges = highlightRanges.split(",").filter((val) => val);
  const preWidth = pre.scrollWidth;

  for (const range of ranges) {
    let [start, end] = range.split("-");
    if (!start || !end) {
      start = range;
      end = range;
    }

    for (let i = +start; i <= +end; i++) {
      const lineNumberSpan: HTMLSpanElement =
        lineNumberRowsContainer.querySelector(`span:nth-child(${i})`);
      lineNumberSpan.style.setProperty(
        "--highlight-background",
        "rgb(100 100 100 / 0.5)"
      );
      lineNumberSpan.style.setProperty("--highlight-width", `${preWidth}px`);
    }
  }
}

Masih ada error yang muncul lagi?

Jika ada, error ini dikarenakan saya belum menambahkan downLevelIteration flag pada file tsconfig.json.

Maka selanjutnya saya akan menambahkan flag tersebut ke dalam file tsconfig.json.

{
  "compilerOptions": {
    "downlevelIteration": true
  },
}

Jika sudah, langsung buat contoh artikel dengan format file markdown (md) di dalam folder _posts dan tambahkan baris kode di dalamnya. Berikut ini adalah contohnya.

const name = "Bagas";

function greet() {
  console.log(`Hello World, my name is ${name}`);
}

Lalu run local server dengan menjalankan perintah npm run dev pada terminal dan silahkan lihat hasilnya pada browser kamu.

Tampilannya baris kodenya sudah seperti ketika dibuka pada kode editor, kan?

Menambahkan Fitur Copy-to-Clipboard

Setelah mengubah tampilan baris kode menjadi seperti ketika di buka pada kode editor, waktunya untuk menambah sentuhan terakhir. Yaitu dengan membuat tombol yang digunakan untuk menyalin sintaks ke dalam clipboard untuk dapat di tempel ke aplikasi lain, contohnya ya ehm kode editor.

Untuk membuat metode menyalin kode ke dalam clipboard, saya menggunakan api navigator.clipboard.writeText yang akan dimasukkan ke dalam click event dari tombol salin yang akan dibuat nanti.

Pertama-tama, saya akan menambahkan sebuah baris kode di dalam useEffect yang digunakan untuk memasang tombol salin ke setiap element pre yang di dalamnya berisi baris kode pada artikel target.

useEffect(() => {
  const allPres = rootRef.current.querySelectorAll("pre");
  const cleanup: (() => void)[] = [];

  for (const pre of allPres) {
    const code = pre.firstElementChild;
    if (!code || !/code/i.test(code.tagName)) {
      continue;
    }

    pre.appendChild(createCopyButton(code)); // ini kodenya

Selanjutnya adalah membuat fungsi yang dipanggil pada useEffect, yaitu createCopyButton. Fungsi ini bertugas untuk membuat elemen button, menerapkan styling di dalamnya serta memasangkan api navigator.clipboard.writeText pada event onClick-nya.

function createCopyButton(codeEl) {
  const button = document.createElement("button");
  button.classList.add("prism-copy-button");
  button.textContent = "Copy";

  button.addEventListener("click", () => {
    if (button.textContent === "Copied") {
      return;
    }
    navigator.clipboard.writeText(codeEl.textContent || "");
    button.textContent = "Copied";
    button.disabled = true;
    setTimeout(() => {
      button.textContent = "Copy";
      button.disabled = false;
    }, 3000);
  });

  return button;
}

Langkah terakhir dalam membuat tombol salin baris kode adalah menambahkan styling untuk tombol dan properti word wrap untuk elemen yang menyimpan baris kode pada layout artikel supaya terlihat lebih rapih lagi.

Tambahkan baris kode di bawah ini pada file prism-overrides.css yang sudah dibuat sebelumnya.

code[class*="language-"] {
  white-space: pre-wrap !important;
}

.prism-copy-button {
  margin: 16px 0 0 0;
  float: right;
  width: 10ch;
  background-color: rgb(100 100 100 / 0.5);
  border-width: 0;
  color: #FFFFFFF;
  cursor: pointer;
}

.prism-copy-button[disabled] {
  cursor: default;
}

Untuk tampilan akhir dari fitur syntax highlighting yang sudah dibuat kurang lebih seperti ini.

Hasil Akhir

Penutup

Itu tadi yang adalah dokumentasi tentang proses saya menambahkan fitur syntax highlighting pada layout baru dari situs pribadi saya yang sedang dikembangkan. Semoga berguna bagi yang membaca tulisan ini, termasuk saya sendiri.

Terimakasih sudah mampir, semoga sehat selalu!

Referensi: CSS Tricks

© 2022 Built with Next.js and ChakraUI. Inspired by Takuya Matsuyama's site.