From b08302740b70bac5c92d77c5c8e697e87906f50d Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Thu, 24 Sep 2026 11:35:56 +0000 Subject: [PATCH 1/2] swap local JSON viewer from Shiki to Twinkleplop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces Shiki's JS engine/JSON grammar/dual-theme setup in the local UI JSON viewer with @twinkleplop/json, which highlights synchronously (no promise, no WASM, no async effect) while keeping HTML escaping, the malformed-input source fallback, and copy behavior intact. Theming moves from Shiki's --shiki-light/--shiki-dark CSS variables to Twinkleplop's .tok. classes (has_classes), mapped to the same github-light/dark colors. JSON object keys — which Twinkleplop tags as plain strings — are recovered via the render token hook (a string token followed by ':' gets .tok-key) so keys and string values stay visually distinct as they were under Shiki. Fixes #1632 --- apps/local/package.json | 5 +- apps/local/src/App.integration.test.tsx | 2 +- apps/local/src/components/json-view.tsx | 94 ++++++++----------------- apps/local/src/index.css | 33 +++++++-- pnpm-lock.yaml | 27 +++---- 5 files changed, 71 insertions(+), 90 deletions(-) diff --git a/apps/local/package.json b/apps/local/package.json index b652b39f5..2ed8fe9c6 100644 --- a/apps/local/package.json +++ b/apps/local/package.json @@ -17,11 +17,8 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-slot": "^1.3.3", - "@shikijs/core": "^3.23.0", - "@shikijs/engine-javascript": "^3.23.0", - "@shikijs/langs": "^3.23.0", - "@shikijs/themes": "^3.23.0", "@tailwindcss/vite": "^4.3.3", + "@twinkleplop/json": "0.1.4", "@uidotdev/usehooks": "^2.4.1", "@vercel/analytics": "^2.0.1", "class-variance-authority": "^0.7.1", diff --git a/apps/local/src/App.integration.test.tsx b/apps/local/src/App.integration.test.tsx index bd434522a..94746ce5d 100644 --- a/apps/local/src/App.integration.test.tsx +++ b/apps/local/src/App.integration.test.tsx @@ -211,7 +211,7 @@ describe('local receiver to viewer integration', () => { fireEvent.click(screen.getByRole('button', { name: 'Copy JSON' })) expect(await screen.findByRole('status', { name: 'JSON copied' })).not.toBeNull() await waitFor(() => { - expect(screen.getByTestId('highlighted-json').querySelector('.shiki')).not.toBeNull() + expect(screen.getByTestId('highlighted-json').querySelector('.json-view-code')).not.toBeNull() }) fireEvent.click(events[0]!) fireEvent.click(screen.getByRole('tab', { name: 'JSON' })) diff --git a/apps/local/src/components/json-view.tsx b/apps/local/src/components/json-view.tsx index 5ae09ba69..f926c046a 100644 --- a/apps/local/src/components/json-view.tsx +++ b/apps/local/src/components/json-view.tsx @@ -1,21 +1,12 @@ -import { createHighlighterCore } from '@shikijs/core' -import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript' -import json from '@shikijs/langs/json' -import githubDark from '@shikijs/themes/github-dark' -import githubLight from '@shikijs/themes/github-light' +import { language } from '@twinkleplop/json' import { Check, Copy } from 'lucide-react' -import { useEffect, useState } from 'react' import { useCopyToClipboard } from '@uidotdev/usehooks' type JsonViewProps = { code: string } -const highlighter = createHighlighterCore({ - engine: createJavaScriptRegexEngine(), - langs: [json], - themes: [githubLight, githubDark], -}) +const highlight = language() function formatJson(code: string) { try { @@ -25,72 +16,43 @@ function formatJson(code: string) { } } -/** Render JSON with Shiki only after its containing event has been expanded. */ +/** A string token immediately followed by `:` is an object key. */ +function highlightJson(code: string) { + return highlight(code, { + class_name: 'json-view-code', + has_classes: true, + token: (type, _start, end) => { + if (type === 'string' && code[end] === ':') { + return { class: 'tok-key' } + } + }, + }) +} + +/** Render JSON with Twinkleplop after its containing event has been expanded. */ export function JsonView({ code }: JsonViewProps) { - const [html, setHtml] = useState() const [copiedText, copyToClipboard] = useCopyToClipboard() const formattedCode = formatJson(code) + const html = highlightJson(formattedCode) const copied = copiedText === code - useEffect(() => { - let disposed = false - - void highlighter - .then((instance) => - instance.codeToHtml(formattedCode, { - lang: 'json', - themes: { - light: 'github-light', - dark: 'github-dark', - }, - defaultColor: false, - }) - ) - .then((result) => { - if (!disposed) { - setHtml(result) - } - }) - .catch(() => { - // Keep the readable, escaped source fallback if syntax highlighting cannot load. - }) - - return () => { - disposed = true - } - }, [formattedCode]) - const copyJson = () => { void copyToClipboard(code) } - const copyButton = ( - - ) - - if (!html) { - return ( -
-
{copyButton}
- {copied ? JSON copied : null} -
-          {formattedCode}
-        
-
- ) - } - return (
-
{copyButton}
+
+ +
{copied ? JSON copied : null}
=5.7.2' + '@twinkleplop/core@0.2.1': + resolution: {integrity: sha512-e/5EokljcDDsGvh55n0QO+HXDMVbpUhkNpg/QCWlY0Igw4a0qTB7fbJWtFrcRhBqfg+H7Wtw07A6U8YADm9J/A==} + + '@twinkleplop/json@0.1.4': + resolution: {integrity: sha512-lal3aivE56eP+i5JhK4eQAN1vOKbyft2Mbbc86+RrZwynN4f9MylQxwJbw/o1dTt5OY0ewO3UGtPaR5XCmctJA==} + '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -7769,6 +7766,12 @@ snapshots: dependencies: typescript: 5.9.3 + '@twinkleplop/core@0.2.1': {} + + '@twinkleplop/json@0.1.4': + dependencies: + '@twinkleplop/core': 0.2.1 + '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.8.1 From 063298526b778d89a9b011b7c349d6c298c04931 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Thu, 24 Sep 2026 11:58:17 +0000 Subject: [PATCH 2/2] fix(local): make JSON key color win over string token color The key span renders as `tok string tok-key`, and `.json-view .tok-key` lost the specificity tie to `.json-view .tok.string`, so keys picked up the string color. Scope the key rule to `.tok.tok-key` and order it after the string rule so keys keep their distinct color. --- apps/local/src/index.css | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/local/src/index.css b/apps/local/src/index.css index 15274c89c..448991bd5 100644 --- a/apps/local/src/index.css +++ b/apps/local/src/index.css @@ -88,29 +88,29 @@ color: #24292e; } - .json-view .tok-key, + .json-view .tok.string { + color: #032f62; + } + .json-view .tok.number, .json-view .tok.boolean, - .json-view .tok.keyword { + .json-view .tok.keyword, + .json-view .tok.tok-key { color: #005cc5; } - .json-view .tok.string { - color: #032f62; - } - .dark .json-view .json-view-code { color: #e1e4e8; } - .dark .json-view .tok-key, + .dark .json-view .tok.string { + color: #9ecbff; + } + .dark .json-view .tok.number, .dark .json-view .tok.boolean, - .dark .json-view .tok.keyword { + .dark .json-view .tok.keyword, + .dark .json-view .tok.tok-key { color: #79b8ff; } - - .dark .json-view .tok.string { - color: #9ecbff; - } }