Browse Source

syntax highlighting

master
Silberengel 1 month ago
parent
commit
94a8ee6efb
  1. 14
      package-lock.json
  2. 5
      package.json
  3. 6
      public/healthz.json
  4. 126
      src/lib/components/content/MarkdownRenderer.svelte
  5. 36
      src/lib/modules/events/EventView.svelte
  6. 5
      src/lib/modules/feed/FeedPost.svelte

14
package-lock.json generated

@ -1,12 +1,12 @@ @@ -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 @@ @@ -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 @@ @@ -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",

5
package.json

@ -1,6 +1,6 @@ @@ -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 @@ @@ -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",

6
public/healthz.json

@ -1,8 +1,8 @@ @@ -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
}

126
src/lib/components/content/MarkdownRenderer.svelte

@ -10,6 +10,9 @@ @@ -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 @@ @@ -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 <br>
gfm: true, // GitHub Flavored Markdown
@ -826,6 +830,66 @@ @@ -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: <pre><code>
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: <div class="listingblock"><pre><code> or <pre class="highlight"><code>
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 @@ @@ -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 @@ @@ -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 */

36
src/lib/modules/events/EventView.svelte

@ -221,27 +221,29 @@ @@ -221,27 +221,29 @@
{/if}
{/if}
<!-- Display chapter title prominently for kind 30041 (chapter sections) -->
{#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]}
<h2 class="chapter-title text-xl font-semibold mb-3 text-fog-text dark:text-fog-dark-text">
{chapterTitleTag[1]}
</h2>
<!-- For content events (not kind 30040 indexes), render the content -->
{#if item.event.kind !== 30040}
<!-- Display chapter title prominently for kind 30041 (chapter sections) -->
{#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]}
<h2 class="chapter-title text-xl font-semibold mb-3 text-fog-text dark:text-fog-dark-text">
{chapterTitleTag[1]}
</h2>
{/if}
{/if}
{/if}
<!-- Render the event itself -->
<div class="event-with-comments">
<FeedPost post={item.event} fullView={true} />
<!-- Load and display comments for each event in the index -->
{#if item.event.kind !== 30040}
<!-- Render the event content -->
<div class="event-with-comments">
<!-- Hide title in FeedPost since we're already showing it above as chapter-title -->
<FeedPost post={item.event} fullView={true} hideTitle={true} />
<!-- Load and display comments for each event in the index -->
<div class="comments-section mt-4">
<CommentThread threadId={item.event.id} event={item.event} />
</div>
{/if}
</div>
</div>
{/if}
<!-- Recursively render children if this is a nested index -->
{#if item.children && item.children.length > 0}
@ -281,7 +283,7 @@ @@ -281,7 +283,7 @@
{:else}
<!-- Display regular events using FeedPost -->
<div class="event-section">
<FeedPost post={rootEvent} fullView={true} />
<FeedPost post={rootEvent} fullView={true} hideTitle={false} />
</div>
<!-- Load and display comments for all event types -->

5
src/lib/modules/feed/FeedPost.svelte

@ -24,9 +24,10 @@ @@ -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 @@ @@ -603,7 +604,7 @@
<MetadataCard event={post} hideTitle={true} hideImageIfInMedia={true} />
{@const title = getTitle()}
{#if title && title !== 'Untitled'}
{#if !hideTitle && title && title !== 'Untitled'}
<h2 class="post-title font-bold mb-4 text-fog-text dark:text-fog-dark-text" style="font-size: 1.5em;">
{title}
</h2>

Loading…
Cancel
Save