Repository: shikijs/shiki (ts)

Issue:
Adjacent brackets are colored incorrectly on https://shiki.style/packages/colorized-brackets
### Validations

- [x] Follow our [Code of Conduct](https://github.com/shikijs/shiki/blob/main/CODE_OF_CONDUCT.md)
- [x] Read the [Contributing Guide](https://github.com/shikijs/shiki/blob/main/CONTRIBUTING.md).
- [x] Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.

### Describe the bug

Colorized brackets are not working correctly on https://shiki.style/packages/colorized-brackets. Brackets that are right next to each other all share a color. See for this screenshot:

![Image](https://github.com/user-attachments/assets/4c70e952-3ef4-487e-8903-cd719a540a18)

There are many instances of the bug in the screenshot, for example in the second to last line, `})]` should be 3 different colors, but they are all the same color.

I haven't investigated the code yet, but a few observations:
- it looks like each "group" of brackets all gets the color of the final bracket in the group
- other punctuation seems to be affected too; see some of the commas and colons in the screenshot
- apparently colorized bracket tests are still passing, so this looks to be specific to some config
- this documentation page was colored correctly when colorized-brackets package was first introduced

### Reproduction

Run the shiki docs site, go to the /packages/colorized-brackets.

### Contributes

- [x] I am willing to submit a PR to fix this issue
- [ ] I am willing to submit a PR with failing tests

Pull request description:
fix(colorized-brackets): handle correctly with dual themes
### Description

This fixes a few issues with the @shikijs/colorized-brackets plugin:
- brackets colors could be incorrect when using dual themes (caused by shared `htmlStyle` objects)
- bracket colors didn't match theme when using Expressive Code (caused by EC appending IDs to theme names)
- some minor typos

### Linked Issues

- Fixes #989 (caused by the first issue)


Patch:
diff --git a/docs/.vitepress/components/HomeDemo.vue b/docs/.vitepress/components/HomeDemo.vue
index c78fdc19..94f47acd 100644
--- a/docs/.vitepress/components/HomeDemo.vue
+++ b/docs/.vitepress/components/HomeDemo.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="container max-w-250 mx-a my-10 min-h-250 vp-doc p4" of-hidden>
+  <div class="container max-w-250 mx-a my-10 vp-doc p4" of-hidden>
     <ShikiMiniPlayground />
   </div>
 </template>
diff --git a/packages/colorized-brackets/package.json b/packages/colorized-brackets/package.json
index 7fd9a361..f43ef49f 100644
--- a/packages/colorized-brackets/package.json
+++ b/packages/colorized-brackets/package.json
@@ -2,7 +2,7 @@
   "name": "@shikijs/colorized-brackets",
   "type": "module",
   "version": "3.2.2",
-  "description": "Collective of common transformers transformers for Shiki",
+  "description": "VSCode-style colorized brackets transformer for Shiki",
   "author": "Michael Moore <mscottmoore@pm.me>",
   "license": "MIT",
   "homepage": "https://github.com/shikijs/shiki#readme",
diff --git a/packages/colorized-brackets/src/colorizeBracketTokens.ts b/packages/colorized-brackets/src/colorizeBracketTokens.ts
index 25b69f55..6d9f88fd 100644
--- a/packages/colorized-brackets/src/colorizeBracketTokens.ts
+++ b/packages/colorized-brackets/src/colorizeBracketTokens.ts
@@ -107,7 +107,8 @@ function assignColorToToken(
       = shikiOptions
     const styles: Record<string, string> = typeof token.htmlStyle === 'string'
       ? {}
-      : token.htmlStyle || {}
+      // clone to make sure we're not mutating a shared style object
+      : structuredClone(token.htmlStyle ?? {})
 
     for (const [colorName, theme] of Object.entries(shikiOptions.themes)) {
       const themeName = typeof theme === 'string' ? theme : theme?.name
@@ -143,7 +144,7 @@ function getColor(
   const colors
     = themeName == null
       ? DEFAULT_BRACKETS_COLORS
-      : themes[themeName] ?? builtInThemes[themeName] ?? DEFAULT_BRACKETS_COLORS
+      : getThemeColors(themeName, themes) ?? getThemeColors(themeName, builtInThemes) ?? DEFAULT_BRACKETS_COLORS
 
   const isUnexpected = level === -1
   if (isUnexpected) {
@@ -153,3 +154,16 @@ function getColor(
     return colors[level % (colors.length - 1)]
   }
 }
+
+function getThemeColors(themeName: string, themes: Record<string, string[]>): string[] | null {
+  if (themes[themeName])
+    return themes[themeName]
+
+  // check if the start of the name matches any themes
+  // this improves compatibility with "Expressive Code", which appends unique IDs to the end of themeNames
+  const startsWithName = Object.keys(themes).sort().reverse().find(key => themeName.startsWith(key))
+  if (startsWithName)
+    return themes[startsWithName]
+
+  return null
+}
diff --git a/packages/colorized-brackets/src/index.ts b/packages/colorized-brackets/src/index.ts
index 467c7300..3cf584ef 100644
--- a/packages/colorized-brackets/src/index.ts
+++ b/packages/colorized-brackets/src/index.ts
@@ -28,7 +28,7 @@ const jinjaLikeBracketPairs: BracketPair[] = [
  *
  * @param options
  * @param options.themes - custom themes; all Shiki built-in themes are supported without additional configuration
- * @param options.bracketPairs - bracket definitions; be default [], {}, (), and <> (TS-only)
+ * @param options.bracketPairs - bracket definitions; by default [], {}, (), and <> (TS-only)
  * @param options.langs - language-specific overrides for themes and bracketPairs
  * @returns Shiki transformer
  */


Test patch:
diff --git a/packages/colorized-brackets/test/dual-themes.test.ts b/packages/colorized-brackets/test/dual-themes.test.ts
index 4d635b5a..aba8f717 100644
--- a/packages/colorized-brackets/test/dual-themes.test.ts
+++ b/packages/colorized-brackets/test/dual-themes.test.ts
@@ -16,7 +16,7 @@ describe('dual themes', async () => {
   })
 
   it('light and dark', () => {
-    const htmlStr = highlighter.codeToHtml('{}', {
+    const htmlStr = highlighter.codeToHtml('[({})](', {
       lang,
       themes: { light: 'light-plus', dark: 'dark-plus' },
       transformers: [
@@ -28,7 +28,14 @@ describe('dual themes', async () => {
         }),
       ],
     })
-    expect(htmlStr).toContain('<span style="color:Y;--shiki-dark:y">{</span>')
+
+    expect(htmlStr).toContain('<span style="color:Y;--shiki-dark:y">[</span>')
+    expect(htmlStr).toContain('<span style="color:P;--shiki-dark:p">(</span>')
+    expect(htmlStr).toContain('<span style="color:B;--shiki-dark:b">{</span>')
+    expect(htmlStr).toContain('<span style="color:B;--shiki-dark:b">}</span>')
+    expect(htmlStr).toContain('<span style="color:P;--shiki-dark:p">)</span>')
+    expect(htmlStr).toContain('<span style="color:Y;--shiki-dark:y">]</span>')
+    expect(htmlStr).toContain('<span style="color:R;--shiki-dark:r">(</span>')
   })
 
   it('custom prefix', () => {