Kaynağa Gözat

enh: summary tag support

Timothy J. Baek 7 ay önce
ebeveyn
işleme
ffd598c5d7

+ 5 - 2
src/lib/components/chat/Messages/Markdown.svelte

@@ -1,9 +1,11 @@
 <script>
 	import { marked } from 'marked';
-	import markedKatex from '$lib/utils/marked/katex-extension';
 	import { replaceTokens, processResponseContent } from '$lib/utils';
 	import { user } from '$lib/stores';
 
+	import markedExtension from '$lib/utils/marked/extension';
+	import markedKatexExtension from '$lib/utils/marked/katex-extension';
+
 	import MarkdownTokens from './Markdown/MarkdownTokens.svelte';
 
 	export let id;
@@ -16,7 +18,8 @@
 		throwOnError: false
 	};
 
-	marked.use(markedKatex(options));
+	marked.use(markedKatexExtension(options));
+	marked.use(markedExtension(options));
 
 	$: (async () => {
 		if (content) {

+ 7 - 0
src/lib/components/chat/Messages/Markdown/MarkdownTokens.svelte

@@ -8,6 +8,7 @@
 	import MarkdownInlineTokens from '$lib/components/chat/Messages/Markdown/MarkdownInlineTokens.svelte';
 	import KatexRenderer from './KatexRenderer.svelte';
 	import { WEBUI_BASE_URL } from '$lib/constants';
+	import { stringify } from 'postcss';
 
 	export let id: string;
 	export let tokens: Token[];
@@ -94,6 +95,12 @@
 				{/each}
 			</ul>
 		{/if}
+	{:else if token.type === 'details'}
+		<details>
+			<summary>{token.summary}</summary>
+
+			<svelte:self id={`${id}-${tokenIdx}-d`} tokens={marked.lexer(token.text)} />
+		</details>
 	{:else if token.type === 'html'}
 		{@const html = DOMPurify.sanitize(token.text)}
 		{#if html && html.includes('<video')}

+ 70 - 0
src/lib/utils/marked/extension.ts

@@ -0,0 +1,70 @@
+// Helper function to find matching closing tag
+function findMatchingClosingTag(src, openTag, closeTag) {
+	let depth = 1;
+	let index = openTag.length;
+	while (depth > 0 && index < src.length) {
+		if (src.startsWith(openTag, index)) {
+			depth++;
+		} else if (src.startsWith(closeTag, index)) {
+			depth--;
+		}
+		if (depth > 0) {
+			index++;
+		}
+	}
+	return depth === 0 ? index + closeTag.length : -1;
+}
+
+function detailsTokenizer(src) {
+	const detailsRegex = /^<details>\n/;
+	const summaryRegex = /^<summary>(.*?)<\/summary>\n/;
+
+	if (detailsRegex.test(src)) {
+		const endIndex = findMatchingClosingTag(src, '<details>', '</details>');
+		if (endIndex === -1) return;
+
+		const fullMatch = src.slice(0, endIndex);
+		let content = fullMatch.slice(10, -10).trim(); // Remove <details> and </details>
+
+		let summary = '';
+		const summaryMatch = summaryRegex.exec(content);
+		if (summaryMatch) {
+			summary = summaryMatch[1].trim();
+			content = content.slice(summaryMatch[0].length).trim();
+		}
+
+		return {
+			type: 'details',
+			raw: fullMatch,
+			summary: summary,
+			text: content
+		};
+	}
+}
+
+function detailsStart(src) {
+	return src.match(/^<details>/) ? 0 : -1;
+}
+
+function detailsRenderer(token) {
+	return `<details>
+  ${token.summary ? `<summary>${token.summary}</summary>` : ''}
+  ${token.text}
+  </details>`;
+}
+
+function detailsExtension() {
+	return {
+		name: 'details',
+		level: 'block',
+		start: detailsStart,
+		tokenizer: detailsTokenizer,
+		renderer: detailsRenderer
+	};
+}
+
+export default function (options = {}) {
+	return {
+		extensions: [detailsExtension(options)]
+	};
+}