Inline Code Highlighting With Markdown-It & Shiki
The project has switched to Efterklang/hexo-renderer-markdown-exit + markdown-exit-shiki
First, a preview of the result
- **Plain**: `printf("Hello, World")`- **Python**: `{python} print("Hello, World")`- **JavaScript**: `{javascript} console.log("Hello, World")`- **HTML**: `{html} <h1>Hello, World!</h1>`- **Rust** `{rust} fn main() { println!("Hello, World!"); }`- **Shell**: `{shell} echo "Hello, World!"`- Plain:
printf("Hello, World") - Python:
print("Hello, World") - JavaScript:
console.log("Hello, World") - HTML:
<h1>Hello, World!</h1> - Rust
fn main() { println!("Hello, World!"); } - Shell:
echo "Hello, World!"
Tips: If you use Obsidian as your editor, install obsidian-shiki-plugin and enable the
inline-highlightfeature
Usage
Install a markdown-it renderer
Take Hexo as an example. Its default renderer is marked[1], which converts Markdown into HTML. For example:
**Bold**renders as<strong>Bold<strong/>*Italic*renders as<em>Italic<em/>
This plugin is built on the markdown-it renderer, so you need to switch to markdown-it first. I use the hexo-renderer-markdown-it-plus plugin[2]. Run these commands in the root of your Hexo blog:bun uninstall hexo-renderer-marked --savebun i hexo-renderer-markdown-it-plus --savebun i markdown-it-inline-code@0.2.0
Update the Hexo config:markdown_it_plus: highlight: false html: true xhtmlOut: true breaks: true langPrefix: null linkify: true typographer: null quotes: “”‘’ pre_class: highlight plugins: - plugin: name: markdown-it-inline-code enable: true
Test the output, add custom styles
Run hexo clean && hexo s. Taking *Italic* as an example, the expected rendered output is:<code> <span style=" color: #d20f39; --shiki-light-font-style: italic; --shiki-dark: #f38ba8; --shiki-dark-font-style: italic; --shiki-tokyo: #c0caf5; --shiki-tokyo-font-style: italic; " > Italic </span></code>
You can refer to my earlier post to make it work with theme switching. Here is a CSS demo that switches via attribute selectors:code span { font-style: var(--shiki-light-font-style);}:where([data-theme="tokyo_night"]) { code span { font-style: var(--shiki-tokyo-font-style); color: var(--shiki-tokyo); }}:where([data-theme="mocha"], [data-theme="macchiato"]) { code span { font-style: var(--shiki-dark-font-style); color: var(--shiki-dark); }}
Development Story
At first I skimmed markdown-it’s plugin docs and figured it should be easy, so I started coding. The plan was to finish before bed, but I ended up grinding until 4 a.m. and gave up. Partly because I’m no good at TS/JS, partly because coding before sleep is a bad idea.
The next day I cleaned up the code and vibe-coded a beta version. The code quality could be better, but
Using Shiki Synchronously
Initially I used Shiki’s codeToHtml API, which is async and returns a Promise. Since markdown-it’s render.rules must be synchronous, I followed Synchronous Usage | Shiki docs and used createJavaScriptRegexEngine({lang, themes, engine}) to create a highlighter instance synchronouslyimport { createHighlighterCoreSync } from 'shiki/core'import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'// allLangs: list of languages supported by shikijs// see all-langs.ts in Efterklang/markdown-it-inline-code/srcimport { allLangs } from './all-langs'// themeimport latte from '@shikijs/themes/catppuccin-latte'import mocha from '@shikijs/themes/catppuccin-mocha'import tokyo_night from '@shikijs/themes/tokyo-night'const themes = [latte, mocha, tokyo_night]const SHIKI_KEY = Symbol.for('mdit-inline-code-shiki')// global shiki singleton instancefunction getShiki() { const g = globalThis as any if (g[SHIKI_KEY]) return g[SHIKI_KEY] // Handle potential default export wrapping when bundled/externalized // [!code warning] const themeList = themes.map(t => (t as any).default || t) const shiki = createHighlighterCoreSync({ langs: allLangs, themes: themeList, engine: createJavaScriptRegexEngine() }) g[SHIKI_KEY] = shiki return shiki}
Custom Render Rules
You can override the renderer.rules.code_inline method on the MarkdownIt instance to customize inline code rendering. The idea here:
- Match the code content with a regex and split out the language and the code
- Call
shiki.codeToHtmlto highlight the code synchronously
function inlineCodeHighlightPlugin( md: MarkdownIt, _options: null ) { // get the shiki singleton instance const shiki = getShiki() const defaultRender = md.renderer.rules.code_inline || function (tokens, idx, _options, _env, self) { const token = tokens[idx] if (!token) return '' return ( '<code' + self.renderAttrs(token) + '>' + md.utils.escapeHtml(token.content) + '</code>' ) } const themeMap = { light: 'catppuccin-latte', dark: 'catppuccin-mocha', tokyo: 'tokyo-night' } md.renderer.rules.code_inline = function (tokens, idx, options, env, self) { const token = tokens[idx] if (!token) return '' const content = token.content.trim() // capture lang and code from `{lang} code` const match = content.match(/^\{(\w+)\}\s+(.+)$/) if (match === null) { return defaultRender(tokens, idx, options, env, self) } try { const highlighted = shiki.codeToHtml(match[2], { lang: match[1], themes: themeMap, structure: 'inline' }) return '<code' + self.renderAttrs(token) + '>' + highlighted + '</code>' } catch (e) { console.error('Highlighting failed', e) return defaultRender(tokens, idx, options, env, self) } }}export default inlineCodeHighlightPluginA markdown parser and compiler. Built for speed. markedjs/marked: A markdown parser and compiler. Built for speed. ↩︎
Markdown-it is a markdown parser, done right. A faster and CommonMark compliant alternative for Hexo. CHENXCHEN/hexo-renderer-markdown-it-plus. The plugin hasn’t been maintained for a while, but it’s simple and there isn’t much to develop anyway. If needed later, I may fork it or look for an alternative ↩︎
