Introducing Microlighter

Introducing Microlighter

I made a smol client-side syntax highlighter that uses the CSS Custom Highlights API called MicroLighter. I’ll talk about why I made it in a bit but first I wanted to… ahem… highlight… some of the features. 我制作了一个名为 MicroLighter 的小型客户端语法高亮工具,它使用了 CSS Custom Highlights API。稍后我会谈谈制作它的初衷,但首先我想……咳咳……“高亮”一下它的一些特性:

  • Zero-dependencies

  • ~2kb minifed+gzipped

  • Uses CSS ::highlight(token-name), not spans

  • Leverages Textmate’s language grammars

  • Human-readable light-dark() themes

  • All languages/grammars loaded on-demand

  • Moves all non-highlight functionality to a <micro-lighter> custom element

  • 零依赖

  • 压缩并 Gzip 后约 2kb

  • 使用 CSS ::highlight(token-name) 而非 <span> 标签

  • 利用 Textmate 的语言语法规则

  • 易于阅读的 light-dark() 主题

  • 所有语言/语法按需加载

  • 将所有非高亮功能移至 <micro-lighter> 自定义元素中

At some point I broke syntax highlighting on my Jekyll blog. I’ve used a handful of syntax highlighters over the years (Highlight.js, PrismJS, Rouge, Shiki, etc, etc) and I’ve felt the trade-offs between different client-side and server-side implementations. Faced with picking another, I knew I wanted to explore Bramus’s technique of syntax highlighting with the CSS Custom Highlights API. There’s some limitations with the CSS ::highlight() pseudo; no italics, no bold, no font swapping – but otherwise it’s pretty cool syntax to idiomatically express “I want to highlight this token” via CSS instead of injecting spans everywhere. 有段时间,我弄坏了 Jekyll 博客上的语法高亮功能。多年来我用过不少语法高亮工具(Highlight.js, PrismJS, Rouge, Shiki 等等),也体会到了不同客户端和服务端实现方案之间的权衡。在面临重新选择时,我知道我想尝试 Bramus 使用 CSS Custom Highlights API 进行语法高亮的技术。CSS ::highlight() 伪元素确实有一些局限性:不支持斜体、粗体或字体切换,但除此之外,它是一种非常酷的语法,可以通过 CSS 惯用地表达“我想高亮这个标记”,而无需到处注入 <span> 标签。

Using the Highlight API means I avoid any DOM mutation and the scope of the library shrinks down to: scan code blocks using regex patterns and send CSS.highlights.set(category, textRanges) to highlight the code blocks. I don’t need much syntax highlighting on this site. Not all posts have code and my code samples are a whole fifteen lines long at best. My struggle is that I swap languages often. I’ll do HTML, CSS, and JavaScript all in the same post. A little bash here, a little ruby there, some markdown as a treat. Using so all those languages grew the complexity beyond the limits of my regex-fu, so I decided to lean on Textmate’s established collections of patterns used by VS Code. And before I knew it, my little highlighter could do almost any language. 使用 Highlight API 意味着我避免了任何 DOM 变动,库的范围也缩小到了:使用正则表达式扫描代码块,并发送 CSS.highlights.set(category, textRanges) 来高亮代码块。我并不需要这个网站有太复杂的语法高亮。并非所有文章都有代码,而且我的代码示例最多也就十五行长。我的困扰在于我经常切换语言。我会在同一篇文章中同时使用 HTML、CSS 和 JavaScript,偶尔再加点 bash、ruby 或 markdown。使用这些语言使得复杂度超出了我正则表达式能力的极限,所以我决定依赖 VS Code 所使用的 Textmate 成熟的模式集合。不知不觉中,我的小高亮工具几乎可以处理任何语言了。

Knowing I use different languages often, one principle I established was that all language grammars should be auto-loaded on-demand to reduce configuration and bundle size. That way you only pay for what you use. Inspired by PrismJS’s simplified token categories I flattened down Textmate’s granular token categories to a more human-friendly set, making it easier to style. In addition to that, one enormous nit-pick I have with codeblock styling is that light and dark themes are separate entities, so I merged them into one theme using light-dark(). 鉴于我经常使用不同的语言,我确立的一个原则是:所有语言语法都应按需自动加载,以减少配置和包体积。这样你只需为你使用的部分付费。受 PrismJS 简化标记类别的启发,我将 Textmate 细粒度的标记类别扁平化为更易于理解的集合,从而使其更容易设置样式。此外,我对代码块样式的一个巨大不满是亮色和暗色主题通常是分开的,所以我使用 light-dark() 将它们合并为一个主题。

The last big opinion I baked in was that I wanted the syntax highlighter to do one job: infer language and highlight code in that language. With that a guideline, I moved all extra functionality (like line-numbers, etc) over to a web component. The vanilla web component adds about ~1 KiB in size, but co-locating UI into a UI primitive like native custom elements feels right and the ShadowDOM encapsulation makes it easy to separate the code from the presentational UI. Obviously, I’m a web component pervert but it feels like a great separation of concerns versus trying to cram everything into the core library. 我坚持的最后一个重要观点是:我希望语法高亮工具只做一件事:推断语言并高亮该语言的代码。以此为准则,我将所有额外功能(如行号等)移到了 Web Component 中。原生 Web Component 大约增加了 1 KiB 的体积,但将 UI 放置在像原生自定义元素这样的 UI 原语中感觉很对,而且 ShadowDOM 的封装使得将代码与展示层 UI 分离变得很容易。显然,我是个 Web Component 狂热者,但相比于试图把所有东西塞进核心库,这感觉是一种更好的关注点分离。

Try it out

试用一下

To get started on your site, I’d use the self-initializing minified bundle, but I’m also shipping ESM and a web component. 要在你的网站上开始使用,我建议使用自初始化的压缩包,但我同时也提供了 ESM 版本和 Web Component 版本。

<script type="module" src="path/to/microlighter/microlighter.min.js"></script>

Like I said above, not all my posts have syntax highlighting, so I even wait to import the script unless I know there’s a page with code on it. 正如我上面所说,并非我所有的文章都有语法高亮,所以我甚至会等到确定页面上有代码时才导入脚本。

if(document.querySelector('pre>code').length) {
  import('path/to/microlighter/microlighter.min.js');
}

You can use the ESM version if you’re going to do something fancy yourself: 如果你想做一些高级操作,可以使用 ESM 版本:

import { highlightAll } from 'microlighter'
highlightAll({ selector: 'pre.onlyTheseGetHighlights' })

And you can also use the web component if you want those extra features I talked about: 如果你想要我提到的那些额外功能,也可以使用 Web Component:

<micro-lighter data-syntax-theme="github" line-numbers controls="copy">
  <pre><code>Code goes here</code></pre>
</micro-lighter>

Web component classes are pretty extendible too so if I don’t support something you need, you can “fork” it by extending the base class and adding your own features. Web Component 类也非常易于扩展,所以如果我没有支持你所需的功能,你可以通过继承基类并添加自己的功能来“分叉”它。

Lastly, you can use one of the pre-supplied themes or roll your own. The basic structure is: 最后,你可以使用预设的主题之一,或者自己动手制作。基本结构如下:

/**
 * Setup semantic `--syntax-*` tokens
 * @value background | foreground | comment | keyword |
 * operator |string | constant | function | type | variable |
 * property | tag | selector | inserted | deleted
 */
[data-syntax-theme="my-theme-name"] {
  color-scheme: light dark;
  /* Code block tokens */
  --syntax-background: light-dark(#f8f8f8, #3a3a3a);
  --syntax-foreground: light-dark(#3a3a3a, #f8f8f8);
  /* Highlight tokens */
  --syntax-comment: light-dark(#6e7781, #8b949e);
  --syntax-function: light-dark(#8250df, #d2a8ff);
  /* ...etc... */
}

[data-syntax-theme="my-theme-name"] pre:has(code) {
  background-color:var(--syntax-background);
  color:var(--syntax-foreground)
}

::highlight(comment) { color: var(--syntax-comment) }
::highlight(function) { color: var(--syntax-function) }
/* ...etc... */

And that’s MicroLighter. If you end up using it and trying it out, let me know what you think. 这就是 MicroLighter。如果你最终使用了它并进行了尝试,请告诉我你的想法。