mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-18 04:43:55 +00:00
refac
This commit is contained in:
parent
7ca62fdf93
commit
c9a52208da
7 changed files with 187 additions and 62 deletions
|
|
@ -1,11 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { getContext, tick } from 'svelte';
|
||||
import { marked } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import MarkdownDocument from './MarkdownDocument.svelte';
|
||||
import { settings, config } from '$lib/stores';
|
||||
import { injectCsp } from '$lib/utils/csp';
|
||||
import { isCodeFile } from '$lib/utils/codeHighlight';
|
||||
import { initMermaid, renderMermaidDiagram, sanitizeSvg } from '$lib/utils';
|
||||
import { sanitizeSvg } from '$lib/utils';
|
||||
import Spinner from '../../common/Spinner.svelte';
|
||||
import PdfPagesPreview from '../../common/PdfPagesPreview.svelte';
|
||||
import PanzoomContainer from '../../common/PanzoomContainer.svelte';
|
||||
|
|
@ -125,46 +124,6 @@
|
|||
isHtml && selectedFile && baseUrl && baseUrl.includes('/api/v1/terminals/')
|
||||
? `${baseUrl}/files/serve/${selectedFile.replace(/^\//, '')}`
|
||||
: null;
|
||||
$: renderedHtml =
|
||||
isMarkdown && fileContent
|
||||
? DOMPurify.sanitize(marked.parse(fileContent, { async: false }) as string)
|
||||
: '';
|
||||
|
||||
let markdownEl: HTMLDivElement;
|
||||
let mermaidInstance: any = null;
|
||||
|
||||
const renderMermaidBlocks = async (el: HTMLDivElement) => {
|
||||
if (!el) return;
|
||||
const codeEls = el.querySelectorAll('code.language-mermaid');
|
||||
if (codeEls.length === 0) return;
|
||||
|
||||
if (!mermaidInstance) {
|
||||
mermaidInstance = await initMermaid();
|
||||
}
|
||||
|
||||
for (const codeEl of codeEls) {
|
||||
const pre = codeEl.parentElement;
|
||||
if (!pre || pre.tagName !== 'PRE' || pre.dataset.mermaidRendered) continue;
|
||||
pre.dataset.mermaidRendered = 'true';
|
||||
|
||||
try {
|
||||
const svg = await renderMermaidDiagram(mermaidInstance, codeEl.textContent ?? '');
|
||||
if (svg) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'mermaid-diagram flex justify-center py-2';
|
||||
wrapper.innerHTML = svg;
|
||||
pre.replaceWith(wrapper);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Mermaid render error:', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$: if (renderedHtml && markdownEl) {
|
||||
tick().then(() => renderMermaidBlocks(markdownEl));
|
||||
}
|
||||
|
||||
// Simple CSV parser that handles quoted fields
|
||||
const parseCsv = (text: string, delimiter: string): string[][] => {
|
||||
const rows: string[][] = [];
|
||||
|
|
@ -424,9 +383,9 @@
|
|||
/>
|
||||
</div>
|
||||
{:else if isMarkdown && !showRaw}
|
||||
<div bind:this={markdownEl} class="prose dark:prose-invert max-w-full text-sm p-3">
|
||||
{@html renderedHtml}
|
||||
</div>
|
||||
{#key selectedFile}
|
||||
<MarkdownDocument content={fileContent} id={`file-${selectedFile}`} />
|
||||
{/key}
|
||||
{:else if isMarkdown && showRaw}
|
||||
<div class="absolute inset-0">
|
||||
<FileCodeEditor
|
||||
|
|
|
|||
133
src/lib/components/chat/FileNav/MarkdownDocument.svelte
Normal file
133
src/lib/components/chat/FileNav/MarkdownDocument.svelte
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
<script lang="ts" context="module">
|
||||
import { Marked } from 'marked';
|
||||
import markedKatexExtension from '$lib/utils/marked/katex-extension';
|
||||
import { footnoteExtension } from '$lib/utils/marked/footnote-extension';
|
||||
|
||||
// Documents must not inherit chat's global extensions or soft-line-break settings.
|
||||
const documentMarkdown = new Marked(markedKatexExtension(), {
|
||||
breaks: false,
|
||||
extensions: [
|
||||
{
|
||||
...footnoteExtension(),
|
||||
renderer: (token) => `<sup class="footnote-ref">${token.escapedText}</sup>`
|
||||
}
|
||||
]
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte';
|
||||
import MarkdownTokens from '../Messages/Markdown/MarkdownTokens.svelte';
|
||||
import type { Token } from 'marked';
|
||||
|
||||
export let content: string;
|
||||
export let id: string;
|
||||
|
||||
setContext('markdown', {
|
||||
document: true,
|
||||
allowRemoteContent: true,
|
||||
lexer: (source: string) => documentMarkdown.lexer(source),
|
||||
parseInline: (tokens: Token[]) =>
|
||||
documentMarkdown.Parser.parseInline(tokens, documentMarkdown.defaults)
|
||||
});
|
||||
$: tokens = documentMarkdown.lexer(content);
|
||||
</script>
|
||||
|
||||
<div class="document-markdown prose prose-sm dark:prose-invert min-w-0 max-w-none p-3">
|
||||
<MarkdownTokens {id} {tokens} editCodeBlock={false} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.document-markdown {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.document-markdown :global(h1),
|
||||
.document-markdown :global(h2),
|
||||
.document-markdown :global(h3),
|
||||
.document-markdown :global(h4),
|
||||
.document-markdown :global(h5),
|
||||
.document-markdown :global(h6) {
|
||||
margin: 1rem 0 0.375rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.document-markdown :global(h1) {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
.document-markdown :global(h2) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.document-markdown :global(h3) {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.document-markdown :global(h4),
|
||||
.document-markdown :global(h5),
|
||||
.document-markdown :global(h6) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.document-markdown :global(p),
|
||||
.document-markdown :global(ul),
|
||||
.document-markdown :global(ol) {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.document-markdown :global(li) {
|
||||
margin: 0.125rem 0;
|
||||
}
|
||||
.document-markdown :global(blockquote) {
|
||||
margin: 0.75rem 0;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
.document-markdown :global(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
.document-markdown :global(hr) {
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
.document-markdown :global(img) {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.document-markdown :global(table) {
|
||||
font-size: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.document-markdown :global(th),
|
||||
.document-markdown :global(td) {
|
||||
padding: 0.375rem 0.5rem !important;
|
||||
}
|
||||
.document-markdown :global(pre) {
|
||||
margin: 0.5rem 0;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.document-markdown :global(pre.hljs) {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
.document-markdown :global(pre code) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.document-markdown :global(.codespan) {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.document-markdown :global(input[type='checkbox']) {
|
||||
cursor: default;
|
||||
}
|
||||
.document-markdown :global(.document-code-block) {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.document-markdown :global(.document-code-block > .sticky) {
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.document-markdown :global(.document-code-block > .sticky button) {
|
||||
height: 1.5rem;
|
||||
padding-inline: 0.375rem;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
.document-markdown > :global(:first-child) {
|
||||
margin-top: 0;
|
||||
}
|
||||
.document-markdown > :global(:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -30,7 +30,9 @@
|
|||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
const markdown = getContext<{ allowRemoteContent: boolean } | undefined>('markdown');
|
||||
const markdown = getContext<{ allowRemoteContent: boolean; document?: boolean } | undefined>(
|
||||
'markdown'
|
||||
);
|
||||
|
||||
export let id = '';
|
||||
export let edit = true;
|
||||
|
|
@ -370,7 +372,10 @@
|
|||
const render = async () => {
|
||||
onUpdate(token, id);
|
||||
if (markdown?.allowRemoteContent === false) return;
|
||||
if (lang === 'mermaid' && (token?.raw ?? '').slice(-4).includes('```')) {
|
||||
if (
|
||||
lang === 'mermaid' &&
|
||||
(markdown?.document || (token?.raw ?? '').slice(-4).includes('```'))
|
||||
) {
|
||||
try {
|
||||
renderHTML = await renderMermaid(code);
|
||||
} catch (error) {
|
||||
|
|
@ -381,7 +386,7 @@
|
|||
}
|
||||
} else if (
|
||||
(lang === 'vega' || lang === 'vega-lite') &&
|
||||
(token?.raw ?? '').slice(-4).includes('```')
|
||||
(markdown?.document || (token?.raw ?? '').slice(-4).includes('```'))
|
||||
) {
|
||||
try {
|
||||
renderHTML = await renderVegaVisualization(code, lang);
|
||||
|
|
|
|||
|
|
@ -43,7 +43,10 @@
|
|||
}
|
||||
};
|
||||
|
||||
export function alertComponent(token: Token): AlertData | false {
|
||||
export function alertComponent(
|
||||
token: Token,
|
||||
lexer: (source: string) => Token[] = marked.lexer
|
||||
): AlertData | false {
|
||||
const regExpStr = `^(?:\\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\\])\\s*?\n*`;
|
||||
const regExp = new RegExp(regExpStr);
|
||||
const matches = token.text?.match(regExp);
|
||||
|
|
@ -51,7 +54,7 @@
|
|||
if (matches && matches.length) {
|
||||
const alertType = matches[1] as AlertType;
|
||||
const newText = token.text.replace(regExp, '');
|
||||
const newTokens = marked.lexer(newText);
|
||||
const newTokens = lexer(newText);
|
||||
return {
|
||||
type: alertType,
|
||||
text: newText,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { settings } from '$lib/stores';
|
||||
|
||||
const i18n: any = getContext('i18n');
|
||||
const markdown = getContext<{ document?: boolean } | undefined>('markdown');
|
||||
|
||||
export let id: string;
|
||||
export let token: Token;
|
||||
|
|
@ -18,7 +19,9 @@
|
|||
</script>
|
||||
|
||||
{#if token.type === 'html'}
|
||||
{#if html && html.includes('<video')}
|
||||
{#if markdown?.document}
|
||||
{@html html ?? ''}
|
||||
{:else if html && html.includes('<video')}
|
||||
{@const video = html.match(/<video[^>]*>([\s\S]*?)<\/video>/)}
|
||||
{@const videoSrc = video && video[1]}
|
||||
{#if videoSrc}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@
|
|||
import { goto } from '$app/navigation';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const markdown = getContext<{ allowRemoteContent: boolean } | undefined>('markdown');
|
||||
const markdown = getContext<
|
||||
| { allowRemoteContent: boolean; document?: boolean; parseInline?: (tokens: Token[]) => string }
|
||||
| undefined
|
||||
>('markdown');
|
||||
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { skills, terminalSkills } from '$lib/stores';
|
||||
|
|
@ -82,16 +85,24 @@
|
|||
// Invalid URL, let browser handle it
|
||||
}
|
||||
};
|
||||
$: inlineHtml =
|
||||
markdown?.document && tokens.some((token) => token.type === 'html')
|
||||
? DOMPurify.sanitize(markdown.parseInline?.(tokens) ?? '')
|
||||
: null;
|
||||
</script>
|
||||
|
||||
{#each tokens as token, tokenIdx (tokenIdx)}
|
||||
<!-- Keep paired inline HTML tags together; sanitizing individual tags loses their contents. -->
|
||||
{#if inlineHtml !== null}
|
||||
{@html inlineHtml}
|
||||
{/if}
|
||||
{#each inlineHtml === null ? tokens : [] as token, tokenIdx (tokenIdx)}
|
||||
{#if token.type === 'escape'}
|
||||
{unescapeHtml(token.text)}
|
||||
{:else if token.type === 'html'}
|
||||
<HtmlToken {id} {token} {onSourceClick} />
|
||||
{:else if token.type === 'link'}
|
||||
{@const linkToken = getLinkToken(token)}
|
||||
{#if linkToken.noteId && markdown?.allowRemoteContent !== false}
|
||||
{#if linkToken.noteId && !markdown?.document && markdown?.allowRemoteContent !== false}
|
||||
<NoteLinkToken noteId={linkToken.noteId} href={linkToken.href} />
|
||||
{:else if token.tokens && !linkToken.isAutolink}
|
||||
<a
|
||||
|
|
@ -162,6 +173,10 @@
|
|||
<TextToken {token} {done} />
|
||||
{/if}
|
||||
{:else if token.type === 'text'}
|
||||
<TextToken {token} {done} />
|
||||
{#if markdown?.document}
|
||||
{unescapeHtml(token.text)}
|
||||
{:else}
|
||||
<TextToken {token} {done} />
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
import { decode } from 'html-entities';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
const markdown = getContext<
|
||||
{ document?: boolean; lexer?: (source: string) => Token[] } | undefined
|
||||
>('markdown');
|
||||
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
|
@ -184,17 +187,19 @@
|
|||
/>
|
||||
</svelte:element>
|
||||
{:else if token.type === 'code'}
|
||||
{#if token.codeBlockStyle !== 'indented'}
|
||||
{#if markdown?.document || token.codeBlockStyle !== 'indented'}
|
||||
<CodeBlock
|
||||
id={`${id}-${tokenIdx}`}
|
||||
collapsed={$settings?.collapseCodeBlocks ?? false}
|
||||
className={markdown?.document ? 'document-code-block' : ''}
|
||||
collapsed={markdown?.document ? false : ($settings?.collapseCodeBlocks ?? false)}
|
||||
{token}
|
||||
lang={token?.lang ?? ''}
|
||||
code={token?.text ?? ''}
|
||||
{attributes}
|
||||
{save}
|
||||
{preview}
|
||||
edit={editCodeBlock}
|
||||
save={save && !markdown?.document}
|
||||
preview={preview && !markdown?.document}
|
||||
run={!markdown?.document}
|
||||
edit={editCodeBlock && !markdown?.document}
|
||||
stickyButtonsClassName={topPadding ? 'top-10' : 'top-0'}
|
||||
onSave={(value) => {
|
||||
onSave({
|
||||
|
|
@ -295,7 +300,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else if token.type === 'blockquote'}
|
||||
{@const alert = alertComponent(token)}
|
||||
{@const alert = alertComponent(token, markdown?.lexer)}
|
||||
{#if alert}
|
||||
<AlertRenderer {token} {alert} {allowEmbeds} />
|
||||
{:else}
|
||||
|
|
@ -327,6 +332,7 @@
|
|||
<input
|
||||
class=" translate-y-[1px] -translate-x-1 flex-shrink-0"
|
||||
type="checkbox"
|
||||
disabled={markdown?.document ?? false}
|
||||
checked={item.checked}
|
||||
on:change={(e) => {
|
||||
onTaskClick({
|
||||
|
|
@ -368,6 +374,7 @@
|
|||
<input
|
||||
class="flex-shrink-0"
|
||||
type="checkbox"
|
||||
disabled={markdown?.document ?? false}
|
||||
checked={item.checked}
|
||||
on:change={(e) => {
|
||||
onTaskClick({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue