diff --git a/src/lib/components/Gedit.svelte b/src/lib/components/Gedit.svelte index 94250a5..4d5f26d 100644 --- a/src/lib/components/Gedit.svelte +++ b/src/lib/components/Gedit.svelte @@ -1,24 +1,23 @@ + +{#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(null); let selectedSection = $state<'overview' | 'aliases' | 'services' | 'keys' | 'raw'>('overview'); @@ -264,7 +265,7 @@ {:else}

Raw DID Document

-
{inspection.rawJson}
+
{/if} {/if} @@ -570,9 +571,9 @@ margin: 0; padding: var(--space-3); overflow: auto; - color: #28190f; - background: #fff8ed; - border: 1px solid #c6ad89; + color: #eeeeec; + background: #300a24; + border: 1px solid #555753; border-radius: var(--radius-1); box-shadow: var(--shadow-sunken); white-space: pre-wrap; diff --git a/src/lib/components/SystemMonitor.svelte b/src/lib/components/SystemMonitor.svelte index acdaa35..a9da9b3 100644 --- a/src/lib/components/SystemMonitor.svelte +++ b/src/lib/components/SystemMonitor.svelte @@ -14,6 +14,7 @@ type LiveStreamKind } from '$lib/atproto/live-stream'; import { errorMessage } from '$lib/utils/errors'; + import HighlightedJson from './HighlightedJson.svelte'; type Props = { stream: LiveStreamKind }; let { stream }: Props = $props(); @@ -430,7 +431,7 @@
{selectedEvent.source ?? '—'}
-
{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 { + const highlighter = await getJsonHighlighter(); + return highlighter.codeToTokensBase(code, { lang: 'json', theme: themeName }); +} diff --git a/src/routes/lexicons/+layout.svelte b/src/routes/lexicons/+layout.svelte index bd62f59..a3a2819 100644 --- a/src/routes/lexicons/+layout.svelte +++ b/src/routes/lexicons/+layout.svelte @@ -10,6 +10,7 @@ import BrowserToolbar from '$lib/components/browser/BrowserToolbar.svelte'; import SchemaDocument from '$lib/components/browser/SchemaDocument.svelte'; import TraceView from '$lib/components/browser/TraceView.svelte'; + import HighlightedJson from '$lib/components/HighlightedJson.svelte'; let { children } = $props(); let activeTab = $state('schema'); @@ -115,7 +116,7 @@ {:else if activeTab === 'schema'} {:else if activeTab === 'json'} -
{rawJson}
+
{:else} {/if}