Repository: shikijs/shiki
Pull request #835 (2024-11-14 07:33:51)

feat(transformers): introduce `matchAlgorithm` option for new matching algorithm
<!-- DO NOT IGNORE THE TEMPLATE!

Thank you for contributing!

Before submitting the PR, please make sure you do the following:

- Read the [Contributing Guide](https://github.com/shikijs/shiki/blob/main/CONTRIBUTING.md).
- Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
- Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`).
- Ideally, include relevant tests that fail without this PR but pass with it.

-->

### Description

This PR aims to redesign notation transformer utility and its usage to improve performance and make it more intuitive.

#### Changes

- Parse comments separately and cache it into the `data` property of `code` nodes.
- Notation transformers read and replace parsed comments.
- Update original token, remove empty comment tokens and lines if necessary.

#### New Syntax

```js
console.log("hello") // [!code highlight]

console.log("highlighted") // [!code highlight:2]
console.log("highlighted")

// the comment below will be removed
// [!code highlight:2]
console.log("highlighted")
console.log("highlighted")

console.log("combine") // [!code focus] [!code highlight]
console.log("combine") // Comment Allowed [!code focus] [!code highlight]
```

#### JSX

```jsx
<div>
    <p>Hello World</p>
    {/* [!code highlight] */}
    <p>Highlighted</p>
</div>
```

#### Breaking

It only looks for the last few tokens of line to scan comments:

```jsx
console.log("// [!code highlight]"); // will not work
```

It doesn't need multiple `//` to chain notations:

```ts
console.log("test") // [!code highlight] // [!code ++]

// will cause weird result like:
console.log("test") // //
```

#### Edge cases

for usage like:

```ts
// some text [!code highlight:2]
console.log("highlighted")
console.log("highlighted")
```

line 2-3 will be highlighted, but the original comment is kept.

#### Backward Compatibility

Added a `legacy` option to force legacy behaviors:

```ts
transformerNotationFocus({ legacy: true })
transformerNotationHighlight({ legacy: true })
transformerNotationErrorLevel({ legacy: true })
```

### Linked Issues

#619 #770 

### Additional context

this PR will introduce breaking changes, I currently made a separate package to avoid the problems. Backward compatibility is possible (edit: supported), but it takes extra care and further discussion.

code implementations:
- the `createCommentNotationTransformer` parses comment with `parseComments` and cache the result.
- each parsed comment object contains related hast nodes, and `info`:
  ```
      /* hello world */
   |----|-----------|-|
   prefix   content  suffix
  ```
- remove nodes when `content` became empty.

- transformers using `createCommentNotationTransformer` still provides a regex, but the regex no longer need to match the comment

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *shiki-matsu* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 2456510e0fb6a12bcd75d7f976165020b0586e80 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/shiki-matsu/deploys/6735a7e1615c8b000880e7d8 |
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *shiki-next* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 2456510e0fb6a12bcd75d7f976165020b0586e80 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/shiki-next/deploys/6735a7e1887233000884be92 |
fuma-nama: I kept the failing unit tests to experiment about some changes for backward compatibility
Fro-Q: This is what I'm looking for! Are you still working on it ? 'cause I actually need it so bad :>
fuma-nama: See https://github.com/fuma-nama/shiki-transformers
antfu: Hey @fuma-nama, thanks for the awesome work and sorry for the long due. In other to process this PR, we have to first make it non-breaking and rolling out it progressively. I have push some commits to introduce the `matchAlgorithm` which will default to the old behaviour, and we will rolling it by default in v3. I also wrote some guide for people to migrate or opt-in, if I messed anything feel free to push and improve it. Thanks

Patch:
diff --git a/docs/packages/transformers.md b/docs/packages/transformers.md
index f1106e7b..151ff589 100644
--- a/docs/packages/transformers.md
+++ b/docs/packages/transformers.md
@@ -41,6 +41,44 @@ const html = await codeToHtml(code, {
 
 Transformers only applies classes and does not come with styles; you can provide your own CSS rules to style them properly.
 
+## Matching Algorithm
+
+We found that the algorithm for matching comments in v1 is sometime conterintuitive, where we are trying to fix it in a progressive way. Since v1.29.0, we introduced a new `matchAlgorithm` option to most of the transformer for you to toggle between different matching algorithms. Right now, the default is `v1` which is the old algorithm, and `v3` is the new algorithm. When Shiki v3 is landed, the default will be `v3`.
+
+```ts
+const html = await codeToHtml(code, {
+  lang: 'ts',
+  theme: 'nord',
+  transformers: [
+    transformerNotationDiff({
+      matchAlgorithm: 'v3', // [!code hl]
+    }),
+  ],
+})
+```
+
+### `matchAlgorithm: 'v1'`
+
+The matching algorithm mostly affects the single-line comment matching, in `v1`, it will count the comment line as the first line, while in `v3`, it will count start from the comment line:
+
+```ts
+// [\!code highlight:3]
+console.log('highlighted') // [!code hl]
+console.log('highlighted') // [!code hl]
+console.log('not highlighted')
+```
+
+### `matchAlgorithm: 'v3'`
+
+In `v3`, the matching algorithm will start counting from the line below the comment:
+
+```ts
+// [\!code highlight:2]
+console.log('highlighted') // [!code hl]
+console.log('highlighted') // [!code hl]
+console.log('not highlighted')
+```
+
 ## Transformers
 
 ### `transformerNotationDiff`
diff --git a/packages/transformers/src/index.ts b/packages/transformers/src/index.ts
index 05ed4d21..306724f7 100644
--- a/packages/transformers/src/index.ts
+++ b/packages/transformers/src/index.ts
@@ -11,4 +11,3 @@ export * from './transformers/remove-line-breaks'
 export * from './transformers/remove-notation-escape'
 export * from './transformers/render-whitespace'
 export * from './transformers/style-to-class'
-export * from './utils'
diff --git a/packages/transformers/src/shared/highlight-word.ts b/packages/transformers/src/shared/highlight-word.ts
index 7aef3b04..9f9b1ffe 100644
--- a/packages/transformers/src/shared/highlight-word.ts
+++ b/packages/transformers/src/shared/highlight-word.ts
@@ -24,6 +24,7 @@ function getTextContent(element: ElementContent): string {
  * @param ignoredElement
  * @param index highlight beginning index
  * @param len highlight length
+ * @param className class name to add to highlighted nodes
  */
 function highlightRange(
   this: ShikiTransformerContext,
@@ -64,14 +65,14 @@ function highlightRange(
   }
 }
 
-function hasOverlap(range1: [number, number], range2: [ number, number]): boolean {
+function hasOverlap(range1: [number, number], range2: [number, number]): boolean {
   return (range1[0] <= range2[1]) && (range1[1]) >= range2[0]
 }
 
 function separateToken(span: Element, textNode: Text, index: number, len: number): [
-  before: Element | undefined,
-  med: Element,
-  after: Element | undefined,
+    before: Element | undefined,
+    med: Element,
+    after: Element | undefined,
 ] {
   const text = textNode.value
 
diff --git a/packages/transformers/src/shared/notation-transformer.ts b/packages/transformers/src/shared/notation-transformer.ts
new file mode 100644
index 00000000..b6bef717
--- /dev/null
+++ b/packages/transformers/src/shared/notation-transformer.ts
@@ -0,0 +1,97 @@
+import type { Element, Text } from 'hast'
+import type { ShikiTransformer, ShikiTransformerContext } from 'shiki'
+import { parseComments, type ParsedComments, v1ClearEndCommentPrefix } from './parse-comments'
+
+export type MatchAlgorithm = 'v1' | 'v3'
+
+export interface MatchAlgorithmOptions {
+  /**
+   * Match algorithm to use
+   *
+   * @see https://shiki.style/packages/transformers#matching-algorithm
+   * @default 'v1'
+   */
+  matchAlgorithm?: MatchAlgorithm
+}
+
+export function createCommentNotationTransformer(
+  name: string,
+  regex: RegExp,
+  onMatch: (
+    this: ShikiTransformerContext,
+    match: string[],
+    line: Element,
+    commentNode: Element,
+    lines: Element[],
+    index: number
+  ) => boolean,
+  matchAlgorithm: MatchAlgorithm = 'v1',
+): ShikiTransformer {
+  return {
+    name,
+    code(code) {
+      const lines = code.children.filter(i => i.type === 'element')
+      const linesToRemove: (Element | Text)[] = []
+
+      code.data ??= {} as any
+      const data = code.data as {
+        _shiki_notation?: ParsedComments
+      }
+
+      data._shiki_notation ??= parseComments(lines, ['jsx', 'tsx'].includes(this.options.lang), matchAlgorithm)
+      const parsed = data._shiki_notation
+
+      for (const comment of parsed) {
+        if (comment.info[1].length === 0)
+          continue
+
+        const isLineCommentOnly = comment.line.children.length === (comment.isJsxStyle ? 3 : 1)
+        let lineIdx = lines.indexOf(comment.line)
+        if (isLineCommentOnly && matchAlgorithm !== 'v1')
+          lineIdx++
+
+        let replaced = false
+        comment.info[1] = comment.info[1].replace(regex, (...match) => {
+          if (onMatch.call(this, match, comment.line, comment.token, lines, lineIdx)) {
+            replaced = true
+            return ''
+          }
+
+          return match[0]
+        })
+
+        if (!replaced)
+          continue
+
+        if (matchAlgorithm === 'v1') {
+          comment.info[1] = v1ClearEndCommentPrefix(comment.info[1])
+        }
+
+        const isEmpty = comment.info[1].trim().length === 0
+        // ignore comment node
+        if (isEmpty)
+          comment.info[1] = ''
+
+        if (isEmpty && isLineCommentOnly) {
+          linesToRemove.push(comment.line)
+        }
+        else if (isEmpty && comment.isJsxStyle) {
+          comment.line.children.splice(comment.line.children.indexOf(comment.token) - 1, 3)
+        }
+        else if (isEmpty) {
+          comment.line.children.splice(comment.line.children.indexOf(comment.token), 1)
+        }
+        else {
+          const head = comment.token.children[0]
+
+          if (head.type === 'text') {
+            head.value = comment.info.join('')
+          }
+        }
+      }
+
+      for (const line of linesToRemove)
+        code.children.splice(code.children.indexOf(line), 1)
+    },
+  }
+}
diff --git a/packages/transformers/src/shared/parse-comments.ts b/packages/transformers/src/shared/parse-comments.ts
new file mode 100644
index 00000000..fcc80996
--- /dev/null
+++ b/packages/transformers/src/shared/parse-comments.ts
@@ -0,0 +1,134 @@
+import type { Element, ElementContent } from 'hast'
+import type { MatchAlgorithm } from './notation-transformer'
+
+export type ParsedComments = {
+  line: Element
+  token: Element
+  info: [prefix: string, content: string, suffix?: string]
+  isJsxStyle: boolean
+}[]
+
+/**
+ * some comment formats have to be located at the end of line
+ * hence we can skip matching them for other tokens
+ */
+const matchers: [re: RegExp, endOfLine: boolean][] = [
+  [/^(<!--)(.+)(-->)$/, false],
+  [/^(\/\*)(.+)(\*\/)$/, false],
+  [/^(\/\/|["'#]|;{1,2}|%{1,2}|--)(.*)$/, true],
+  /**
+   * for multi-line comments like this
+   */
+  [/^(\*)(.+)$/, true],
+]
+
+/**
+ * @param lines line tokens
+ * @param jsx enable JSX parsing
+ * @param matchAlgorithm matching algorithm
+ */
+export function parseComments(
+  lines: Element[],
+  jsx: boolean,
+  matchAlgorithm: MatchAlgorithm,
+): ParsedComments {
+  const out: ParsedComments = []
+
+  for (const line of lines) {
+    const elements = line.children
+    let start = elements.length - 1
+    if (matchAlgorithm === 'v1')
+      start = 0
+    else if (jsx)
+      // one step further for JSX as comment is inside curly brackets
+      start = elements.length - 2
+
+    for (let i = Math.max(start, 0); i < elements.length; i++) {
+      const token = elements[i]
+      if (token.type !== 'element')
+        continue
+      const head = token.children.at(0)
+      if (head?.type !== 'text')
+        continue
+
+      const isLast = i === elements.length - 1
+      const match = matchToken(head.value, isLast)
+      if (!match)
+        continue
+
+      if (jsx && !isLast && i !== 0) {
+        out.push({
+          info: match,
+          line,
+          token,
+          isJsxStyle: isValue(elements[i - 1], '{') && isValue(elements[i + 1], '}'),
+        })
+      }
+      else {
+        out.push({
+          info: match,
+          line,
+          token,
+          isJsxStyle: false,
+        })
+      }
+    }
+  }
+
+  return out
+}
+
+function isValue(element: ElementContent, value: string): boolean {
+  if (element.type !== 'element')
+    return false
+  const text = element.children[0]
+  if (text.type !== 'text')
+    return false
+
+  return text.value.trim() === value
+}
+
+/**
+ * @param text text value of comment node
+ * @param isLast whether the token is located at the end of line
+ */
+function matchToken(text: string, isLast: boolean): [prefix: string, content: string, suffix?: string] | undefined {
+  // no leading and trailing spaces allowed for matchers
+  // we extract the spaces
+  let trimmed = text.trimStart()
+  const spaceFront = text.length - trimmed.length
+
+  trimmed = trimmed.trimEnd()
+  const spaceEnd = text.length - trimmed.length - spaceFront
+
+  for (const [matcher, endOfLine] of matchers) {
+    if (endOfLine && !isLast)
+      continue
+
+    const result = matcher.exec(trimmed)
+    if (!result)
+      continue
+
+    return [
+      ' '.repeat(spaceFront) + result[1],
+      result[2],
+      result[3] ? result[3] + ' '.repeat(spaceEnd) : undefined,
+    ]
+  }
+}
+
+/**
+ * Remove empty comment prefixes at line end, e.g. `// `
+ *
+ * For matchAlgorithm v1
+ */
+export function v1ClearEndCommentPrefix(text: string): string {
+  const regex = /(?:\/\/|["'#]|;{1,2}|%{1,2}|--)(.*)$/
+  const result = regex.exec(text)
+
+  if (result && result[1].trim().length === 0) {
+    return text.slice(0, result.index)
+  }
+
+  return text
+}
diff --git a/packages/transformers/src/transformers/meta-highlight.ts b/packages/transformers/src/transformers/meta-highlight.ts
index cc5c7ae0..0c6037e0 100644
--- a/packages/transformers/src/transformers/meta-highlight.ts
+++ b/packages/transformers/src/transformers/meta-highlight.ts
@@ -12,8 +12,7 @@ export function parseMetaHighlightString(meta: string): number[] | null {
       const num = v.split('-').map(v => Number.parseInt(v, 10))
       if (num.length === 1)
         return [num[0]]
-      else
-        return Array.from({ length: num[1] - num[0] + 1 }, (_, i) => i + num[0])
+      return Array.from({ length: num[1] - num[0] + 1 }, (_, i) => i + num[0])
     })
   return lines
 }
@@ -45,8 +44,13 @@ export function transformerMetaHighlight(
       if (!this.options.meta?.__raw) {
         return
       }
-      ;(this.meta as any)[symbol] ||= parseMetaHighlightString(this.options.meta.__raw)
-      const lines: number[] = (this.meta as any)[symbol] || []
+      const meta = this.meta as {
+        [symbol]: number[] | null
+      }
+
+      meta[symbol] ??= parseMetaHighlightString(this.options.meta.__raw)
+      const lines: number[] = meta[symbol] ?? []
+
       if (lines.includes(line))
         this.addClassToHast(node, className)
       return node
diff --git a/packages/transformers/src/transformers/notation-diff.ts b/packages/transformers/src/transformers/notation-diff.ts
index b21f9b22..d3907ff5 100644
--- a/packages/transformers/src/transformers/notation-diff.ts
+++ b/packages/transformers/src/transformers/notation-diff.ts
@@ -1,7 +1,8 @@
 import type { ShikiTransformer } from 'shiki'
+import type { MatchAlgorithmOptions } from '../shared/notation-transformer'
 import { transformerNotationMap } from './notation-map'
 
-export interface TransformerNotationDiffOptions {
+export interface TransformerNotationDiffOptions extends MatchAlgorithmOptions {
   /**
    * Class for added lines
    */
@@ -35,6 +36,7 @@ export function transformerNotationDiff(
         '--': classLineRemove,
       },
       classActivePre,
+      matchAlgorithm: options.matchAlgorithm,
     },
     '@shikijs/transformers:notation-diff',
   )
diff --git a/packages/transformers/src/transformers/notation-error-level.ts b/packages/transformers/src/transformers/notation-error-level.ts
index 03a514f8..9764d712 100644
--- a/packages/transformers/src/transformers/notation-error-level.ts
+++ b/packages/transformers/src/transformers/notation-error-level.ts
@@ -1,7 +1,8 @@
 import type { ShikiTransformer } from 'shiki'
+import type { MatchAlgorithmOptions } from '../shared/notation-transformer'
 import { transformerNotationMap } from './notation-map'
 
-export interface TransformerNotationErrorLevelOptions {
+export interface TransformerNotationErrorLevelOptions extends MatchAlgorithmOptions {
   classMap?: Record<string, string | string[]>
   /**
    * Class added to the <pre> element when the current code has diff
@@ -27,6 +28,7 @@ export function transformerNotationErrorLevel(
     {
       classMap,
       classActivePre,
+      matchAlgorithm: options.matchAlgorithm,
     },
     '@shikijs/transformers:notation-error-level',
   )
diff --git a/packages/transformers/src/transformers/notation-focus.ts b/packages/transformers/src/transformers/notation-focus.ts
index 1af4dce1..74a02d9b 100644
--- a/packages/transformers/src/transformers/notation-focus.ts
+++ b/packages/transformers/src/transformers/notation-focus.ts
@@ -1,7 +1,8 @@
 import type { ShikiTransformer } from 'shiki'
+import type { MatchAlgorithmOptions } from '../shared/notation-transformer'
 import { transformerNotationMap } from './notation-map'
 
-export interface TransformerNotationFocusOptions {
+export interface TransformerNotationFocusOptions extends MatchAlgorithmOptions {
   /**
    * Class for focused lines
    */
@@ -29,6 +30,7 @@ export function transformerNotationFocus(
         focus: classActiveLine,
       },
       classActivePre,
+      matchAlgorithm: options.matchAlgorithm,
     },
     '@shikijs/transformers:notation-focus',
   )
diff --git a/packages/transformers/src/transformers/notation-highlight-word.ts b/packages/transformers/src/transformers/notation-highlight-word.ts
index 1e5cc5cb..43b50592 100644
--- a/packages/transformers/src/transformers/notation-highlight-word.ts
+++ b/packages/transformers/src/transformers/notation-highlight-word.ts
@@ -1,8 +1,9 @@
 import type { ShikiTransformer } from 'shiki'
+import type { MatchAlgorithmOptions } from '../shared/notation-transformer'
 import { highlightWordInLine } from '../shared/highlight-word'
-import { createCommentNotationTransformer } from '../utils'
+import { createCommentNotationTransformer } from '../shared/notation-transformer'
 
-export interface TransformerNotationWordHighlightOptions {
+export interface TransformerNotationWordHighlightOptions extends MatchAlgorithmOptions {
   /**
    * Class for highlighted words
    */
@@ -23,23 +24,20 @@ export function transformerNotationWordHighlight(
 
   return createCommentNotationTransformer(
     '@shikijs/transformers:notation-highlight-word',
-    // comment-start             | marker    | word           | range | comment-end
-    /^\s*(?:\/\/|\/\*|<!--|#)\s+\[!code word:((?:\\.|[^:\]])+)(:\d+)?\]\s*(?:\*\/|-->)?/,
+    /\s*\[!code word:((?:\\.|[^:\]])+)(:\d+)?\]/,
     function ([_, word, range], _line, comment, lines, index) {
       const lineNum = range ? Number.parseInt(range.slice(1), 10) : lines.length
 
       // escape backslashes
       word = word.replace(/\\(.)/g, '$1')
-
-      lines
-        // Don't include the comment itself
-        .slice(index + 1, index + 1 + lineNum)
-        .forEach(line => highlightWordInLine.call(this, line, comment, word, classActiveWord))
+      for (let i = index; i < Math.min(index + lineNum, lines.length); i++) {
+        highlightWordInLine.call(this, lines[i], comment, word, classActiveWord)
+      }
 
       if (classActivePre)
         this.addClassToHast(this.pre, classActivePre)
       return true
     },
-    true, // remove empty lines
+    options.matchAlgorithm,
   )
 }
diff --git a/packages/transformers/src/transformers/notation-highlight.ts b/packages/transformers/src/transformers/notation-highlight.ts
index f0287971..b8e6bca7 100644
--- a/packages/transformers/src/transformers/notation-highlight.ts
+++ b/packages/transformers/src/transformers/notation-highlight.ts
@@ -1,7 +1,8 @@
 import type { ShikiTransformer } from 'shiki'
+import type { MatchAlgorithmOptions } from '../shared/notation-transformer'
 import { transformerNotationMap } from './notation-map'
 
-export interface TransformerNotationHighlightOptions {
+export interface TransformerNotationHighlightOptions extends MatchAlgorithmOptions {
   /**
    * Class for highlighted lines
    */
@@ -30,6 +31,7 @@ export function transformerNotationHighlight(
         hl: classActiveLine,
       },
       classActivePre,
+      matchAlgorithm: options.matchAlgorithm,
     },
     '@shikijs/transformers:notation-highlight',
   )
diff --git a/packages/transformers/src/transformers/notation-map.ts b/packages/transformers/src/transformers/notation-map.ts
index 728f7e76..c2269008 100644
--- a/packages/transformers/src/transformers/notation-map.ts
+++ b/packages/transformers/src/transformers/notation-map.ts
@@ -1,7 +1,8 @@
 import type { ShikiTransformer } from 'shiki'
-import { createCommentNotationTransformer } from '../utils'
+import type { MatchAlgorithmOptions } from '../shared/notation-transformer'
+import { createCommentNotationTransformer } from '../shared/notation-transformer'
 
-export interface TransformerNotationMapOptions {
+export interface TransformerNotationMapOptions extends MatchAlgorithmOptions {
   classMap?: Record<string, string | string[]>
   /**
    * Class added to the <pre> element when the current code has diff
@@ -24,17 +25,18 @@ export function transformerNotationMap(
 
   return createCommentNotationTransformer(
     name,
-    new RegExp(`\\s*(?://|/\\*|<!--|#|--|%{1,2}|;{1,2}|"|')\\s+\\[!code (${Object.keys(classMap).map(escapeRegExp).join('|')})(:\\d+)?\\]\\s*(?:\\*/|-->)?\\s*$`),
+    new RegExp(`\\s*\\[!code (${Object.keys(classMap).map(escapeRegExp).join('|')})(:\\d+)?\\]`),
     function ([_, match, range = ':1'], _line, _comment, lines, index) {
       const lineNum = Number.parseInt(range.slice(1), 10)
-      lines
-        .slice(index, index + lineNum)
-        .forEach((line) => {
-          this.addClassToHast(line, classMap[match])
-        })
+
+      for (let i = index; i < Math.min(index + lineNum, lines.length); i++) {
+        this.addClassToHast(lines[i], classMap[match])
+      }
+
       if (classActivePre)
         this.addClassToHast(this.pre, classActivePre)
       return true
     },
+    options.matchAlgorithm,
   )
 }
diff --git a/packages/transformers/src/utils.ts b/packages/transformers/src/utils.ts
deleted file mode 100644
index 2095b9eb..00000000
--- a/packages/transformers/src/utils.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import type { Element, Text } from 'hast'
-import type { ShikiTransformer, ShikiTransformerContext } from 'shiki'
-
-export function createCommentNotationTransformer(
-  name: string,
-  regex: RegExp,
-  onMatch: (
-    this: ShikiTransformerContext,
-    match: string[],
-    line: Element,
-    commentNode: Element,
-    lines: Element[],
-    index: number,
-  ) => boolean,
-  removeEmptyLines = false,
-): ShikiTransformer {
-  return {
-    name,
-    code(code) {
-      const lines = code.children.filter(i => i.type === 'element') as Element[]
-      const linesToRemove: (Element | Text)[] = []
-      lines.forEach((line, idx) => {
-        let nodeToRemove: Element | undefined
-
-        for (const child of line.children) {
-          if (child.type !== 'element')
-            continue
-          const text = child.children[0]
-          if (text.type !== 'text')
-            continue
-
-          let replaced = false
-          text.value = text.value.replace(regex, (...match) => {
-            if (onMatch.call(this, match, line, child, lines, idx)) {
-              replaced = true
-              return ''
-            }
-            return match[0]
-          })
-          if (replaced && !text.value.trim())
-            nodeToRemove = child
-        }
-
-        if (nodeToRemove) {
-          line.children.splice(line.children.indexOf(nodeToRemove), 1)
-
-          // Remove if empty
-          if (line.children.length === 0) {
-            linesToRemove.push(line)
-            if (removeEmptyLines) {
-              const next = code.children[code.children.indexOf(line) + 1]
-              if (next && next.type === 'text' && next.value === '\n')
-                linesToRemove.push(next)
-            }
-          }
-        }
-      })
-
-      for (const line of linesToRemove)
-        code.children.splice(code.children.indexOf(line), 1)
-    },
-  }
-}


Test patch:
diff --git a/packages/transformers/test/fixtures.test.ts b/packages/transformers/test/fixtures.test.ts
index bc3ef5de..ffd20191 100644
--- a/packages/transformers/test/fixtures.test.ts
+++ b/packages/transformers/test/fixtures.test.ts
@@ -61,7 +61,10 @@ body { margin: 0; }
 suite(
   'diff',
   import.meta.glob('./fixtures/diff/*.*', { query: '?raw', import: 'default', eager: true }),
-  [transformerNotationDiff(), transformerRemoveLineBreak()],
+  [
+    transformerNotationDiff(),
+    transformerRemoveLineBreak(),
+  ],
   code => `${code}
 <style>
 body { margin: 0; }
@@ -79,7 +82,10 @@ body { margin: 0; }
 suite(
   'focus',
   import.meta.glob('./fixtures/focus/*.*', { query: '?raw', import: 'default', eager: true }),
-  [transformerNotationFocus(), transformerRemoveLineBreak()],
+  [
+    transformerNotationFocus(),
+    transformerRemoveLineBreak(),
+  ],
   code => `${code}
 <style>
 body { margin: 0; }
@@ -92,7 +98,10 @@ body { margin: 0; }
 suite(
   'highlight',
   import.meta.glob('./fixtures/highlight/*.*', { query: '?raw', import: 'default', eager: true }),
-  [transformerNotationHighlight(), transformerRemoveLineBreak()],
+  [
+    transformerNotationHighlight(),
+    transformerRemoveLineBreak(),
+  ],
   code => `${code}
 <style>
 body { margin: 0; }
@@ -105,7 +114,10 @@ body { margin: 0; }
 suite(
   'highlight-word',
   import.meta.glob('./fixtures/highlight-word/*.*', { query: '?raw', import: 'default', eager: true }),
-  [transformerNotationWordHighlight(), transformerRemoveLineBreak()],
+  [
+    transformerNotationWordHighlight(),
+    transformerRemoveLineBreak(),
+  ],
   code => `${code}
 <style>
 body { margin: 0; }
@@ -118,7 +130,10 @@ body { margin: 0; }
 suite(
   'error-level',
   import.meta.glob('./fixtures/error-level/*.*', { query: '?raw', import: 'default', eager: true }),
-  [transformerNotationErrorLevel(), transformerRemoveLineBreak()],
+  [
+    transformerNotationErrorLevel(),
+    transformerRemoveLineBreak(),
+  ],
   code => `${code}
 <style>
 body { margin: 0; }
@@ -132,7 +147,9 @@ body { margin: 0; }
 suite(
   'whitespace:all',
   import.meta.glob('./fixtures/whitespace/*.*', { query: '?raw', import: 'default', eager: true }),
-  [transformerRenderWhitespace({ position: 'all' })],
+  [
+    transformerRenderWhitespace({ position: 'all' }),
+  ],
   code => `${code}${CSS_RENDER_WHITESPACE}`,
   '.all',
 )
@@ -140,7 +157,9 @@ suite(
 suite(
   'whitespace:boundary',
   import.meta.glob('./fixtures/whitespace/*.*', { query: '?raw', import: 'default', eager: true }),
-  [transformerRenderWhitespace({ position: 'boundary' })],
+  [
+    transformerRenderWhitespace({ position: 'boundary' }),
+  ],
   code => `${code}${CSS_RENDER_WHITESPACE}`,
   '.boundary',
 )
@@ -148,7 +167,9 @@ suite(
 suite(
   'whitespace:trailing',
   import.meta.glob('./fixtures/whitespace/*.*', { query: '?raw', import: 'default', eager: true }),
-  [transformerRenderWhitespace({ position: 'trailing' })],
+  [
+    transformerRenderWhitespace({ position: 'trailing' }),
+  ],
   code => `${code}${CSS_RENDER_WHITESPACE}`,
   '.trailing',
 )
@@ -185,3 +206,41 @@ body { margin: 0; }
 .space::before { content: "\\B7"; position: absolute; opacity: 0.3; }
 </style>`,
 )
+
+const CSS_COMPARE = `<style>
+* { tab-size: 4; }
+body { margin: 0; }
+.line { display: block; width: 100%; height: 1.2em; }
+.shiki { padding: 1em; }
+.has-focused .focused { background-color: #8805; }
+.highlighted { background-color: #8885; }
+.highlighted-word { background-color: #8885; }
+.highlighted.warning { background-color: #9905; }
+.highlighted.error { background-color: #8005; }
+</style>`
+
+suite(
+  'compare-v1',
+  import.meta.glob('./fixtures/match-algorithm/v1.*', { query: '?raw', import: 'default', eager: true }),
+  [
+    transformerNotationFocus({ matchAlgorithm: 'v1' }),
+    transformerNotationHighlight({ matchAlgorithm: 'v1' }),
+    transformerNotationErrorLevel({ matchAlgorithm: 'v1' }),
+    transformerNotationWordHighlight({ matchAlgorithm: 'v1' }),
+    transformerRemoveLineBreak(),
+  ],
+  code => `${code}${CSS_COMPARE}`,
+)
+
+suite(
+  'compare-v3',
+  import.meta.glob('./fixtures/match-algorithm/v3.*', { query: '?raw', import: 'default', eager: true }),
+  [
+    transformerNotationFocus({ matchAlgorithm: 'v3' }),
+    transformerNotationHighlight({ matchAlgorithm: 'v3' }),
+    transformerNotationErrorLevel({ matchAlgorithm: 'v3' }),
+    transformerNotationWordHighlight({ matchAlgorithm: 'v3' }),
+    transformerRemoveLineBreak(),
+  ],
+  code => `${code}${CSS_COMPARE}`,
+)
diff --git a/packages/transformers/test/fixtures/all/a.js b/packages/transformers/test/fixtures/all/a.js
index d00d765c..1f4f6e8b 100644
--- a/packages/transformers/test/fixtures/all/a.js
+++ b/packages/transformers/test/fixtures/all/a.js
@@ -1,5 +1,5 @@
 function hello(indentSize, type) {
   if (indentSize === 4 && type !== 'tab') {
-    	console.log('Each next indentation will increase on 4 spaces'); // [!code error] // [!code focus]
+    	console.log('Each next indentation will increase on 4 spaces'); // [!code error] [!code focus]
   }
 }
diff --git a/packages/transformers/test/fixtures/highlight-word/complex.js.output.html b/packages/transformers/test/fixtures/highlight-word/complex.js.output.html
index 7ce28d97..5233e7c2 100644
--- a/packages/transformers/test/fixtures/highlight-word/complex.js.output.html
+++ b/packages/transformers/test/fixtures/highlight-word/complex.js.output.html
@@ -1,4 +1,4 @@
-<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> transformerNotationFocus</span><span style="color:#E1E4E8">(</span></span><span class="line"><span style="color:#FFAB70">  options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}, </span></span><span class="line"><span style="color:#E1E4E8">) {</span></span><span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> options</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'options'</span></span><span class="line"><span style="color:#E1E4E8">  </span><span style="color:#E1E4E8" class="highlighted-word">console.</span><span style="color:#B392F0" class="highlighted-word">log</span><span style="color:#E1E4E8">(options)</span></span><span class="line"><span style="color:#E1E4E8">  </span><span style="color:#E1E4E8" class="highlighted-word">options.a</span><span style="color:#E1E4E8"> </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span></span><span class="line"><span style="color:#E1E4E8">  console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'// [!code word:</span><span style="color:#9ECBFF" class="highlighted-word">options.a</span><span style="color:#9ECBFF">]'</span><span style="color:#E1E4E8">)</span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span></code></pre>
+<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> transformerNotationFocus</span><span style="color:#E1E4E8">(</span></span><span class="line"><span style="color:#FFAB70">  options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}, </span></span><span class="line"><span style="color:#E1E4E8">) {</span></span><span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> options</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'options'</span></span><span class="line"><span style="color:#E1E4E8">  console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(options)</span></span><span class="line"><span style="color:#E1E4E8">  </span><span style="color:#E1E4E8" class="highlighted-word">options.a</span><span style="color:#E1E4E8"> </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span></span><span class="line"><span style="color:#E1E4E8">  console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'// [!code word:</span><span style="color:#9ECBFF" class="highlighted-word">options.a</span><span style="color:#9ECBFF">]'</span><span style="color:#E1E4E8">)</span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span></code></pre>
 <style>
 body { margin: 0; }
 .shiki { padding: 1em; }
diff --git a/packages/transformers/test/fixtures/highlight-word/mutliple-words.js.output.html b/packages/transformers/test/fixtures/highlight-word/mutliple-words.js.output.html
index 40b9b337..f9234001 100644
--- a/packages/transformers/test/fixtures/highlight-word/mutliple-words.js.output.html
+++ b/packages/transformers/test/fixtures/highlight-word/mutliple-words.js.output.html
@@ -1,4 +1,4 @@
-<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> transformerNotationFocus</span><span style="color:#E1E4E8">(</span></span><span class="line"><span style="color:#FFAB70">  </span><span style="color:#FFAB70" class="highlighted-word">options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}, </span></span><span class="line"><span style="color:#E1E4E8">) {</span></span><span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> </span><span style="color:#79B8FF" class="highlighted-word">options</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> '</span><span style="color:#9ECBFF" class="highlighted-word">options</span><span style="color:#9ECBFF">'</span></span><span class="line"><span style="color:#E1E4E8">  console.</span><span style="color:#B392F0" class="highlighted-word">log</span><span style="color:#E1E4E8">(options)</span></span><span class="line"><span style="color:#E1E4E8">  options.a </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span><span style="color:#6A737D"> // should not be highlighted</span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span></code></pre>
+<pre class="shiki github-dark" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">export</span><span style="color:#F97583"> function</span><span style="color:#B392F0"> transformerNotationFocus</span><span style="color:#E1E4E8">(</span></span><span class="line"><span style="color:#FFAB70">  </span><span style="color:#FFAB70" class="highlighted-word">options</span><span style="color:#F97583"> =</span><span style="color:#E1E4E8"> {}, </span></span><span class="line"><span style="color:#E1E4E8">) {</span></span><span class="line"><span style="color:#F97583">  const</span><span style="color:#79B8FF"> options</span><span style="color:#F97583"> =</span><span style="color:#9ECBFF"> 'options'</span></span><span class="line"><span style="color:#E1E4E8">  console.</span><span style="color:#B392F0" class="highlighted-word">log</span><span style="color:#E1E4E8">(options)</span></span><span class="line"><span style="color:#E1E4E8">  options.a </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span><span style="color:#6A737D"> // should not be highlighted</span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span></code></pre>
 <style>
 body { margin: 0; }
 .shiki { padding: 1em; }
diff --git a/packages/transformers/test/fixtures/match-algorithm/v1.js b/packages/transformers/test/fixtures/match-algorithm/v1.js
new file mode 100644
index 00000000..6829624e
--- /dev/null
+++ b/packages/transformers/test/fixtures/match-algorithm/v1.js
@@ -0,0 +1,22 @@
+function hello(indentSize, type) {
+  console.log('error and focus'); // [!code error] // [!code focus]
+}
+
+// [!code focus:5]
+console.log('focus')
+console.log('focus')
+console.log('focus')
+console.log('focus')
+
+// [!code highlight:3]
+console.log('highlighted')
+console.log('highlighted')
+
+console.log('highlighted') // [!code highlight:2]
+console.log('highlighted')
+
+// [!code word:options:4]
+let options = 'options'
+options = {}, // [!code word:log]
+console.log(options)
+options.a = "HELLO" // should not be highlighted
diff --git a/packages/transformers/test/fixtures/match-algorithm/v1.js.output.html b/packages/transformers/test/fixtures/match-algorithm/v1.js.output.html
new file mode 100644
index 00000000..51374d0b
--- /dev/null
+++ b/packages/transformers/test/fixtures/match-algorithm/v1.js.output.html
@@ -0,0 +1,11 @@
+<pre class="shiki github-dark has-focused has-highlighted" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> hello</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">indentSize</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">type</span><span style="color:#E1E4E8">) {</span></span><span class="line focused highlighted error"><span style="color:#E1E4E8">  console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'error and focus'</span><span style="color:#E1E4E8">); </span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span><span class="line focused"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'focus'</span><span style="color:#E1E4E8">)</span></span><span class="line focused"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'focus'</span><span style="color:#E1E4E8">)</span></span><span class="line focused"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'focus'</span><span style="color:#E1E4E8">)</span></span><span class="line focused"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'focus'</span><span style="color:#E1E4E8">)</span></span><span class="line"></span><span class="line highlighted"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'highlighted'</span><span style="color:#E1E4E8">)</span></span><span class="line highlighted"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'highlighted'</span><span style="color:#E1E4E8">)</span></span><span class="line"></span><span class="line highlighted"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'highlighted'</span><span style="color:#E1E4E8">) </span></span><span class="line highlighted"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'highlighted'</span><span style="color:#E1E4E8">)</span></span><span class="line"></span><span class="line"><span style="color:#F97583">let</span><span style="color:#E1E4E8"> </span><span style="color:#E1E4E8" class="highlighted-word">options</span><span style="color:#E1E4E8"> </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> '</span><span style="color:#9ECBFF" class="highlighted-word">options</span><span style="color:#9ECBFF">'</span></span><span class="line"><span style="color:#E1E4E8" class="highlighted-word">options</span><span style="color:#E1E4E8"> </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> {}, </span></span><span class="line"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0" class="highlighted-word">log</span><span style="color:#E1E4E8">(</span><span style="color:#E1E4E8" class="highlighted-word">options</span><span style="color:#E1E4E8">)</span></span><span class="line"><span style="color:#E1E4E8">options.a </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span><span style="color:#6A737D"> // should not be highlighted</span></span><span class="line"></span></code></pre><style>
+* { tab-size: 4; }
+body { margin: 0; }
+.line { display: block; width: 100%; height: 1.2em; }
+.shiki { padding: 1em; }
+.has-focused .focused { background-color: #8805; }
+.highlighted { background-color: #8885; }
+.highlighted-word { background-color: #8885; }
+.highlighted.warning { background-color: #9905; }
+.highlighted.error { background-color: #8005; }
+</style>
\ No newline at end of file
diff --git a/packages/transformers/test/fixtures/match-algorithm/v3.js b/packages/transformers/test/fixtures/match-algorithm/v3.js
new file mode 100644
index 00000000..77831b82
--- /dev/null
+++ b/packages/transformers/test/fixtures/match-algorithm/v3.js
@@ -0,0 +1,22 @@
+function hello(indentSize, type) {
+  console.log('error and focus'); // [!code error] [!code focus]
+}
+
+// [!code focus:4]
+console.log('focus')
+console.log('focus')
+console.log('focus')
+console.log('focus')
+
+// [!code highlight:2]
+console.log('highlighted')
+console.log('highlighted')
+
+console.log('highlighted') // [!code highlight:2]
+console.log('highlighted')
+
+// [!code word:options:3]
+let options = 'options'
+options = {}, // [!code word:log]
+console.log(options)
+options.a = "HELLO" // should not be highlighted
diff --git a/packages/transformers/test/fixtures/match-algorithm/v3.js.output.html b/packages/transformers/test/fixtures/match-algorithm/v3.js.output.html
new file mode 100644
index 00000000..51374d0b
--- /dev/null
+++ b/packages/transformers/test/fixtures/match-algorithm/v3.js.output.html
@@ -0,0 +1,11 @@
+<pre class="shiki github-dark has-focused has-highlighted" style="background-color:#24292e;color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#F97583">function</span><span style="color:#B392F0"> hello</span><span style="color:#E1E4E8">(</span><span style="color:#FFAB70">indentSize</span><span style="color:#E1E4E8">, </span><span style="color:#FFAB70">type</span><span style="color:#E1E4E8">) {</span></span><span class="line focused highlighted error"><span style="color:#E1E4E8">  console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'error and focus'</span><span style="color:#E1E4E8">); </span></span><span class="line"><span style="color:#E1E4E8">}</span></span><span class="line"></span><span class="line focused"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'focus'</span><span style="color:#E1E4E8">)</span></span><span class="line focused"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'focus'</span><span style="color:#E1E4E8">)</span></span><span class="line focused"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'focus'</span><span style="color:#E1E4E8">)</span></span><span class="line focused"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'focus'</span><span style="color:#E1E4E8">)</span></span><span class="line"></span><span class="line highlighted"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'highlighted'</span><span style="color:#E1E4E8">)</span></span><span class="line highlighted"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'highlighted'</span><span style="color:#E1E4E8">)</span></span><span class="line"></span><span class="line highlighted"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'highlighted'</span><span style="color:#E1E4E8">) </span></span><span class="line highlighted"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0">log</span><span style="color:#E1E4E8">(</span><span style="color:#9ECBFF">'highlighted'</span><span style="color:#E1E4E8">)</span></span><span class="line"></span><span class="line"><span style="color:#F97583">let</span><span style="color:#E1E4E8"> </span><span style="color:#E1E4E8" class="highlighted-word">options</span><span style="color:#E1E4E8"> </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> '</span><span style="color:#9ECBFF" class="highlighted-word">options</span><span style="color:#9ECBFF">'</span></span><span class="line"><span style="color:#E1E4E8" class="highlighted-word">options</span><span style="color:#E1E4E8"> </span><span style="color:#F97583">=</span><span style="color:#E1E4E8"> {}, </span></span><span class="line"><span style="color:#E1E4E8">console.</span><span style="color:#B392F0" class="highlighted-word">log</span><span style="color:#E1E4E8">(</span><span style="color:#E1E4E8" class="highlighted-word">options</span><span style="color:#E1E4E8">)</span></span><span class="line"><span style="color:#E1E4E8">options.a </span><span style="color:#F97583">=</span><span style="color:#9ECBFF"> "HELLO"</span><span style="color:#6A737D"> // should not be highlighted</span></span><span class="line"></span></code></pre><style>
+* { tab-size: 4; }
+body { margin: 0; }
+.line { display: block; width: 100%; height: 1.2em; }
+.shiki { padding: 1em; }
+.has-focused .focused { background-color: #8805; }
+.highlighted { background-color: #8885; }
+.highlighted-word { background-color: #8885; }
+.highlighted.warning { background-color: #9905; }
+.highlighted.error { background-color: #8005; }
+</style>
\ No newline at end of file