+// note: angle brackets not colored
+{
+ // @colors 0=Y
+ public void Add(T input) { }
+ // P P P P @colors
+}
+// @colors 0=Y
diff --git a/packages/colorized-brackets/test/fixtures/css/basic.css b/packages/colorized-brackets/test/fixtures/css/basic.css
new file mode 100644
index 000000000..85fb393c7
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/css/basic.css
@@ -0,0 +1,9 @@
+body[data-theme="dark"] {
+ /*Y Y Y @colors */
+ a {
+ /* @colors 4=P */
+ color: #AAAAFF
+ }
+ /* @colors 2=P */
+}
+/* @colors 0=Y */
\ No newline at end of file
diff --git a/packages/colorized-brackets/test/fixtures/html/basic.html b/packages/colorized-brackets/test/fixtures/html/basic.html
new file mode 100644
index 000000000..97036ad44
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/html/basic.html
@@ -0,0 +1,4 @@
+
+ Brackets in text are not colorized (like these parentheses)
+ Brackets in attributes are also not colorized.
+
\ No newline at end of file
diff --git a/packages/colorized-brackets/test/fixtures/html/embedded.html b/packages/colorized-brackets/test/fixtures/html/embedded.html
new file mode 100644
index 000000000..0bde5197c
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/html/embedded.html
@@ -0,0 +1,14 @@
+
+
+ Rock and stone to the bone!
+
+
diff --git a/packages/colorized-brackets/test/fixtures/java/generic.java b/packages/colorized-brackets/test/fixtures/java/generic.java
new file mode 100644
index 000000000..ce72a28b7
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/java/generic.java
@@ -0,0 +1,11 @@
+public class Box {
+ // Y @colors
+ // note: the angle brackets are not highlighted
+ private T t;
+
+ public void set(T t) { this.t = t; }
+ // P P P P @colors
+ public T get() { return t; }
+ // PP P P @colors
+}
+// @colors 0=Y
\ No newline at end of file
diff --git a/packages/colorized-brackets/test/fixtures/jinja/basic.jinja b/packages/colorized-brackets/test/fixtures/jinja/basic.jinja
new file mode 100644
index 000000000..b0b58a6fd
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/jinja/basic.jinja
@@ -0,0 +1,12 @@
+{% if test %}
+ {# @colors 0-1=Y 11-12=Y #}
+
+ {{test(foo[1])}}
+ {# @colors 4-5=Y 10=P 14=B 16=B 17=P 18-19=Y #}
+
+{% endif %}
+{# @colors 0-1=Y 9-10=Y #}
+
\ No newline at end of file
diff --git a/packages/colorized-brackets/test/fixtures/liquid/basic.liquid b/packages/colorized-brackets/test/fixtures/liquid/basic.liquid
new file mode 100644
index 000000000..9ffbd55f4
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/liquid/basic.liquid
@@ -0,0 +1,16 @@
+Recommended Products
+
+ {% assign recommended_products = product.metafields.my_fields.rec_products.value %}
+
+ {% for product in recommended_products %}
+
+ -
+
+
+ {{product.title[0]}}
+
+
+
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/packages/colorized-brackets/test/fixtures/python/basic.py b/packages/colorized-brackets/test/fixtures/python/basic.py
new file mode 100644
index 000000000..8c4d0b1a6
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/python/basic.py
@@ -0,0 +1,23 @@
+from collections.abc import Callable, Awaitable
+
+def feeder(get_next_item: Callable[[], str]) -> None:
+ # Y PBB PY @colors
+ pass
+
+def async_query(on_success: Callable[[int], None],
+ # PB B P @colors 15=Y
+ on_error: Callable[[int, Exception], None]) -> None:
+ # PB B PY @colors
+ pass
+
+async def on_update(value: str) -> None:
+ # Y Y @colors
+ pass
+
+callback: Callable[[str], Awaitable[None]] = on_update
+# YP P P PY @colors
+
+l = [1, 2, 3]
+# Y Y @colors
+s = f"last: {l[-1]}"
+# Y P PY @colors
\ No newline at end of file
diff --git a/packages/colorized-brackets/test/fixtures/rust/generic.rs b/packages/colorized-brackets/test/fixtures/rust/generic.rs
new file mode 100644
index 000000000..b379a7542
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/rust/generic.rs
@@ -0,0 +1,19 @@
+fn largest(list: &[T]) -> &T {
+ // Y P PY Y @colors
+ // note: the angle brackets are not highlighted
+ let mut largest = &list[0];
+ // P P @colors
+
+ for item in list {
+ // P @colors
+ if item > largest {
+ // B @colors
+ largest = item;
+ }
+ // @colors 4=B
+ }
+ // @colors 2=P
+
+ largest
+}
+// @colors 0=Y
diff --git a/packages/colorized-brackets/test/fixtures/rust/turbofish.rs b/packages/colorized-brackets/test/fixtures/rust/turbofish.rs
new file mode 100644
index 000000000..5f86af027
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/rust/turbofish.rs
@@ -0,0 +1,12 @@
+
+#![allow(unused_variables)]
+// P PY @colors 2=Y
+fn main() {
+ // YY Y @colors
+ let v = Vec::::new();
+ // PP @colors
+ // note: angle brackets not colored
+ println!("{:?}", v);
+ // P P @colors
+}
+// @colors 0=Y
\ No newline at end of file
diff --git a/packages/colorized-brackets/test/fixtures/svelte/embedded.svelte b/packages/colorized-brackets/test/fixtures/svelte/embedded.svelte
new file mode 100644
index 000000000..b3d337951
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/svelte/embedded.svelte
@@ -0,0 +1,35 @@
+
+
+
+ {#each rocks as rockWords}
+
+ {#each rockWords as rock, i}
+
+ {rock}
+
+ {i < rocks.length - 1 ? "and" : ""}
+
+ {/each}
+
+ {/each}
+
+ !
+
+
+
diff --git a/packages/colorized-brackets/test/fixtures/ts/angle-brackets.ts b/packages/colorized-brackets/test/fixtures/ts/angle-brackets.ts
new file mode 100644
index 000000000..4ec35e367
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/ts/angle-brackets.ts
@@ -0,0 +1,11 @@
+const objectToEntries = (obj: Record) => {
+ // Y P PY Y @colors
+ if (Object.keys(obj).length > 0) {
+ // B B P P @colors 5=P
+ return Object.entries(obj);
+ // B B @colors
+ }
+ // @colors 2=P
+ return null;
+};
+// @colors 0=Y
diff --git a/packages/colorized-brackets/test/fixtures/ts/comments.ts b/packages/colorized-brackets/test/fixtures/ts/comments.ts
new file mode 100644
index 000000000..858d644a4
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/ts/comments.ts
@@ -0,0 +1,5 @@
+// ([{}][0])
+/*
+including multiline comments
+Record
+*/
diff --git a/packages/colorized-brackets/test/fixtures/ts/generic.ts b/packages/colorized-brackets/test/fixtures/ts/generic.ts
new file mode 100644
index 000000000..03ddf7712
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/ts/generic.ts
@@ -0,0 +1,9 @@
+function first(array: T[]): T | undefined {
+ // Y YY PPY Y @colors
+ return array[0];
+ // P P @colors
+}
+// @colors 0=Y
+
+first([1, 2, 3]);
+// Y YYP PY @colors
diff --git a/packages/colorized-brackets/test/fixtures/ts/jsdoc.ts b/packages/colorized-brackets/test/fixtures/ts/jsdoc.ts
new file mode 100644
index 000000000..ed5ab2e89
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/ts/jsdoc.ts
@@ -0,0 +1,16 @@
+/**
+ *
+ * @param {Array} [strings=["()"]] - description
+ * Y P PY Y P BB PY @colors
+ * note: the colored () inside the string in the default bracket
+ * there is not enough context from the scopes to do otherwise
+ * this matches VSCode behavior
+ * @returns {string[]}
+ * Y PPY @colors
+ */
+function reverse(strings: string[]) {
+ // Y PPY Y @colors
+ return strings.reverse();
+ // PP @colors
+}
+// @colors 0=Y
diff --git a/packages/colorized-brackets/test/fixtures/ts/strings.ts b/packages/colorized-brackets/test/fixtures/ts/strings.ts
new file mode 100644
index 000000000..c223193e5
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/ts/strings.ts
@@ -0,0 +1,14 @@
+let foo = {
+ // Y @colors
+ bar: ["()", "[]", "{}"],
+ // P P @colors
+};
+// @colors 0=Y
+`foo:
+ ${foo}
+ ${0}
+ [[]{}()]
+`;
+
+`foo.bar[0]: ${foo.bar[0]}`;
+// Y Y @colors
diff --git a/packages/colorized-brackets/test/fixtures/ts/template.ts b/packages/colorized-brackets/test/fixtures/ts/template.ts
new file mode 100644
index 000000000..451696f9d
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/ts/template.ts
@@ -0,0 +1,4 @@
+`(outer) ${[`(inner) ${[[]]}`]}`;
+// Y PBBP Y @colors
+// note: vscode does not color the inner expression, but this plugin does
+// for now, considering this desired behavior even though it is divergent
diff --git a/packages/colorized-brackets/test/fixtures/ts/unexpected.txt b/packages/colorized-brackets/test/fixtures/ts/unexpected.txt
new file mode 100644
index 000000000..bb1b02d83
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/ts/unexpected.txt
@@ -0,0 +1,13 @@
+// this needs to be a txt file, otherwise `jsr publish` fails while checking for slow types
+if (true) {
+// Y Y R @colors
+
+ let obj = {
+ // P @colors
+ foo: 'foo'.split()(
+ // BBR @colors
+ }];
+ // @colors 2=P 3=R
+
+let foo = ([[[[)]]]]
+// PRRRRPRRRR @colors
diff --git a/packages/colorized-brackets/test/fixtures/tsx/basic.tsx b/packages/colorized-brackets/test/fixtures/tsx/basic.tsx
new file mode 100644
index 000000000..602823d2a
--- /dev/null
+++ b/packages/colorized-brackets/test/fixtures/tsx/basic.tsx
@@ -0,0 +1,6 @@
+function Hello({ name }: { name: string }) {
+ // YP P P PY Y @colors
+ return Hello, {name}
;
+ // P P @colors
+}
+// @colors 0=Y
\ No newline at end of file
diff --git a/packages/colorized-brackets/test/utils.ts b/packages/colorized-brackets/test/utils.ts
new file mode 100644
index 000000000..238e7233b
--- /dev/null
+++ b/packages/colorized-brackets/test/utils.ts
@@ -0,0 +1,89 @@
+import c from 'picocolors'
+
+interface ColoredBracket {
+ bracket: string
+ color: string
+}
+
+export function parseExpectedBrackets(content: string): ColoredBracket[] {
+ const brackets: ColoredBracket[] = []
+ const lines = content.split('\n')
+ const implicitIndexRegex = /[RYPB]/g
+ const explicitIndexRegex = /(\d+)(?:-(\d+))?=([RYPB])/g
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i]
+ if (line.includes('@colors')) {
+ const prev = lines[i - 1]
+ const [implicitIndexPart, explicitIndexPart] = line.split('@colors')
+ for (const match of explicitIndexPart.matchAll(explicitIndexRegex)) {
+ const start = Number.parseInt(match[1])
+ const end = Number.parseInt(match[2] || match[1]) + 1
+ const color = match[3]
+ brackets.push({
+ bracket: prev.substring(start, end),
+ color,
+ })
+ }
+ for (const match of implicitIndexPart.matchAll(implicitIndexRegex)) {
+ const index = match.index
+ const color = match[0]
+ brackets.push({ bracket: prev[index], color })
+ }
+ }
+ }
+ return brackets
+}
+
+export function parseActualBrackets(html: string): ColoredBracket[] {
+ const spanRegex
+ // eslint-disable-next-line regexp/no-super-linear-backtracking -- this is only run on input we control, so DoS is not a concern
+ = /\s*([0-9A-F]+;|..?)\s*<\/span>/g
+ const brackets = Array.from(html.matchAll(spanRegex)).map(
+ (match) => {
+ const color = match[1]
+ let bracket = match[2]
+ if (bracket.startsWith('')) {
+ bracket = String.fromCharCode(
+ Number.parseInt(bracket.substring(3, bracket.length - 1), 16),
+ )
+ }
+ return { color, bracket }
+ },
+ )
+ return brackets
+}
+
+export function prettifyBrackets(
+ brackets: ColoredBracket[],
+ { noAnsi = false } = {},
+): string {
+ if (!brackets.length)
+ return noAnsi ? 'none' : c.gray('none')
+ return brackets
+ .map(b => getColoredBracketTerminalOutput(b, { noAnsi }))
+ .join(' ')
+}
+
+function getColoredBracketTerminalOutput(
+ { bracket, color }: ColoredBracket,
+ { noAnsi = false } = {},
+): string {
+ const isCloser = [']', '}', ')', '>', '}}', '%}'].includes(bracket)
+ if (noAnsi)
+ return isCloser ? `${bracket}${color}` : `${color}${bracket}`
+ if (color === 'R') {
+ return c.red(bracket)
+ }
+ else if (color === 'Y') {
+ return c.yellow(bracket)
+ }
+ else if (color === 'P') {
+ return c.magenta(bracket)
+ }
+ else if (color === 'B') {
+ return c.blue(bracket)
+ }
+ else {
+ return `${color}${bracket}`
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 52b1a3b06..26b91f3cf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -436,6 +436,9 @@ importers:
'@iconify-json/svg-spinners':
specifier: 'catalog:'
version: 1.2.1
+ '@shikijs/colorized-brackets':
+ specifier: workspace:*
+ version: link:../packages/colorized-brackets
'@shikijs/transformers':
specifier: workspace:*
version: link:../packages/transformers
@@ -489,6 +492,12 @@ importers:
specifier: 'catalog:'
version: 1.2.5
+ packages/colorized-brackets:
+ dependencies:
+ shiki:
+ specifier: workspace:*
+ version: link:../shiki
+
packages/compat:
dependencies:
'@shikijs/core':