mdBook icon indicating copy to clipboard operation
mdBook copied to clipboard

Better syntax highlighting: switching from `highlight.js` to `shiki`

Open julio4 opened this issue 1 year ago • 3 comments

Problem

The current highlighting library highlight.js is lightweight and can run on both server and client, but provides a very basic syntax parsing that could be greatly improved.

Supersede #2313 #2292 #2268 #2238 #2237 #2360 #2445 #1652 ...

Proposed Solution

Consider switching to shiki, a high quality syntax highlighter that uses TextMate grammars and themes (same as VS Code), yet stays performant. It is also actively maintained.

There have been significant efforts and improvements that led to shiki-v1, and I believe that it's worthwile to make this migration and make shiki the default highlighter for mdbook. Today, mdbook is used to document a lot of code snippets and this change would definitely improve readability.

Example

Let's take Listing 20-24 from the rust book:

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv();

            match message {
                Ok(job) => {
                    println!("Worker {id} got a job; executing.");

                    job();
                }
                Err(_) => {
                    println!("Worker {id} disconnected; shutting down.");
                    break;
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}
mdbook w/ highlight.js (Coal theme) Shiki (github-dark-dimmed theme)
Open in browser Open in browser
Image Image

Vs-code reference (theme Github Dark Dimmed): Image

Implementation example

@cestef overriding of theme/book.js for shiki compatibility as explained here

Notes

Shiki v1: https://nuxt.com/blog/shiki-v1 Another alternative: https://github.com/wooorm/starry-night

Further ideas

Twoslash allows to extend Typescript code snippets from markdown file with extended Typescript compiler's information and vs code syntax engine. It is maintained and integrated perfectly with shiki.

With the strong rust-analyzer and the recent abstraction of the twoslash API in twoslash-protocol, it would make sense to build a twoslash-rs for mdbook. Some preliminary explorations have been done: by ayazhafiz/twoslash-rust and @jxom

This first implementation of Shiki in mdbook would pave the way for this later on.

julio4 avatar Nov 04 '24 07:11 julio4

@ehuss Any update on this? Would this be something that you would consider?

julio4 avatar Feb 07 '25 14:02 julio4

We are likely going to switch to build-side rendering with syntect (https://github.com/rust-lang/mdBook/pull/1652), so probably not going to add any other systems.

ehuss avatar Feb 07 '25 14:02 ehuss

We are likely going to switch to build-side rendering with syntect (#1652), so probably not going to add any other systems.

Looks really nice but the issue seems to be stale, so not sure if it can be included soon... It's been a pain for years to manually provide custom highlight.js file for unsupported languages.

Also, shiki provide build-side rendering, with the possibility of exploring further ideas as I mentioned in the issue and with better syntax highlighting as, IMHO, TextMate grammars are slightly better than Sublime Text syntax with oniguruma. Won't be in rust but build time overhead is not significant. So I would still like to consider Shiki if possible

julio4 avatar Feb 07 '25 15:02 julio4

For those are interested in Shiki, you can put a highlight.js file in "theme" directory, and use the code below:

let shikiModule

window.hljs = {
    configure() {
        shikiModule = import('https://esm.sh/[email protected]')
    },
    /** @param {HTMLElement} block */
    async highlightBlock(block) {
        const lang = [...block.classList.values()]
            .map(name => name.match(/^language-(.+)$/)?.[1])
            .filter(Boolean)[0]
        if (!lang) {
            return
        }
        const shiki = await shikiModule
        block.parentElement.innerHTML =
            await shiki.codeToHtml(block.innerText, { lang, theme: 'one-light' })
    }
}
  • Note 1: This will load full size of Shiki, please tweak the imports accordingly.
  • Note 2: Buttons (such as "Copy") in code block will lost.

g-plane avatar May 07 '25 07:05 g-plane

For anyone interested, I got a slightly more integrated version of the snippet above to work on https://reaction.ppom.me/reference.html I made shiki aware of the current light/dark theme: highlight.js:

// Keep only 2 themes, one dark and one light
(function themeOverride() {
	document.getElementById("light").remove();
	document.getElementById("coal").remove();
	document.getElementById("navy").remove();
	document.getElementById("rust").innerText = "Light (Rust)";
	document.getElementById("ayu").innerText = "Dark (Ayu)";
})();

(function shikiHighlighting() {
	let shikiModule;

	window.hljs = {
		configure() {
			shikiModule = import("https://esm.sh/[email protected]");
		},
		/** @param {HTMLElement} block */
		async highlightBlock(block) {
			const lang = [...block.classList.values()]
				.map((name) => name.match(/^language-(.+)$/)?.[1])
				.filter(Boolean)[0];
			if (!lang) {
				return;
			}
			const shiki = await shikiModule;
			block.parentElement.innerHTML = await shiki.codeToHtml(block.innerText, {
				lang,
				themes: {
					light: "gruvbox-light-hard",
					dark: "aurora-x",
				},
				defaultColor: false,
			});
		},
	};
})();

And I added a new CSS file that inherits the code background color of mdbook's code blocks, to make it more consistent: fix.css:

/* Aurora X */
html.ayu {
	--code-background-color: #191f26;
	--tab-color: #bbbbbb;
}

/* Gruvbox Light Hard */
html.rust {
	--code-background-color: #f6f7f6;
	--tab-color: #3c3836;
}

code {
	background-color: var(--code-background-color);
}

html.rust .shiki,
html.rust .shiki span {
  color: var(--shiki-light);
  /* Optional, if you also want font styles */
  font-style: var(--shiki-light-font-style);
  font-weight: var(--shiki-light-font-weight);
  text-decoration: var(--shiki-light-text-decoration);
}

html.ayu .shiki,
html.ayu .shiki span {
  color: var(--shiki-dark);
  /* Optional, if you also want font styles */
  font-style: var(--shiki-dark-font-style);
  font-weight: var(--shiki-dark-font-weight);
  text-decoration: var(--shiki-dark-text-decoration);
}

That you must add in book.toml:

[output.html]
# add the CSS file
additional-css = ["fix.css"]
# update default themes
default-theme = "rust"
preferred-dark-theme = "ayu"

Complete source

ppom0 avatar Oct 14 '25 15:10 ppom0