diff --git a/package-lock.json b/package-lock.json
index e9c1af7..b1a5071 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "aitherboard",
- "version": "0.1.1",
+ "version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aitherboard",
- "version": "0.1.1",
+ "version": "0.2.0",
"license": "MIT",
"dependencies": {
"@sveltejs/kit": "^2.0.0",
@@ -15,6 +15,7 @@
"asciidoctor": "3.0.x",
"dompurify": "^3.0.6",
"emoji-picker-element": "^1.28.1",
+ "highlight.js": "^11.11.1",
"idb": "^8.0.0",
"marked": "^11.1.1",
"nostr-tools": "^2.22.1",
@@ -3115,6 +3116,15 @@
"node": ">= 0.4"
}
},
+ "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/idb": {
"version": "8.0.3",
"resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz",
diff --git a/package.json b/package.json
index 516515d..16ec1f4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "aitherboard",
- "version": "0.1.1",
+ "version": "0.2.0",
"type": "module",
"author": "silberengel@gitcitadel.com",
"description": "A decentralized messageboard built on the Nostr protocol.",
@@ -26,10 +26,11 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.6",
"@tanstack/svelte-virtual": "^3.0.0",
+ "asciidoctor": "3.0.x",
"dompurify": "^3.0.6",
"emoji-picker-element": "^1.28.1",
+ "highlight.js": "^11.11.1",
"idb": "^8.0.0",
- "asciidoctor": "3.0.x",
"marked": "^11.1.1",
"nostr-tools": "^2.22.1",
"svelte": "^5.0.0",
diff --git a/public/healthz.json b/public/healthz.json
index c36a911..5b449ab 100644
--- a/public/healthz.json
+++ b/public/healthz.json
@@ -1,8 +1,8 @@
{
"status": "ok",
"service": "aitherboard",
- "version": "0.1.1",
- "buildTime": "2026-02-06T08:55:58.492Z",
+ "version": "0.2.0",
+ "buildTime": "2026-02-06T13:19:22.159Z",
"gitCommit": "unknown",
- "timestamp": 1770368158493
+ "timestamp": 1770383962159
}
\ No newline at end of file
diff --git a/src/lib/components/content/MarkdownRenderer.svelte b/src/lib/components/content/MarkdownRenderer.svelte
index 07c5760..76e9ccd 100644
--- a/src/lib/components/content/MarkdownRenderer.svelte
+++ b/src/lib/components/content/MarkdownRenderer.svelte
@@ -10,6 +10,9 @@
import { getHighlightsForEvent, findHighlightMatches, type Highlight } from '../../services/nostr/highlight-service.js';
import { mountComponent } from './mount-component-action.js';
import type { NostrEvent } from '../../types/nostr.js';
+ import hljs from 'highlight.js';
+ // Use VS Code theme for IDE-like appearance
+ import 'highlight.js/styles/vs2015.css';
import EmbeddedEvent from './EmbeddedEvent.svelte';
let mountingEmbeddedEvents = $state(false); // Guard for mounting
@@ -437,6 +440,7 @@
}
// Configure marked once - ensure images are rendered and HTML is preserved
+ // Note: Code highlighting is done post-render in the effect below, not via marked options
marked.setOptions({
breaks: true, // Convert line breaks to
gfm: true, // GitHub Flavored Markdown
@@ -826,6 +830,66 @@
// Use requestAnimationFrame + setTimeout to ensure DOM is ready
const frameId = requestAnimationFrame(() => {
const timeoutId = setTimeout(() => {
+ if (!containerRef) return;
+
+ // Highlight code blocks (both Markdown and AsciiDoc)
+ // Markdown:
+ const codeBlocks = containerRef.querySelectorAll('pre code');
+ codeBlocks.forEach((block) => {
+ if (!block.classList.contains('hljs')) {
+ const code = block.textContent || '';
+ const className = block.className || '';
+ const langMatch = className.match(/language-(\w+)/);
+ const lang = langMatch ? langMatch[1] : '';
+
+ if (lang && hljs.getLanguage(lang)) {
+ try {
+ block.innerHTML = hljs.highlight(code, { language: lang }).value;
+ block.className = `hljs ${className}`;
+ } catch (err) {
+ console.warn('Highlight.js error:', err);
+ }
+ } else {
+ try {
+ block.innerHTML = hljs.highlightAuto(code).value;
+ block.className = `hljs ${className}`;
+ } catch (err) {
+ console.warn('Highlight.js auto-detect error:', err);
+ }
+ }
+ }
+ });
+
+ // AsciiDoc: or
+ if (!containerRef) return;
+ const asciidocBlocks = containerRef.querySelectorAll('.listingblock pre code, pre.highlight code');
+ asciidocBlocks.forEach((block) => {
+ if (!block.classList.contains('hljs')) {
+ const code = block.textContent || '';
+ // AsciiDoc might have language in data-lang or class
+ const preElement = block.closest('pre');
+ const lang = preElement?.getAttribute('data-lang') ||
+ preElement?.className.match(/(?:^|\s)language-(\w+)/)?.[1] ||
+ block.className.match(/(?:^|\s)language-(\w+)/)?.[1] || '';
+
+ if (lang && hljs.getLanguage(lang)) {
+ try {
+ block.innerHTML = hljs.highlight(code, { language: lang }).value;
+ block.className = `hljs ${block.className}`;
+ } catch (err) {
+ console.warn('Highlight.js error:', err);
+ }
+ } else {
+ try {
+ block.innerHTML = hljs.highlightAuto(code).value;
+ block.className = `hljs ${block.className}`;
+ } catch (err) {
+ console.warn('Highlight.js auto-detect error:', err);
+ }
+ }
+ }
+ });
+
mountProfileBadges();
mountEmbeddedEvents();
}, 150);
@@ -998,6 +1062,64 @@
padding: 0;
}
+ /* IDE-style code block styling - always dark/black background like VS Code/JetBrains */
+ :global(.markdown-content pre) {
+ background: #1e1e1e !important; /* VS Code dark background */
+ border: 1px solid #3e3e3e;
+ border-radius: 4px;
+ padding: 1rem;
+ margin: 1rem 0;
+ overflow-x: auto;
+ position: relative;
+ }
+
+ :global(.markdown-content pre code.hljs) {
+ display: block;
+ overflow-x: auto;
+ padding: 0;
+ background: transparent !important;
+ color: #d4d4d4; /* VS Code text color */
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace;
+ font-size: 0.9em;
+ line-height: 1.5;
+ }
+
+ /* Inline code - keep light styling but make it subtle */
+ :global(.markdown-content code.hljs:not(pre code)) {
+ padding: 0.2em 0.4em;
+ border-radius: 0.25rem;
+ background: var(--fog-border, #e5e7eb);
+ color: inherit;
+ }
+
+ :global(.dark .markdown-content code.hljs:not(pre code)) {
+ background: var(--fog-dark-border, #374151);
+ }
+
+ /* Ensure pre blocks always have dark background regardless of theme */
+ :global(.markdown-content pre) {
+ background: #1e1e1e !important;
+ border-color: #3e3e3e !important;
+ }
+
+ /* AsciiDoc code blocks - same styling */
+ :global(.markdown-content .listingblock pre) {
+ background: #1e1e1e !important;
+ border: 1px solid #3e3e3e;
+ border-radius: 4px;
+ padding: 1rem;
+ margin: 1rem 0;
+ overflow-x: auto;
+ }
+
+ :global(.markdown-content .listingblock pre code.hljs) {
+ background: transparent !important;
+ color: #d4d4d4;
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace;
+ font-size: 0.9em;
+ line-height: 1.5;
+ }
+
:global(.markdown-content blockquote) {
border-left: 4px solid var(--fog-border, #e5e7eb);
padding-left: 1rem;
@@ -1012,14 +1134,14 @@
/* Greentext styling - 4chan style */
:global(.markdown-content .greentext) {
- color: #789922;
+ color: #4a7c3a; /* Deeper, darker green for better readability in light mode */
display: block;
margin: 0.25rem 0;
font-family: inherit;
}
:global(.dark .markdown-content .greentext) {
- color: #8fbc8f;
+ color: #8fbc8f; /* Lighter green for dark mode */
}
/* Ensure greentext lines appear on their own line even if markdown processes them */
diff --git a/src/lib/modules/events/EventView.svelte b/src/lib/modules/events/EventView.svelte
index 87c2061..6484fb2 100644
--- a/src/lib/modules/events/EventView.svelte
+++ b/src/lib/modules/events/EventView.svelte
@@ -221,27 +221,29 @@
{/if}
{/if}
-
- {#if item.event.kind === 30041 || item.event.kind === 1 || item.event.kind === 30817}
- {@const chapterTitleTag = item.event.tags.find(t => t[0] === 'title')}
- {#if chapterTitleTag && chapterTitleTag[1]}
-
- {chapterTitleTag[1]}
-
+
+ {#if item.event.kind !== 30040}
+
+ {#if item.event.kind === 30041 || item.event.kind === 1 || item.event.kind === 30817}
+ {@const chapterTitleTag = item.event.tags.find(t => t[0] === 'title')}
+ {#if chapterTitleTag && chapterTitleTag[1]}
+
+ {chapterTitleTag[1]}
+
+ {/if}
{/if}
- {/if}
-
-
-
-
-
- {#if item.event.kind !== 30040}
+
+
+
+
+
+
- {/if}
-
+
+ {/if}
{#if item.children && item.children.length > 0}
@@ -281,7 +283,7 @@
{:else}
-
+
diff --git a/src/lib/modules/feed/FeedPost.svelte b/src/lib/modules/feed/FeedPost.svelte
index 3154045..64c3502 100644
--- a/src/lib/modules/feed/FeedPost.svelte
+++ b/src/lib/modules/feed/FeedPost.svelte
@@ -24,9 +24,10 @@
preloadedReactions?: NostrEvent[]; // Pre-loaded reactions to avoid duplicate fetches
parentEvent?: NostrEvent; // Optional parent event if already loaded
quotedEvent?: NostrEvent; // Optional quoted event if already loaded
+ hideTitle?: boolean; // If true, don't render the title (useful when title is rendered elsewhere)
}
- let { post, fullView = false, preloadedReactions, parentEvent: providedParentEvent, quotedEvent: providedQuotedEvent }: Props = $props();
+ let { post, fullView = false, preloadedReactions, parentEvent: providedParentEvent, quotedEvent: providedQuotedEvent, hideTitle = false }: Props = $props();
// Check if this event is bookmarked (async, so we use state)
// Only check if user is logged in
@@ -603,7 +604,7 @@
{@const title = getTitle()}
- {#if title && title !== 'Untitled'}
+ {#if !hideTitle && title && title !== 'Untitled'}
{title}