Plumbiu
banner
avatar
Plumbiu
这人很勤奋,啥都没留

转换 Shiki 的内联样式

Shiki 是一个基于 TextMate 语法的代码语法高亮器,它与 VS Code 的语法高亮引擎 onIguruma 一致,几乎所有主流编程语言提供非常准确且快速的语法高亮,然而 Shiki 并不关注于 CSS,它的语法高亮都是通过 HTML 的 style 属性实现的,这会导致很多样式无法得到复用,产生体积更大的 HTML 体积,本文实现内联样式类名的转换。

shiki-inline-styles

如果你有这方面的需求,可以使用 shiki-class-transformer,另外 shiki 也是有自己的转换器的,但是它是 js 动态产生 css 片段,并不是直接导入 css。

为什么是内联样式?

如果你曾经魔改过 VS Code 的主题的话,你应该有印象需要修改 scope 属性和 settings.foreground 等属性,我们举一个 tm-themes 仓库shiki 中的主题基于这个) 的一个简化例子:

{  "tokenColors": [    {      "scope": ["comment"],      "settings": {        "foreground": "#a0ada0"      }    },    {      "scope": ["constant"],      "settings": {        "foreground": "#999999"      }    }  ]}

如果你好奇为什么是 scopesettings 这种设计模式,你可以参考 vscode-textmate 官方的例子,在注释代码中可以看到打印结果。

可以看到代码颜色是基于 scope 字段决定的,而配置颜色的字段在 settings.forground 中,我在看到这段配置的时候,也是在想,为什么 settings 不能直接写一个类名?

原因之一在于有时候 scope 会过长,如果我们像 prism 一样,将类名分为 keywordvariable 等等,会导致类名比内联样式还长的情况:

{  "scope": [    "delimiter.bracket",    "delimiter",    "invalid.illegal.character-not-allowed-here.html",    "keyword.operator.rest",    "keyword.operator.spread",    "keyword.operator.type.annotation",    "keyword.operator.relational",    "keyword.operator.assignment",    "keyword.operator.type",    "meta.brace",    "meta.tag.block.any.html",    "meta.tag.inline.any.html",    "meta.tag.structure.input.void.html",    "meta.type.annotation",    "meta.embedded.block.github-actions-expression",    "storage.type.function.arrow",    "meta.objectliteral.ts",    "punctuation",    "punctuation.definition.string.begin.html.vue",    "punctuation.definition.string.end.html.vue"  ],  "settings": {    "foreground": "#999999"  }},

因此直接从 settings 中设置 style 属性是最方便的做法,但是我认为 shiki 可以做得更好,因为颜色可以一一对应一个类名,这样 shiki 就不需要内联样式了,这也是这篇文章实现的主要思路。

实现思路

之前讲过,我们可以将颜色对应为一个类名,结合 shikitransfomer 即可实现外联样式:

// 导入对应的 json 文件import vitesseDark from 'tm-themes/themes/vitesse-dark.json'import fs from 'node:fs'// 准备好前缀和后缀const PREFIX = 's'let suffix = 0function generateMap() {  // key 为颜色,value 为类名  const map = {}  for (const token of vitesseDark.tokenColors) {    const color = token?.settings?.foreground    if (color && !map[color]) {      map[color] = PREFIX + suffix      suffix++    }  }  fs.writeFileSync('./vitesse-dark.json', JSON.stringify(map))  return map}generateMap()

现在我们得到了一个 map,这个 map 会在后面的 transfomer 中使用:

{  "#a0ada0": "s0",  "#999999": "s1",  "#a65e2b": "s2",  "#59873a": "s3"  // ...}

接下来生成对应的 css 文件

function generateCss() {  const map = generateMap()  let style = ''  for (const [color, className] of Object.entries(map)) {    style += `      .${className}: {        color: ${color};      }    `  }  fs.writeFileSync('./vitesse-dark.css')}generateCss()

得到的文件:

.s0 {  color: #a0ada0;}.s1 {  color: #999999;}.s2 {  color: #a65e2b;}.s3 {  color: #59873a;}/* ... */

接下来书写 transformer

完整代码请看 shiki-class-transformer,这里实现的只是最基本的,很多情况没有考虑。

// map 为之前我们生成的 json 文件function shikiClassTransformer({ map }) {  return {    tokens(tokens) {      for (const items of tokens) {        for (const token of items) {          let htmlStyle = token.htmlStyle          if (!htmlStyle || typeof htmlStyle === 'string') {            return          }          // shiki 默认情况下会使用 color 作为颜色,例如 style="color:#fff"          const className = map[htmlStyle.color]          // map 中没有对应类名,删除掉          if (!className) {            continue          }          // 保证 token.htmlAttrs 不为空          if (!token.htmlAttrs) {            token.htmlAttrs = {}          }          // 设置类名          token.htmlAttrs.class = className          // 将原有 style.color 删除          // 如果 htmlStyle 为 {} 空对象,不会产生内联样式          delete token.htmlStyle['color']        }      }    },  }}

大功告成(你可以开发者面板看一下我博客里的类名):

shiki-className

评论区
CC BY-NC-SA 4.0 2024 © Plumbiu