mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
Refactor chat components and improve markdown processing
- Enhanced ChatArea component to handle streaming messages more efficiently with incremental updates. - Removed MarkdownService as it was no longer needed; integrated its functionality into StreamingMarkdownService. - Updated ServiceRegistration to reflect the removal of MarkdownService and adjusted AI class type. - Improved StreamingMarkdownService for better streaming performance and error handling. - Added new styles for syntax highlighting and markdown rendering in styles.css. - Removed old styles_old.css as they were deprecated. - Updated package.json and package-lock.json to include lowlight for improved syntax highlighting.
This commit is contained in:
parent
7efdac24cf
commit
9e429c4cd1
10 changed files with 703 additions and 366 deletions
|
|
@ -42,7 +42,7 @@ export class Gemini implements IAIClass {
|
|||
"\n" +
|
||||
this.aiPrompt.responseFormat() +
|
||||
"\n" +
|
||||
this.aiPrompt.getDirectories() +
|
||||
//this.aiPrompt.getDirectories() +
|
||||
"\n" +
|
||||
prompt +
|
||||
"\n" +
|
||||
|
|
|
|||
|
|
@ -1,51 +1,119 @@
|
|||
<script lang="ts">
|
||||
import { Resolve } from "Services/DependencyService";
|
||||
import { MarkdownService } from "Services/MarkdownService";
|
||||
import { Services } from "Services/Services";
|
||||
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
|
||||
import type { StreamingMarkdownService } from "Services/StreamingMarkdownService";
|
||||
|
||||
export let messages: Array<{id: string, content: string, isUser: boolean, isStreaming: boolean}> = [];
|
||||
|
||||
let chatContainer: HTMLDivElement;
|
||||
let streamingMarkdownService: StreamingMarkdownService = Resolve(Services.StreamingMarkdownService);
|
||||
let messageElements = new Map<string, HTMLElement>();
|
||||
let lastProcessedContent = new Map<string, string>();
|
||||
|
||||
|
||||
// Process each message content with markdown
|
||||
$: processedMessages = messages.map((message) => {
|
||||
if (message.isUser) {
|
||||
return {
|
||||
...message,
|
||||
htmlContent: `<p>${message.content}</p>`
|
||||
};
|
||||
// Track streaming messages and update them incrementally
|
||||
$: {
|
||||
messages.forEach((message) => {
|
||||
if (!message.isUser) {
|
||||
const lastContent = lastProcessedContent.get(message.id) || '';
|
||||
|
||||
// Only update if content has changed
|
||||
if (message.content !== lastContent) {
|
||||
updateMessageContent(message);
|
||||
lastProcessedContent.set(message.id, message.content);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateMessageContent(message: {id: string, content: string, isUser: boolean, isStreaming: boolean}) {
|
||||
const element = messageElements.get(message.id);
|
||||
if (!element) return;
|
||||
|
||||
if (message.isStreaming) {
|
||||
// Use streaming update
|
||||
streamingMarkdownService.streamChunk(message.id, message.content);
|
||||
} else {
|
||||
let htmlContent;
|
||||
// Finalize the message
|
||||
streamingMarkdownService.finalizeStream(message.id, message.content);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeMessageElement(messageId: string, element: HTMLElement) {
|
||||
messageElements.set(messageId, element);
|
||||
streamingMarkdownService.initializeStream(messageId, element);
|
||||
}
|
||||
|
||||
// Svelte action to handle element initialization
|
||||
function streamingAction(element: HTMLElement, messageId: string) {
|
||||
initializeMessageElement(messageId, element);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
// Cleanup when element is removed
|
||||
messageElements.delete(messageId);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Process static messages (user messages and initial load)
|
||||
function getStaticHTML(message: {id: string, content: string, isUser: boolean, isStreaming: boolean}): string {
|
||||
if (message.isUser) {
|
||||
return `<p>${message.content}</p>`;
|
||||
}
|
||||
|
||||
// For assistant messages that aren't streaming, use traditional parsing
|
||||
if (!message.isStreaming) {
|
||||
try {
|
||||
htmlContent = streamingMarkdownService.formatText(message.content) || `<p>${message.content}</p>`;
|
||||
return streamingMarkdownService.formatText(message.content) || `<p>${message.content}</p>`;
|
||||
} catch (err) {
|
||||
console.error('HTML processing failed:', err);
|
||||
htmlContent = `<p>${message.content}</p>`;
|
||||
return `<p>${message.content}</p>`;
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
htmlContent
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return ''; // Streaming messages will be handled by the streaming service
|
||||
}
|
||||
|
||||
// Clean up when component is destroyed
|
||||
function cleanup() {
|
||||
messageElements.clear();
|
||||
lastProcessedContent.clear();
|
||||
}
|
||||
|
||||
// Make sure to clean up when messages are removed
|
||||
$: {
|
||||
const currentMessageIds = new Set(messages.map(m => m.id));
|
||||
|
||||
// Remove tracking for messages that no longer exist
|
||||
for (const [id] of messageElements) {
|
||||
if (!currentMessageIds.has(id)) {
|
||||
messageElements.delete(id);
|
||||
lastProcessedContent.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="chat-area" bind:this={chatContainer}>
|
||||
{#each processedMessages as message (message.id)}
|
||||
{#each messages as message (message.id)}
|
||||
<div class="message-container" class:user={message.isUser} class:assistant={!message.isUser}>
|
||||
<div class="message-bubble" class:user={message.isUser} class:assistant={!message.isUser}>
|
||||
{#if message.isUser}
|
||||
<p>{message.content}</p>
|
||||
{:else}
|
||||
<div class="markdown-content" class:streaming={message.isStreaming}>
|
||||
{@html message.htmlContent}
|
||||
{#if message.isStreaming}
|
||||
<span class="streaming-indicator">● ● ●</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="markdown-content" class:streaming={message.isStreaming}>
|
||||
{#if message.isStreaming}
|
||||
<!-- Streaming message: use action for initialization -->
|
||||
<div
|
||||
use:streamingAction={message.id}
|
||||
class="streaming-content"
|
||||
></div>
|
||||
<span class="streaming-indicator">● ● ●</span>
|
||||
{:else}
|
||||
<!-- Static message: use traditional rendering -->
|
||||
{@html getStaticHTML(message)}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -105,6 +173,10 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.streaming-content {
|
||||
min-height: 1em; /* Ensure the element exists for binding */
|
||||
}
|
||||
|
||||
.streaming-indicator {
|
||||
display: inline-block;
|
||||
color: var(--text-accent);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
import { Resolve } from "Services/DependencyService";
|
||||
import { Services } from "Services/Services";
|
||||
import ChatArea from "./ChatArea.svelte";
|
||||
import type { IAIClassStreaming } from "AIClasses/Gemini/Gemini";
|
||||
import type { IAIClass } from "AIClasses/IAIClass";
|
||||
|
||||
let ai: IAIClassStreaming = Resolve(Services.IAIClass);
|
||||
let ai: IAIClass = Resolve(Services.IAIClass);
|
||||
let actioner: IActioner = Resolve(Services.IActioner);
|
||||
|
||||
let semaphore: Semaphore = new Semaphore(1, false);
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
import { unified, type Processor } from 'unified';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkMath from 'remark-math';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkToc from 'remark-toc';
|
||||
import remarkEmoji from 'remark-emoji';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
|
||||
export class MarkdownService {
|
||||
private static processor: Processor<any, any, any, any, any> | null = null;
|
||||
|
||||
constructor() {
|
||||
if (!MarkdownService.processor) {
|
||||
MarkdownService.processor = this.createProcessor();
|
||||
}
|
||||
}
|
||||
|
||||
private createProcessor() {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkMath)
|
||||
.use(remarkGfm)
|
||||
.use(remarkToc)
|
||||
.use(remarkEmoji)
|
||||
.use(remarkRehype, { allowDangerousHtml: false })
|
||||
.use(rehypeKatex)
|
||||
.use(rehypeHighlight)
|
||||
.use(rehypeStringify);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats LLM response text into HTML-friendly format
|
||||
* Handles output from Claude, ChatGPT, and Gemini
|
||||
* @param content - The raw LLM response text
|
||||
* @returns HTML string
|
||||
*/
|
||||
formatToHTML(content: string): string {
|
||||
try {
|
||||
const preprocessedContent = this.preprocessContent(content);
|
||||
const result = MarkdownService.processor!.processSync(preprocessedContent);
|
||||
return String(result);
|
||||
} catch (error) {
|
||||
console.error('Markdown processing failed:', error);
|
||||
return `<p>${this.escapeHtml(content)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
private preprocessContent(content: string): string {
|
||||
return content
|
||||
// 1. LaTeX Math Notation (normalize different formats) - No change needed
|
||||
.replace(/\\\[([\s\S]*?)\\\]/g, (_, equation) => `$$${equation}$$`)
|
||||
.replace(/\\\(([\s\S]*?)\\\)/g, (_, equation) => `$${equation}$`)
|
||||
|
||||
// 2. Handle Code Block Language Tags (allow hyphens)
|
||||
.replace(/```([a-zA-Z0-9-]+)\s*\n/g, '```$1\n')
|
||||
|
||||
// 3. Fix Common Escape Issues (use with caution) - No change, but be aware of its behavior
|
||||
.replace(/\\`/g, '`')
|
||||
.replace(/\\\*/g, '*')
|
||||
.replace(/\\_/g, '_')
|
||||
|
||||
// 4. Normalize Line Endings - No change needed
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
|
||||
// 5. Handle Triple Quotes - No change needed
|
||||
.replace(/"""\s*(\w+)?\s*\n([\s\S]*?)\n"""/g, (match, lang, code) => {
|
||||
return lang ? `\`\`\`${lang}\n${code}\n\`\`\`` : `\`\`\`\n${code}\n\`\`\``;
|
||||
})
|
||||
|
||||
// 6. Clean up Multiple Consecutive Newlines (collapse to a standard paragraph break)
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
|
||||
// 7. Handle Gemini's math-in-code-blocks tendency - No change needed
|
||||
.replace(/```math\n([\s\S]*?)\n```/g, (_, equation) => {
|
||||
if (equation.includes('\\') || equation.includes('{') || equation.includes('^') || equation.includes('_')) {
|
||||
return `$$${equation}$$`;
|
||||
}
|
||||
return `\`\`\`\n${equation}\n\`\`\``;
|
||||
})
|
||||
|
||||
// 8. Fix Bold/Italic Formatting Issues - No change needed
|
||||
.replace(/\*\*\s+([^*]+)\s+\*\*/g, '**$1**')
|
||||
.replace(/\*\s+([^*]+)\s+\*/g, '*$1*')
|
||||
|
||||
// 9. Fix Table Formatting Issues - No change needed
|
||||
.replace(/^\|\s+/gm, '| ')
|
||||
.replace(/\s+\|$/gm, ' |')
|
||||
|
||||
// 10. Handle Alternative List Markers - No change needed
|
||||
.replace(/^•\s/gm, '- ')
|
||||
.replace(/^→\s/gm, '- ')
|
||||
|
||||
// 11. Fix Link Formatting Issues - No change needed
|
||||
.replace(/\[\s+([^\]]+)\s+\]/g, '[$1]')
|
||||
|
||||
// 12. Handle Footnote Variations (remove optional spaces)
|
||||
.replace(/\[\^(\w+)\]\s*:/g, '[^$1]:')
|
||||
|
||||
// 13. Clean up Heading Formatting - No change needed
|
||||
.replace(/^#+\s+/gm, (match) => match.replace(/\s+/g, ' '));
|
||||
}
|
||||
|
||||
private escapeHtml(text: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,7 @@ import type { IActioner } from "Actioner/IActioner";
|
|||
import type { IAIClass } from "AIClasses/IAIClass";
|
||||
import { GeminiActionDefinitions } from "Actioner/Gemini/GeminiActionDefinitions";
|
||||
import type { IActionDefinitions } from "Actioner/IActionDefinitions";
|
||||
import { Gemini, type IAIClassStreaming } from "AIClasses/Gemini/Gemini";
|
||||
import { MarkdownService } from "./MarkdownService";
|
||||
import { Gemini } from "AIClasses/Gemini/Gemini";
|
||||
import { StreamingMarkdownService } from "./StreamingMarkdownService";
|
||||
|
||||
export function RegisterDependencies(plugin: DmsAssistantPlugin) {
|
||||
|
|
@ -22,10 +21,8 @@ export function RegisterDependencies(plugin: DmsAssistantPlugin) {
|
|||
RegisterSingleton<IPrompt>(Services.IPrompt, new AIPrompt());
|
||||
RegisterSingleton<IActioner>(Services.IActioner, new Actioner());
|
||||
|
||||
RegisterTransient<MarkdownService>(Services.MarkdownService, () => new MarkdownService());
|
||||
RegisterTransient<StreamingMarkdownService>(Services.StreamingMarkdownService, () => new StreamingMarkdownService());
|
||||
|
||||
|
||||
RegisterAiProvider(plugin);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@ import { unified, type Processor } from 'unified';
|
|||
import remarkParse from 'remark-parse';
|
||||
import remarkMath from 'remark-math';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkToc from 'remark-toc';
|
||||
import remarkEmoji from 'remark-emoji';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import rehypeKatex from 'rehype-katex';
|
||||
import rehypeHighlight from 'rehype-highlight';
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
|
||||
interface StreamingState {
|
||||
element: HTMLElement;
|
||||
buffer: string;
|
||||
lastProcessedLength: number;
|
||||
isComplete: boolean;
|
||||
}
|
||||
|
||||
export class StreamingMarkdownService {
|
||||
private static processor: Processor<any, any, any, any, any> | null = null;
|
||||
private streamingStates = new Map<string, StreamingState>();
|
||||
|
||||
constructor() {
|
||||
if (!StreamingMarkdownService.processor) {
|
||||
|
|
@ -21,59 +27,198 @@ export class StreamingMarkdownService {
|
|||
|
||||
private createProcessor() {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkBreaks) // Note: This makes single newlines into <br> tags.
|
||||
.use(remarkToc)
|
||||
.use(remarkEmoji)
|
||||
.use(remarkMath)
|
||||
.use(remarkRehype, { allowDangerousHtml: false }) // Important for security
|
||||
.use(remarkRehype, {
|
||||
allowDangerousHtml: false
|
||||
})
|
||||
.use(rehypeKatex)
|
||||
.use(rehypeHighlight)
|
||||
.use(rehypeStringify);
|
||||
.use(rehypeHighlight, {
|
||||
detect: true,
|
||||
plainText: ['txt', 'text'],
|
||||
aliases: {
|
||||
javascript: ['js', 'jsx'],
|
||||
typescript: ['ts', 'tsx'],
|
||||
python: ['py'],
|
||||
markdown: ['md', 'mdx'],
|
||||
shell: ['bash', 'sh', 'zsh']
|
||||
}
|
||||
})
|
||||
.use(rehypeStringify, {
|
||||
allowDangerousHtml: false,
|
||||
allowDangerousCharacters: false,
|
||||
closeSelfClosing: true
|
||||
});
|
||||
}
|
||||
|
||||
public formatText(text: string) {
|
||||
public formatText(text: string): string {
|
||||
try {
|
||||
const preprocessed = this.preprocessContent(text);
|
||||
const result = StreamingMarkdownService.processor!.processSync(preprocessed);
|
||||
return String(result);
|
||||
} catch (error) {
|
||||
console.warn('Markdown processing failed, falling back to safe rendering:', error);
|
||||
// Fallback to basic HTML escaping if parsing fails
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
console.warn('Markdown processing failed:', error);
|
||||
return this.getFallbackHTML(text);
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified streaming approach
|
||||
public initializeStream(messageId: string, container: HTMLElement): void {
|
||||
container.innerHTML = '';
|
||||
|
||||
this.streamingStates.set(messageId, {
|
||||
element: container,
|
||||
buffer: '',
|
||||
lastProcessedLength: 0,
|
||||
isComplete: false
|
||||
});
|
||||
}
|
||||
|
||||
public streamChunk(messageId: string, fullText: string): void {
|
||||
const state = this.streamingStates.get(messageId);
|
||||
if (!state || state.isComplete) return;
|
||||
|
||||
// Update buffer
|
||||
state.buffer = fullText;
|
||||
|
||||
// Use debounced rendering for better performance
|
||||
this.debouncedRender(messageId);
|
||||
}
|
||||
|
||||
private renderTimeouts = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
private debouncedRender(messageId: string, immediate: boolean = false): void {
|
||||
const existingTimeout = this.renderTimeouts.get(messageId);
|
||||
if (existingTimeout) {
|
||||
clearTimeout(existingTimeout);
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
const state = this.streamingStates.get(messageId);
|
||||
if (!state) return;
|
||||
|
||||
try {
|
||||
const html = this.formatText(state.buffer);
|
||||
state.element.innerHTML = html;
|
||||
state.lastProcessedLength = state.buffer.length;
|
||||
} catch (error) {
|
||||
console.warn('Streaming render failed:', error);
|
||||
}
|
||||
|
||||
this.renderTimeouts.delete(messageId);
|
||||
};
|
||||
|
||||
if (immediate) {
|
||||
render();
|
||||
} else {
|
||||
const timeout = setTimeout(render, 50); // 50ms debounce
|
||||
this.renderTimeouts.set(messageId, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
public finalizeStream(messageId: string, fullText: string): void {
|
||||
const state = this.streamingStates.get(messageId);
|
||||
if (!state) return;
|
||||
|
||||
state.isComplete = true;
|
||||
state.buffer = fullText;
|
||||
|
||||
// Final render without debounce
|
||||
this.debouncedRender(messageId, true);
|
||||
|
||||
// Cleanup
|
||||
this.streamingStates.delete(messageId);
|
||||
const timeout = this.renderTimeouts.get(messageId);
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
this.renderTimeouts.delete(messageId);
|
||||
}
|
||||
}
|
||||
|
||||
private preprocessContent(content: string): string {
|
||||
// Simplified and safer preprocessing
|
||||
return content
|
||||
// Normalize line endings
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
// Convert LaTeX delimiters
|
||||
.replace(/\\\[([\s\S]*?)\\\]/g, (match, math) => {
|
||||
// Ensure math blocks are on their own lines
|
||||
return '\n$$\n' + math.trim() + '\n$$\n';
|
||||
})
|
||||
.replace(/\\\(([\s\S]*?)\\\)/g, '$$$1$$')
|
||||
// Ensure headers have blank lines before them (but not at start)
|
||||
.replace(/([^\n])\n(#{1,6}\s)/g, '$1\n\n$2')
|
||||
// Collapse excessive newlines but preserve intentional spacing
|
||||
.replace(/\n{4,}/g, '\n\n\n')
|
||||
// Clean up list formatting - ensure consistent spacing
|
||||
.replace(/^(\s*)([*+-]|\d+\.)\s+/gm, '$1$2 ')
|
||||
// Ensure task list checkboxes are properly formatted
|
||||
.replace(/^(\s*)([*+-])\s*\[([ x])\]/gm, '$1$2 [$3]');
|
||||
}
|
||||
|
||||
// Fix LaTeX math delimiters
|
||||
.replace(/\\\[([\s\S]*?)\\\]/g, '$$\n$1\n$$') // Block math
|
||||
.replace(/\\\(([\s\S]*?)\\\)/g, '$$1$') // Inline math - CRITICAL FIX HERE
|
||||
|
||||
// Clean up excessive newlines to a standard paragraph break
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
|
||||
// Fix escaped characters that shouldn't be (use with caution)
|
||||
.replace(/\\\*/g, '*')
|
||||
.replace(/\\_/g, '_')
|
||||
.replace(/\\`/g, '`')
|
||||
|
||||
// Ensure headers have a blank line above them for proper parsing
|
||||
.replace(/(?<!\n)\n(#{1,6}\s)/g, '\n\n$1')
|
||||
|
||||
// Clean up extra spaces inside formatting (robust version)
|
||||
.replace(/\*\*\s+([^*]+?)\s+\*\*/g, '**$1**')
|
||||
.replace(/(?<!\*)\*\s+([^*]+?)\s+\*(?!\*)/g, '*$1*');
|
||||
private getFallbackHTML(text: string): string {
|
||||
// Improved fallback with basic markdown support
|
||||
const escaped = text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
const lines = escaped.split('\n');
|
||||
const html: string[] = [];
|
||||
let inList = false;
|
||||
let inCodeBlock = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('```')) {
|
||||
if (inCodeBlock) {
|
||||
html.push('</code></pre>');
|
||||
inCodeBlock = false;
|
||||
} else {
|
||||
html.push('<pre><code>');
|
||||
inCodeBlock = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inCodeBlock) {
|
||||
html.push(line + '\n');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Basic list support
|
||||
if (/^[*+-]\s/.test(line)) {
|
||||
if (!inList) {
|
||||
html.push('<ul>');
|
||||
inList = true;
|
||||
}
|
||||
html.push(`<li>${line.substring(2)}</li>`);
|
||||
} else if (inList && line.trim() === '') {
|
||||
html.push('</ul>');
|
||||
inList = false;
|
||||
} else {
|
||||
if (inList) {
|
||||
html.push('</ul>');
|
||||
inList = false;
|
||||
}
|
||||
|
||||
// Basic formatting
|
||||
const formatted = line
|
||||
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.+?)`/g, '<code>$1</code>');
|
||||
|
||||
if (line.trim()) {
|
||||
html.push(`<p>${formatted}</p>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inList) html.push('</ul>');
|
||||
if (inCodeBlock) html.push('</code></pre>');
|
||||
|
||||
return html.join('');
|
||||
}
|
||||
}
|
||||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -14,6 +14,7 @@
|
|||
"core-js": "^3.45.1",
|
||||
"express": "^5.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lowlight": "^3.3.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-parse": "^9.0.1",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"core-js": "^3.45.1",
|
||||
"express": "^5.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lowlight": "^3.3.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-parse": "^9.0.1",
|
||||
|
|
|
|||
426
styles.css
426
styles.css
|
|
@ -1,3 +1,42 @@
|
|||
/* ============================== */
|
||||
/* CSS Variables for Theming */
|
||||
/* ============================== */
|
||||
|
||||
:root {
|
||||
--code-background: #24292e;
|
||||
--code-background-inline: rgba(27, 31, 35, 0.05);
|
||||
--text-color: #24292e;
|
||||
--text-muted: #586069;
|
||||
--link-color: #0969da;
|
||||
--blockquote-border: #d0d7de;
|
||||
--table-border: #d0d7de;
|
||||
--table-header-background: #f6f8fa;
|
||||
--hr-color: #d0d7de;
|
||||
--checkbox-bg: #fff;
|
||||
--checkbox-border: #d1d5da;
|
||||
--checkbox-checked-bg: #0969da;
|
||||
--footnote-border: #d0d7de;
|
||||
}
|
||||
|
||||
/* Dark mode variables */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--code-background: #161b22;
|
||||
--code-background-inline: rgba(240, 246, 252, 0.15);
|
||||
--text-color: #c9d1d9;
|
||||
--text-muted: #8b949e;
|
||||
--link-color: #58a6ff;
|
||||
--blockquote-border: #3b434b;
|
||||
--table-border: #3b434b;
|
||||
--table-header-background: #161b22;
|
||||
--hr-color: #3b434b;
|
||||
--checkbox-bg: #161b22;
|
||||
--checkbox-border: #3b434b;
|
||||
--checkbox-checked-bg: #58a6ff;
|
||||
--footnote-border: #3b434b;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Syntax Highlighting Styles */
|
||||
/* GitHub Dark Theme for highlight.js */
|
||||
|
|
@ -11,6 +50,26 @@
|
|||
background: #24292e;
|
||||
}
|
||||
|
||||
/* Language indicator */
|
||||
.hljs.language-js::before,
|
||||
.hljs.language-javascript::before,
|
||||
.hljs.language-python::before,
|
||||
.hljs.language-css::before,
|
||||
.hljs.language-html::before,
|
||||
.hljs.language-typescript::before,
|
||||
.hljs.language-jsx::before,
|
||||
.hljs.language-tsx::before {
|
||||
content: attr(class);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 0.25em 0.5em;
|
||||
font-size: 0.75em;
|
||||
color: #8b949e;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 0 0 0 0.25em;
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-meta .hljs-keyword,
|
||||
|
|
@ -107,12 +166,144 @@
|
|||
color: #e1e4e8;
|
||||
}
|
||||
|
||||
/* Additional highlight.js classes for better coverage */
|
||||
.hljs-selector-pseudo {
|
||||
color: #79b8ff;
|
||||
}
|
||||
|
||||
.hljs-selector-tag {
|
||||
color: #85e89d;
|
||||
}
|
||||
|
||||
.hljs-template-comment {
|
||||
color: #6a737d;
|
||||
}
|
||||
|
||||
.hljs-meta-keyword {
|
||||
color: #f97583;
|
||||
}
|
||||
|
||||
.hljs-meta-string {
|
||||
color: #9ecbff;
|
||||
}
|
||||
|
||||
/* Alternative dark theme colors if preferred */
|
||||
.hljs.github-dark-dimmed {
|
||||
color: #adbac7;
|
||||
background: #22272e;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* GFM Task Lists */
|
||||
/* ============================== */
|
||||
|
||||
/* Task list container */
|
||||
.contains-task-list,
|
||||
ul.contains-task-list {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* Task list items */
|
||||
.task-list-item,
|
||||
li.task-list-item {
|
||||
position: relative;
|
||||
list-style: none;
|
||||
padding-left: 1.75em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Task list checkboxes */
|
||||
.task-list-item > input[type="checkbox"],
|
||||
.task-list-item input[type="checkbox"] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.25em;
|
||||
margin: 0;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
cursor: pointer;
|
||||
background-color: var(--checkbox-bg);
|
||||
border: 1px solid var(--checkbox-border);
|
||||
border-radius: 3px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
/* Checked state */
|
||||
.task-list-item > input[type="checkbox"]:checked,
|
||||
.task-list-item input[type="checkbox"]:checked {
|
||||
background-color: var(--checkbox-checked-bg);
|
||||
border-color: var(--checkbox-checked-bg);
|
||||
}
|
||||
|
||||
/* Checkmark for checked boxes */
|
||||
.task-list-item > input[type="checkbox"]:checked::after,
|
||||
.task-list-item input[type="checkbox"]:checked::after {
|
||||
content: "✓";
|
||||
display: block;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 0.75em;
|
||||
line-height: 1em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
.task-list-item > input[type="checkbox"]:disabled,
|
||||
.task-list-item input[type="checkbox"]:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Nested task lists */
|
||||
.task-list-item .contains-task-list {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* GFM Footnotes */
|
||||
/* ============================== */
|
||||
|
||||
/* Footnote references */
|
||||
sup[data-footnote-ref],
|
||||
a[data-footnote-ref] {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
sup[data-footnote-ref]::before {
|
||||
content: "[";
|
||||
}
|
||||
|
||||
sup[data-footnote-ref]::after {
|
||||
content: "]";
|
||||
}
|
||||
|
||||
/* Footnote section */
|
||||
section[data-footnotes],
|
||||
.footnotes {
|
||||
margin-top: 2em;
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid var(--footnote-border);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Footnote list */
|
||||
section[data-footnotes] ol,
|
||||
.footnotes ol {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
/* Footnote back references */
|
||||
a[data-footnote-backref] {
|
||||
text-decoration: none;
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* KaTeX Math Rendering Styles */
|
||||
/* ============================== */
|
||||
|
|
@ -764,41 +955,48 @@ body {
|
|||
font-size: 1.5em;
|
||||
font-weight: 600;
|
||||
margin: 0.67em 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.message-bubble.assistant h2 {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
margin: 0.75em 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.message-bubble.assistant h3 {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
margin: 0.83em 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.message-bubble.assistant h4 {
|
||||
font-size: 1em;
|
||||
font-weight: 600;
|
||||
margin: 1.12em 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.message-bubble.assistant h5 {
|
||||
font-size: 0.9em;
|
||||
font-weight: 600;
|
||||
margin: 1.5em 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.message-bubble.assistant h6 {
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
margin: 1.67em 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Paragraphs */
|
||||
.message-bubble.assistant p {
|
||||
margin: 0.5em 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Lists */
|
||||
|
|
@ -806,23 +1004,66 @@ body {
|
|||
.message-bubble.assistant ol {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 2em;
|
||||
list-style-position: outside;
|
||||
}
|
||||
|
||||
.message-bubble.assistant ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
.message-bubble.assistant ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.message-bubble.assistant li {
|
||||
margin: 0.25em 0;
|
||||
padding-left: 0.25em;
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Handle nested lists properly */
|
||||
.message-bubble.assistant ol ol,
|
||||
.message-bubble.assistant ul ul,
|
||||
.message-bubble.assistant ol ul,
|
||||
.message-bubble.assistant ul ol {
|
||||
margin: 0.25em 0;
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
/* Ensure nested list numbering works correctly */
|
||||
.message-bubble.assistant ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
|
||||
.message-bubble.assistant ol ol ol {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
|
||||
/* Fix paragraph spacing inside list items */
|
||||
.message-bubble.assistant li > p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.message-bubble.assistant li > p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.message-bubble.assistant li > p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
.message-bubble.assistant pre {
|
||||
background-color: var(--code-background, #f6f8fa);
|
||||
background-color: var(--code-background);
|
||||
border-radius: 4px;
|
||||
padding: 0.5em;
|
||||
overflow-x: auto;
|
||||
margin: 0.5em 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-bubble.assistant code {
|
||||
background-color: var(--code-background, rgba(27, 31, 35, 0.05));
|
||||
background-color: var(--code-background-inline);
|
||||
border-radius: 3px;
|
||||
padding: 0.2em 0.4em;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
|
|
@ -832,11 +1073,12 @@ body {
|
|||
.message-bubble.assistant pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Blockquotes */
|
||||
.message-bubble.assistant blockquote {
|
||||
border-left: 4px solid var(--blockquote-border, #d0d7de);
|
||||
border-left: 4px solid var(--blockquote-border);
|
||||
padding-left: 1em;
|
||||
margin: 0.5em 0;
|
||||
color: var(--text-muted);
|
||||
|
|
@ -847,23 +1089,37 @@ body {
|
|||
border-collapse: collapse;
|
||||
margin: 0.5em 0;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message-bubble.assistant th,
|
||||
.message-bubble.assistant td {
|
||||
border: 1px solid var(--table-border, #d0d7de);
|
||||
border: 1px solid var(--table-border);
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.message-bubble.assistant th {
|
||||
background-color: var(--table-header-background, #f6f8fa);
|
||||
background-color: var(--table-header-background);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Table alignment classes from GFM */
|
||||
.message-bubble.assistant th[align="center"],
|
||||
.message-bubble.assistant td[align="center"] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message-bubble.assistant th[align="right"],
|
||||
.message-bubble.assistant td[align="right"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
.message-bubble.assistant a {
|
||||
color: var(--link-color, #0969da);
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
|
@ -874,7 +1130,7 @@ body {
|
|||
/* Horizontal rules */
|
||||
.message-bubble.assistant hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--hr-color, #d0d7de);
|
||||
border-top: 1px solid var(--hr-color);
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
|
|
@ -898,4 +1154,160 @@ body {
|
|||
/* Strikethrough */
|
||||
.message-bubble.assistant del {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Additional HTML Elements */
|
||||
/* ============================== */
|
||||
|
||||
/* Keyboard input */
|
||||
.message-bubble.assistant kbd {
|
||||
display: inline-block;
|
||||
padding: 0.2em 0.4em;
|
||||
font-size: 0.875em;
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
line-height: 1;
|
||||
color: var(--text-color);
|
||||
vertical-align: middle;
|
||||
background-color: var(--code-background-inline);
|
||||
border: 1px solid var(--table-border);
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* Mark/highlight */
|
||||
.message-bubble.assistant mark {
|
||||
background-color: #fff3cd;
|
||||
color: #000;
|
||||
padding: 0.2em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Subscript and Superscript */
|
||||
.message-bubble.assistant sub {
|
||||
font-size: 0.75em;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.message-bubble.assistant sup {
|
||||
font-size: 0.75em;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
/* Abbreviations */
|
||||
.message-bubble.assistant abbr[title] {
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Definition lists */
|
||||
.message-bubble.assistant dl {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.message-bubble.assistant dt {
|
||||
font-weight: 600;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.message-bubble.assistant dd {
|
||||
margin-left: 2em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* Details/Summary (collapsible sections) */
|
||||
.message-bubble.assistant details {
|
||||
margin: 0.5em 0;
|
||||
padding: 0.5em;
|
||||
border: 1px solid var(--table-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message-bubble.assistant summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: 0.25em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.message-bubble.assistant details[open] summary {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Emoji Support */
|
||||
/* ============================== */
|
||||
|
||||
.message-bubble.assistant .emoji {
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-weight: normal;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Line Numbers for Code Blocks (optional) */
|
||||
/* ============================== */
|
||||
|
||||
/* If using line numbers with rehype-highlight */
|
||||
.hljs .code-line {
|
||||
display: block;
|
||||
padding-left: 1em;
|
||||
margin-left: -1em;
|
||||
}
|
||||
|
||||
.hljs .numbered-code-line::before {
|
||||
content: attr(data-line-number);
|
||||
display: inline-block;
|
||||
width: 2em;
|
||||
text-align: right;
|
||||
margin-right: 1em;
|
||||
color: #6a737d;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Responsive adjustments */
|
||||
/* ============================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.message-bubble.assistant table {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.message-bubble.assistant th,
|
||||
.message-bubble.assistant td {
|
||||
padding: 0.25em;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.message-bubble.assistant pre {
|
||||
padding: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================== */
|
||||
/* Print styles */
|
||||
/* ============================== */
|
||||
|
||||
@media print {
|
||||
.message-bubble.assistant {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.message-bubble.assistant a {
|
||||
color: #0969da;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message-bubble.assistant pre,
|
||||
.message-bubble.assistant code {
|
||||
background-color: #f6f8fa;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.hljs {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
179
styles_old.css
179
styles_old.css
|
|
@ -1,179 +0,0 @@
|
|||
/* Enhanced code block styling with better spacing */
|
||||
pre {
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
line-height: 1.4; /* Better line spacing */
|
||||
font-size: 0.875rem; /* Slightly larger for readability */
|
||||
}
|
||||
|
||||
pre code {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: inherit;
|
||||
background: none; /* Remove any inherited background */
|
||||
padding: 0; /* Remove any inherited padding */
|
||||
border: none;
|
||||
display: block;
|
||||
white-space: pre; /* Preserve all whitespace and indentation */
|
||||
word-wrap: normal;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
/* Fix indentation preservation */
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
tab-size: 4; /* Set consistent tab size */
|
||||
-moz-tab-size: 4;
|
||||
}
|
||||
|
||||
/* Ensure proper spacing for inline elements */
|
||||
.hljs * {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* Syntax highlighting colors (keep your existing colors) */
|
||||
.hljs-keyword, [data-token="keyword"] {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.hljs-string, [data-token="string"] {
|
||||
color: #a5d6ff;
|
||||
}
|
||||
|
||||
.hljs-comment, [data-token="comment"] {
|
||||
color: #8b949e;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-number, [data-token="number"] {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.hljs-function, [data-token="function"] {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
|
||||
.hljs-variable, [data-token="variable"] {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
.hljs-built_in, [data-token="builtin"] {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
.hljs-title, [data-token="title"] {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
/* Additional fixes for better formatting */
|
||||
.hljs-params {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.hljs-attr {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
.hljs-punctuation {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Method/function calls */
|
||||
.hljs-title.function_ {
|
||||
color: #d2a8ff;
|
||||
}
|
||||
|
||||
/* Variable names and identifiers */
|
||||
.hljs-variable, .hljs-name {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
/* Built-in functions and methods */
|
||||
.hljs-built_in, .hljs-title.function {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
/* Property access and attributes */
|
||||
.hljs-property, .hljs-attr {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
/* Operators */
|
||||
.hljs-operator {
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
/* Punctuation and delimiters */
|
||||
.hljs-punctuation {
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
/* Language-specific: Python function definitions */
|
||||
.hljs-title.class_ {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
/* Parameters in function definitions */
|
||||
.hljs-params {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
/* Literals and constants */
|
||||
.hljs-literal, .hljs-constant {
|
||||
color: #79c0ff;
|
||||
}
|
||||
|
||||
/* Class names */
|
||||
.hljs-class .hljs-title {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
/* Module/import names */
|
||||
.hljs-module {
|
||||
color: #ffa657;
|
||||
}
|
||||
|
||||
/* Meta information (like decorators) */
|
||||
.hljs-meta {
|
||||
color: #8b949e;
|
||||
}
|
||||
|
||||
/* Type annotations */
|
||||
.hljs-type {
|
||||
color: #7ee787;
|
||||
}
|
||||
|
||||
.hljs-section { color: #7ee787; }
|
||||
.hljs-tag { color: #ff7b72; }
|
||||
.hljs-link { color: #a5d6ff; }
|
||||
.hljs-subst { color: var(--text-normal); }
|
||||
.hljs-formula { color: #a5d6ff; }
|
||||
.hljs-addition { color: #7ee787; background-color: rgba(46, 160, 67, 0.15); }
|
||||
.hljs-deletion { color: #ff7b72; background-color: rgba(248, 81, 73, 0.15); }
|
||||
.hljs-selector-id, .hljs-selector-class { color: #7ee787; }
|
||||
.hljs-doctag, .hljs-strong { font-weight: bold; }
|
||||
.hljs-emphasis { font-style: italic; }
|
||||
|
||||
/* Ensure bold text renders properly */
|
||||
strong, b {
|
||||
font-weight: bold;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
em, i {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* For markdown-rendered content specifically */
|
||||
.markdown-rendered strong,
|
||||
.markdown-rendered b {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
Loading…
Reference in a new issue