adf0d17497
publish / version_or_publish (push) Waiting to run
storybook-build / changes (push) Waiting to run
storybook-build / :storybook-build (push) Blocked by required conditions
Sync Gradio Skills to Hugging Face / sync-skills (push) Waiting to run
functional / changes (push) Waiting to run
functional / build-frontend (push) Blocked by required conditions
functional / functional-test-SSR=false (push) Blocked by required conditions
functional / functional-reload (push) Blocked by required conditions
functional / functional-test-SSR=true (push) Blocked by required conditions
hygiene / hygiene-test (push) Waiting to run
python / changes (push) Waiting to run
python / build (push) Blocked by required conditions
python / test-ubuntu-latest-flaky (push) Blocked by required conditions
python / test-ubuntu-latest-not-flaky (push) Blocked by required conditions
python / test-windows-latest-flaky (push) Blocked by required conditions
python / test-windows-latest-not-flaky (push) Blocked by required conditions
js / changes (push) Waiting to run
js / js-test (push) Blocked by required conditions
docs-build / changes (push) Waiting to run
docs-build / docs-build (push) Blocked by required conditions
docs-build / website-build (push) Blocked by required conditions
264 lines
6.3 KiB
Svelte
264 lines
6.3 KiB
Svelte
<script lang="ts">
|
|
import { onMount, tick } from "svelte";
|
|
import { create_marked } from "./utils";
|
|
import { sanitize } from "@gradio/sanitize";
|
|
import "./prism.css";
|
|
import { standardHtmlAndSvgTags } from "./html-tags";
|
|
import type { ThemeMode } from "@gradio/core";
|
|
|
|
let {
|
|
chatbot = true,
|
|
message,
|
|
sanitize_html = true,
|
|
latex_delimiters = [],
|
|
render_markdown = true,
|
|
line_breaks = true,
|
|
header_links = false,
|
|
allow_tags = false,
|
|
theme_mode = "system",
|
|
onload
|
|
}: {
|
|
chatbot?: boolean;
|
|
message: string;
|
|
sanitize_html?: boolean;
|
|
latex_delimiters?: {
|
|
left: string;
|
|
right: string;
|
|
display: boolean;
|
|
}[];
|
|
render_markdown?: boolean | undefined;
|
|
line_breaks?: boolean;
|
|
header_links?: boolean;
|
|
allow_tags?: string[] | boolean | undefined;
|
|
theme_mode?: ThemeMode;
|
|
onload?: () => void;
|
|
} = $props();
|
|
|
|
let el: HTMLSpanElement;
|
|
|
|
const marked = create_marked({
|
|
header_links,
|
|
line_breaks,
|
|
latex_delimiters: latex_delimiters || []
|
|
});
|
|
|
|
let html: string = $derived.by(() => {
|
|
if (message && message.trim()) {
|
|
return process_message(message);
|
|
} else {
|
|
return "";
|
|
}
|
|
});
|
|
let katex_loaded = false;
|
|
|
|
function has_math_syntax(text: string): boolean {
|
|
if (!latex_delimiters || latex_delimiters.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return latex_delimiters.some(
|
|
(delimiter) =>
|
|
text.includes(delimiter.left) && text.includes(delimiter.right)
|
|
);
|
|
}
|
|
|
|
function escapeRegExp(string: string): string {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
|
|
function escapeTags(
|
|
content: string,
|
|
tagsToEscape: string[] | boolean
|
|
): string {
|
|
if (tagsToEscape === true) {
|
|
// https://www.w3schools.com/tags/
|
|
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9-]*)([\s>])/g;
|
|
return content.replace(tagRegex, (match, tagName, endChar) => {
|
|
if (!standardHtmlAndSvgTags.includes(tagName.toLowerCase())) {
|
|
return match.replace(/</g, "<").replace(/>/g, ">");
|
|
}
|
|
return match;
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(tagsToEscape)) {
|
|
const tagPattern = tagsToEscape.map((tag) => ({
|
|
open: new RegExp(`<(${tag})(\\s+[^>]*)?>`, "gi"),
|
|
close: new RegExp(`</(${tag})>`, "gi")
|
|
}));
|
|
|
|
let result = content;
|
|
|
|
tagPattern.forEach((pattern) => {
|
|
result = result.replace(pattern.open, (match) =>
|
|
match.replace(/</g, "<").replace(/>/g, ">")
|
|
);
|
|
result = result.replace(pattern.close, (match) =>
|
|
match.replace(/</g, "<").replace(/>/g, ">")
|
|
);
|
|
});
|
|
return result;
|
|
}
|
|
return content;
|
|
}
|
|
|
|
function process_message(value: string): string {
|
|
let parsedValue = value;
|
|
if (render_markdown) {
|
|
const latexBlocks: string[] = [];
|
|
latex_delimiters.forEach((delimiter, index) => {
|
|
const leftDelimiter = escapeRegExp(delimiter.left);
|
|
const rightDelimiter = escapeRegExp(delimiter.right);
|
|
const regex = new RegExp(
|
|
`${leftDelimiter}([\\s\\S]+?)${rightDelimiter}`,
|
|
"g"
|
|
);
|
|
parsedValue = parsedValue.replace(regex, (match, p1) => {
|
|
latexBlocks.push(match);
|
|
return `%%%LATEX_BLOCK_${latexBlocks.length - 1}%%%`;
|
|
});
|
|
});
|
|
|
|
parsedValue = marked.parse(parsedValue) as string;
|
|
|
|
parsedValue = parsedValue.replace(
|
|
/%%%LATEX_BLOCK_(\d+)%%%/g,
|
|
(match, p1) => latexBlocks[parseInt(p1, 10)]
|
|
);
|
|
}
|
|
|
|
if (allow_tags) {
|
|
parsedValue = escapeTags(parsedValue, allow_tags);
|
|
}
|
|
|
|
if (sanitize_html && sanitize) {
|
|
parsedValue = sanitize(parsedValue);
|
|
}
|
|
return parsedValue;
|
|
}
|
|
|
|
async function render_html(value: string): Promise<void> {
|
|
if (latex_delimiters.length > 0 && value && has_math_syntax(value)) {
|
|
if (!katex_loaded) {
|
|
await Promise.all([
|
|
import("katex/dist/katex.min.css"),
|
|
import("katex/contrib/auto-render")
|
|
]).then(([, { default: render_math_in_element }]) => {
|
|
katex_loaded = true;
|
|
render_math_in_element(el, {
|
|
delimiters: latex_delimiters,
|
|
throwOnError: false
|
|
});
|
|
});
|
|
} else {
|
|
const { default: render_math_in_element } =
|
|
await import("katex/contrib/auto-render");
|
|
render_math_in_element(el, {
|
|
delimiters: latex_delimiters,
|
|
throwOnError: false
|
|
});
|
|
}
|
|
}
|
|
|
|
if (el) {
|
|
const mermaidDivs = el.querySelectorAll(".mermaid");
|
|
if (mermaidDivs.length > 0) {
|
|
await tick();
|
|
const { default: mermaid } = await import("mermaid");
|
|
|
|
mermaid.initialize({
|
|
startOnLoad: false,
|
|
theme: theme_mode === "dark" ? "dark" : "default",
|
|
securityLevel: "antiscript"
|
|
});
|
|
await mermaid.run({
|
|
nodes: Array.from(mermaidDivs).map((node) => node as HTMLElement)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
$effect(() => {
|
|
if (el && document.body.contains(el)) {
|
|
render_html(message).then(() => onload?.());
|
|
} else {
|
|
console.error("Element is not in the DOM");
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<span class:chatbot bind:this={el} class="md" class:prose={render_markdown}>
|
|
{@html html}
|
|
</span>
|
|
|
|
<style>
|
|
span {
|
|
/* `anywhere`, not `break-word`: it also lowers min-content width, so
|
|
markdown in a table/flex container shrinks to fit instead of forcing
|
|
the container to overflow. */
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
span :global(div[class*="code_wrap"]) {
|
|
position: relative;
|
|
}
|
|
|
|
/* KaTeX */
|
|
span :global(span.katex) {
|
|
font-size: var(--text-lg);
|
|
direction: ltr;
|
|
}
|
|
|
|
span :global(div[class*="code_wrap"] > button) {
|
|
z-index: 1;
|
|
cursor: pointer;
|
|
border-bottom-left-radius: var(--radius-sm);
|
|
padding: var(--spacing-md);
|
|
width: 25px;
|
|
height: 25px;
|
|
position: absolute;
|
|
right: 0;
|
|
}
|
|
|
|
span :global(.check) {
|
|
opacity: 0;
|
|
z-index: var(--layer-top);
|
|
transition: opacity 0.2s;
|
|
background: var(--code-background-fill);
|
|
color: var(--body-text-color);
|
|
position: absolute;
|
|
top: var(--size-1-5);
|
|
left: var(--size-1-5);
|
|
}
|
|
|
|
span :global(p:not(:first-child)) {
|
|
margin-top: var(--spacing-xxl);
|
|
}
|
|
|
|
span :global(.md-header-anchor) {
|
|
/* position: absolute; */
|
|
margin-left: -25px;
|
|
padding-right: 8px;
|
|
line-height: 1;
|
|
color: var(--body-text-color-subdued);
|
|
opacity: 0;
|
|
}
|
|
|
|
span :global(h1:hover .md-header-anchor),
|
|
span :global(h2:hover .md-header-anchor),
|
|
span :global(h3:hover .md-header-anchor),
|
|
span :global(h4:hover .md-header-anchor),
|
|
span :global(h5:hover .md-header-anchor),
|
|
span :global(h6:hover .md-header-anchor) {
|
|
opacity: 1;
|
|
}
|
|
|
|
span.md :global(.md-header-anchor > svg) {
|
|
color: var(--body-text-color-subdued);
|
|
}
|
|
|
|
span :global(table) {
|
|
word-break: break-word;
|
|
}
|
|
</style>
|