chore: import upstream snapshot with attribution
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user