{#if highlightedCode === code}{#each tokenLines as line, lineIndex (`${lineIndex}-${line.map((token) => token.content).join('')}`)}{#each line as token, tokenIndex (`${lineIndex}-${tokenIndex}`)}{token.content}{/each}{/each}{:else}{code}{/if}
+
+
diff --git a/src/lib/components/IdentityInspector.svelte b/src/lib/components/IdentityInspector.svelte
index 680713e..8992813 100644
--- a/src/lib/components/IdentityInspector.svelte
+++ b/src/lib/components/IdentityInspector.svelte
@@ -3,6 +3,7 @@
import { repoSession } from '$lib/atproto/session.svelte';
import { inspectIdentity, type IdentityCheck, type IdentityInspection } from '$lib/atproto/identity-inspector';
import { errorMessage } from '$lib/utils/errors';
+ import HighlightedJson from './HighlightedJson.svelte';
let inspection = $state{inspection.rawJson}
+ {selectedEvent.json}
+ {:else}
Select an event to inspect its JSON.
{/if} diff --git a/src/lib/syntax-highlighting.test.ts b/src/lib/syntax-highlighting.test.ts new file mode 100644 index 0000000..12e9527 --- /dev/null +++ b/src/lib/syntax-highlighting.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { highlightJson } from './syntax-highlighting'; + +describe('highlightJson', () => { + it('preserves JSON text and adds syntax colours', async () => { + const json = '{\n "enabled": true,\n "count": 3\n}'; + const lines = await highlightJson(json); + const tokens = lines.flat(); + + expect(lines.map((line) => line.map((token) => token.content).join('')).join('\n')).toBe(json); + expect(tokens.some((token) => token.content.includes('enabled') && token.color)).toBe(true); + expect(tokens.some((token) => token.content.includes('true') && token.color)).toBe(true); + }); +}); diff --git a/src/lib/syntax-highlighting.ts b/src/lib/syntax-highlighting.ts new file mode 100644 index 0000000..2361275 --- /dev/null +++ b/src/lib/syntax-highlighting.ts @@ -0,0 +1,58 @@ +import type { ThemeRegistration } from 'shiki'; + +export type SyntaxToken = { content: string; color?: string; fontStyle?: number }; +export type SyntaxTokenLine = SyntaxToken[]; + +const themeName = 'ubuntu-iterm2b24'; +const ubuntuTheme: ThemeRegistration = { + name: themeName, + type: 'dark', + colors: { + 'editor.background': '#300a24', + 'editor.foreground': '#eeeeec', + 'editorLineNumber.foreground': '#747772', + 'editor.selectionBackground': '#555753' + }, + settings: [ + { settings: { foreground: '#eeeeec', background: '#300a24' } }, + { scope: ['comment'], settings: { foreground: '#747772', fontStyle: 'italic' } }, + { scope: ['string', 'string.quoted'], settings: { foreground: '#4e9a06' } }, + { scope: ['constant.numeric', 'constant.language'], settings: { foreground: '#c4a000' } }, + { + scope: ['support.type.property-name', 'meta.structure.dictionary.key.json string'], + settings: { foreground: '#729fcf' } + }, + { scope: ['punctuation'], settings: { foreground: '#b3b7b0' } }, + { scope: ['invalid'], settings: { foreground: '#ef2929' } } + ] +}; + +type JsonTokenizer = (code: string, options: { lang: 'json'; theme: string }) => SyntaxTokenLine[]; +let highlighterPromise: Promise<{ codeToTokensBase: JsonTokenizer }> | null = null; + +async function getJsonHighlighter() { + highlighterPromise ??= Promise.all([ + import('@shikijs/core'), + import('@shikijs/engine-javascript'), + import('@shikijs/langs/json') + ]).then(async ([core, engine, json]) => { + const highlighter = await core.createHighlighterCore({ + themes: [ubuntuTheme], + langs: [json.default], + engine: engine.createJavaScriptRegexEngine() + }); + + return { + codeToTokensBase(code: string, options: { lang: 'json'; theme: string }) { + return highlighter.codeToTokensBase(code, options) as SyntaxTokenLine[]; + } + }; + }); + + return highlighterPromise; +} + +export async function highlightJson(code: string): Promise{rawJson}
+ {:else}