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}