feat(highlight): integrate Highlight.js for syntax highlighting in JSON and SQL views

This commit is contained in:
Sam Chau
2026-01-02 20:21:03 +10:30
parent 77237b54ac
commit 59b032aab0
9 changed files with 216 additions and 100 deletions

View File

@@ -20,7 +20,8 @@
"@soapbox/kysely-deno-sqlite": "jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
"@std/assert": "jsr:@std/assert@^1.0.0",
"marked": "npm:marked@^15.0.6",
"simple-icons": "npm:simple-icons@^15.17.0"
"simple-icons": "npm:simple-icons@^15.17.0",
"highlight.js": "npm:highlight.js@^11.11.1"
},
"tasks": {
"dev": "APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 deno run -A npm:vite dev",

2
deno.lock generated
View File

@@ -1947,6 +1947,7 @@
"dependencies": [
"jsr:@soapbox/kysely-deno-sqlite@^2.2.0",
"jsr:@std/assert@1",
"npm:highlight.js@^11.11.1",
"npm:marked@^15.0.6",
"npm:simple-icons@^15.17.0"
],
@@ -1965,6 +1966,7 @@
"npm:eslint-plugin-svelte@^3.12.4",
"npm:eslint@^9.36.0",
"npm:globals@^16.4.0",
"npm:highlight.js@^11.11.1",
"npm:kysely@0.27.6",
"npm:lucide-svelte@0.546",
"npm:marked@^15.0.6",

10
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"dependencies": {
"@deno/vite-plugin": "^1.0.5",
"@jsr/db__sqlite": "^0.12.0",
"highlight.js": "^11.11.1",
"kysely": "0.27.6",
"lucide-svelte": "^0.546.0",
"marked": "^15.0.6",
@@ -2586,6 +2587,15 @@
"node": ">=8"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",

View File

@@ -6,6 +6,7 @@
"dependencies": {
"@deno/vite-plugin": "^1.0.5",
"@jsr/db__sqlite": "^0.12.0",
"highlight.js": "^11.11.1",
"kysely": "0.27.6",
"lucide-svelte": "^0.546.0",
"marked": "^15.0.6",

View File

@@ -2,6 +2,73 @@
@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100..900&display=swap');
@import 'tailwindcss';
/* Highlight.js: atom-one-light for light mode */
@import 'highlight.js/styles/atom-one-light.css';
/* Highlight.js: atom-one-dark for dark mode */
.dark .hljs {
color: #abb2bf;
}
.dark .hljs-comment,
.dark .hljs-quote {
color: #5c6370;
font-style: italic;
}
.dark .hljs-doctag,
.dark .hljs-keyword,
.dark .hljs-formula {
color: #c678dd;
}
.dark .hljs-section,
.dark .hljs-name,
.dark .hljs-selector-tag,
.dark .hljs-deletion,
.dark .hljs-subst {
color: #e06c75;
}
.dark .hljs-literal {
color: #56b6c2;
}
.dark .hljs-string,
.dark .hljs-regexp,
.dark .hljs-addition,
.dark .hljs-attribute,
.dark .hljs-meta .hljs-string {
color: #98c379;
}
.dark .hljs-attr,
.dark .hljs-variable,
.dark .hljs-template-variable,
.dark .hljs-type,
.dark .hljs-selector-class,
.dark .hljs-selector-attr,
.dark .hljs-selector-pseudo,
.dark .hljs-number {
color: #d19a66;
}
.dark .hljs-symbol,
.dark .hljs-bullet,
.dark .hljs-link,
.dark .hljs-meta,
.dark .hljs-selector-id,
.dark .hljs-title {
color: #61aeee;
}
.dark .hljs-built_in,
.dark .hljs-title.class_,
.dark .hljs-class .hljs-title {
color: #e6c07b;
}
.dark .hljs-emphasis {
font-style: italic;
}
.dark .hljs-strong {
font-weight: bold;
}
.dark .hljs-link {
text-decoration: underline;
}
@custom-variant dark (&:where(.dark, .dark *));
@theme {

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import hljs from 'highlight.js/lib/core';
import json from 'highlight.js/lib/languages/json';
import sql from 'highlight.js/lib/languages/sql';
hljs.registerLanguage('json', json);
hljs.registerLanguage('sql', sql);
export let data: unknown;
// Check if data has a queries array with SQL strings
$: hasQueries = data && typeof data === 'object' && 'queries' in data && Array.isArray((data as Record<string, unknown>).queries);
// Extract queries separately for SQL highlighting
$: queries = hasQueries ? ((data as Record<string, unknown>).queries as string[]) : [];
// Create data without queries for JSON display
$: dataWithoutQueries = hasQueries
? Object.fromEntries(Object.entries(data as Record<string, unknown>).filter(([k]) => k !== 'queries'))
: data;
$: jsonString = JSON.stringify(dataWithoutQueries, null, 2);
$: highlightedJson = hljs.highlight(jsonString, { language: 'json' }).value;
function highlightSql(query: string): string {
return hljs.highlight(query, { language: 'sql' }).value;
}
</script>
<div class="json-view space-y-4">
<!-- JSON metadata -->
<pre class="!bg-transparent !p-0 !m-0 whitespace-pre-wrap font-mono"><code class="hljs font-mono">{@html highlightedJson}</code></pre>
<!-- SQL Queries -->
{#if queries.length > 0}
<div class="border-t border-neutral-200 pt-4 dark:border-neutral-700">
<div class="mb-2 text-xs font-medium uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
Queries ({queries.length})
</div>
<div class="space-y-2">
{#each queries as query, i}
<div class="rounded border border-neutral-200 bg-neutral-100 p-3 dark:border-neutral-600 dark:bg-neutral-900">
<pre class="!bg-transparent !p-0 !m-0 whitespace-pre-wrap text-xs font-mono"><code class="hljs font-mono">{@html highlightSql(query)}</code></pre>
</div>
{/each}
</div>
</div>
{/if}
</div>
<style>
.json-view :global(.hljs),
.json-view :global(.hljs *) {
background: transparent !important;
font-family: var(--font-mono) !important;
}
</style>

View File

@@ -9,6 +9,15 @@
export let confirmText = 'Confirm';
export let cancelText = 'Cancel';
export let confirmDanger = false; // If true, confirm button is styled as danger (red)
export let size: 'sm' | 'md' | 'lg' | 'xl' | '2xl' = 'md';
const sizeClasses = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
'2xl': 'max-w-6xl'
};
const dispatch = createEventDispatcher();
@@ -53,7 +62,7 @@
>
<!-- Modal -->
<div
class="relative w-full max-w-md rounded-lg border border-neutral-200 bg-white shadow-xl dark:border-neutral-700 dark:bg-neutral-900"
class="relative w-full {sizeClasses[size]} rounded-lg border border-neutral-200 bg-white shadow-xl dark:border-neutral-700 dark:bg-neutral-900"
>
<!-- Header -->
<div class="border-b border-neutral-200 px-6 py-4 dark:border-neutral-800">

View File

@@ -105,7 +105,8 @@
return sortDirection === 'desc' ? sorted.reverse() : sorted;
}
$: sortedData = sortData(data);
$: sortedData = sortKey ? sortData(data) : data;
$: sortKey, sortDirection, sortedData = sortData(data);
</script>
<div class="overflow-x-auto rounded-lg border border-neutral-200 dark:border-neutral-800">

View File

@@ -2,6 +2,9 @@
import { Eye, Copy } from 'lucide-svelte';
import { alertStore } from '$alerts/store';
import Modal from '$ui/modal/Modal.svelte';
import JsonView from '$ui/meta/JsonView.svelte';
import Table from '$ui/table/Table.svelte';
import type { Column } from '$ui/table/types';
import LogsActionsBar from './components/LogsActionsBar.svelte';
import { createSearchStore } from '$lib/client/stores/search';
import { invalidateAll } from '$app/navigation';
@@ -45,6 +48,38 @@
ERROR: 'text-red-600 dark:text-red-400'
};
// Table columns
const columns: Column<LogEntry>[] = [
{
key: 'timestamp',
header: 'Timestamp',
sortable: true,
sortAccessor: (row) => new Date(row.timestamp).getTime(),
cell: (row) => ({
html: `<span class="font-mono text-xs text-neutral-600 dark:text-neutral-400">${new Date(row.timestamp).toLocaleString()}</span>`
})
},
{
key: 'level',
header: 'Level',
sortable: true,
cell: (row) => ({
html: `<span class="font-semibold ${levelColors[row.level] || 'text-neutral-600 dark:text-neutral-400'}">${row.level}</span>`
})
},
{
key: 'source',
header: 'Source',
sortable: true,
cell: (row) => row.source || '-'
},
{
key: 'message',
header: 'Message',
cell: (row) => row.message
}
];
// Meta modal state
let showMetaModal = false;
let selectedMeta: unknown = null;
@@ -155,98 +190,31 @@
{/if}
</div>
<!-- Log Container -->
<div
class="overflow-hidden rounded-lg border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900"
>
<table class="w-full text-sm">
<thead class="bg-neutral-50 dark:bg-neutral-800">
<tr>
<th
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
<!-- Log Table -->
<Table data={filteredLogs} {columns} emptyMessage="No logs found" hoverable={true} compact={true} initialSort={{ key: 'timestamp', direction: 'asc' }}>
<svelte:fragment slot="actions" let:row>
<div class="flex items-center justify-end gap-1">
<button
type="button"
on:click={() => copyLog(row)}
class="inline-flex h-7 w-7 items-center justify-center rounded border border-neutral-300 bg-white text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
title="Copy log entry"
>
<Copy size={14} />
</button>
{#if row.meta}
<button
type="button"
on:click={() => viewMeta(row.meta)}
class="inline-flex h-7 w-7 items-center justify-center rounded border border-neutral-300 bg-white text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
title="View metadata"
>
Timestamp
</th>
<th
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
>
Level
</th>
<th
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
>
Source
</th>
<th
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
>
Message
</th>
<th
class="border-b border-neutral-200 px-4 py-3 text-left font-semibold text-neutral-900 dark:border-neutral-700 dark:text-neutral-50"
>
Actions
</th>
</tr>
</thead>
<tbody class="[&_tr:last-child_td]:border-b-0">
{#each filteredLogs as log, index (`${log.timestamp}-${log.level}-${log.source}-${log.message}-${index}`)}
<tr class="hover:bg-neutral-50 dark:hover:bg-neutral-800">
<td
class="border-b border-neutral-200 px-4 py-2 font-mono text-xs text-neutral-600 dark:border-neutral-800 dark:text-neutral-400"
>
{new Date(log.timestamp).toLocaleString()}
</td>
<td
class="border-b border-neutral-200 px-4 py-2 font-semibold dark:border-neutral-800 {levelColors[
log.level
] || 'text-neutral-600 dark:text-neutral-400'}"
>
{log.level}
</td>
<td
class="border-b border-neutral-200 px-4 py-2 text-neutral-600 dark:border-neutral-800 dark:text-neutral-400"
>
{log.source || '-'}
</td>
<td
class="border-b border-neutral-200 px-4 py-2 text-neutral-900 dark:border-neutral-800 dark:text-neutral-50"
>
{log.message}
</td>
<td class="border-b border-neutral-200 px-4 py-2 text-center dark:border-neutral-800">
<div class="flex items-center justify-center gap-1">
<button
type="button"
on:click={() => copyLog(log)}
class="inline-flex h-7 w-7 items-center justify-center rounded border border-neutral-300 bg-white text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
title="Copy log entry"
>
<Copy size={14} />
</button>
{#if log.meta}
<button
type="button"
on:click={() => viewMeta(log.meta)}
class="inline-flex h-7 w-7 items-center justify-center rounded border border-neutral-300 bg-white text-neutral-700 transition-colors hover:bg-neutral-50 dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300 dark:hover:bg-neutral-800"
title="View metadata"
>
<Eye size={14} />
</button>
{/if}
</div>
</td>
</tr>
{:else}
<tr>
<td colspan="5" class="px-4 py-8 text-center text-neutral-500 dark:text-neutral-400">
No logs found
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<Eye size={14} />
</button>
{/if}
</div>
</svelte:fragment>
</Table>
</div>
<!-- Meta Modal -->
@@ -256,14 +224,14 @@
bodyMessage=""
confirmText="Close"
cancelText="Close"
size="2xl"
on:confirm={closeMetaModal}
on:cancel={closeMetaModal}
>
<pre
<div
slot="body"
class="max-h-[400px] overflow-auto rounded-lg bg-neutral-50 p-4 font-mono text-xs whitespace-pre-wrap text-neutral-900 dark:bg-neutral-800 dark:text-neutral-50">{JSON.stringify(
selectedMeta,
null,
2
)}</pre>
class="max-h-[70vh] overflow-auto rounded-lg bg-neutral-50 p-4 font-mono text-sm dark:bg-neutral-800"
>
<JsonView data={selectedMeta} />
</div>
</Modal>