gradio/frontend
1442k
1<script lang="ts">2 import { onMount, tick } from "svelte";3 import { create_marked } from "./utils";4 import { sanitize } from "@gradio/sanitize";5 import "./prism.css";6 import { standardHtmlAndSvgTags } from "./html-tags";7 import type { ThemeMode } from "@gradio/core";8 9 let {10 chatbot = true,11 message,12 sanitize_html = true,13 latex_delimiters = [],14 render_markdown = true,15 line_breaks = true,16 header_links = false,17 allow_tags = false,18 theme_mode = "system"19 }: {20 chatbot?: boolean;21 message: string;22 sanitize_html?: boolean;23 latex_delimiters?: {24 left: string;25 right: string;26 display: boolean;27 }[];28 render_markdown?: boolean | undefined;29 line_breaks?: boolean;30 header_links?: boolean;31 allow_tags?: string[] | boolean | undefined;32 theme_mode?: ThemeMode;33 } = $props();34 35 let el: HTMLSpanElement;36 37 const marked = create_marked({38 header_links,39 line_breaks,40 latex_delimiters: latex_delimiters || []41 });42 43 let html: string = $derived.by(() => {44 if (message && message.trim()) {45 return process_message(message);46 } else {47 return "";48 }49 });50 let katex_loaded = false;51 52 function has_math_syntax(text: string): boolean {53 if (!latex_delimiters || latex_delimiters.length === 0) {54 return false;55 }56 57 return latex_delimiters.some(58 (delimiter) =>59 text.includes(delimiter.left) && text.includes(delimiter.right)60 );61 }62 63 function escapeRegExp(string: string): string {64 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");65 }66 67 function escapeTags(68 content: string,69 tagsToEscape: string[] | boolean70 ): string {71 if (tagsToEscape === true) {72 // https://www.w3schools.com/tags/73 const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9-]*)([\s>])/g;74 return content.replace(tagRegex, (match, tagName, endChar) => {75 if (!standardHtmlAndSvgTags.includes(tagName.toLowerCase())) {76 return match.replace(/</g, "<").replace(/>/g, ">");77 }78 return match;79 });80 }81 82 if (Array.isArray(tagsToEscape)) {83 const tagPattern = tagsToEscape.map((tag) => ({84 open: new RegExp(`<(${tag})(\\s+[^>]*)?>`, "gi"),85 close: new RegExp(`</(${tag})>`, "gi")86 }));87 88 let result = content;89 90 tagPattern.forEach((pattern) => {91 result = result.replace(pattern.open, (match) =>92 match.replace(/</g, "<").replace(/>/g, ">")93 );94 result = result.replace(pattern.close, (match) =>95 match.replace(/</g, "<").replace(/>/g, ">")96 );97 });98 return result;99 }100 return content;101 }102 103 function process_message(value: string): string {104 let parsedValue = value;105 if (render_markdown) {106 const latexBlocks: string[] = [];107 latex_delimiters.forEach((delimiter, index) => {108 const leftDelimiter = escapeRegExp(delimiter.left);109 const rightDelimiter = escapeRegExp(delimiter.right);110 const regex = new RegExp(111 `${leftDelimiter}([\\s\\S]+?)${rightDelimiter}`,112 "g"113 );114 parsedValue = parsedValue.replace(regex, (match, p1) => {115 latexBlocks.push(match);116 return `%%%LATEX_BLOCK_${latexBlocks.length - 1}%%%`;117 });118 });119 120 parsedValue = marked.parse(parsedValue) as string;121 122 parsedValue = parsedValue.replace(123 /%%%LATEX_BLOCK_(\d+)%%%/g,124 (match, p1) => latexBlocks[parseInt(p1, 10)]125 );126 }127 128 if (allow_tags) {129 parsedValue = escapeTags(parsedValue, allow_tags);130 }131 132 if (sanitize_html && sanitize) {133 parsedValue = sanitize(parsedValue);134 }135 return parsedValue;136 }137 138 async function render_html(value: string): Promise<void> {139 if (latex_delimiters.length > 0 && value && has_math_syntax(value)) {140 if (!katex_loaded) {141 await Promise.all([142 import("katex/dist/katex.min.css"),143 import("katex/contrib/auto-render")144 ]).then(([, { default: render_math_in_element }]) => {145 katex_loaded = true;146 render_math_in_element(el, {147 delimiters: latex_delimiters,148 throwOnError: false149 });150 });151 } else {152 const { default: render_math_in_element } =153 await import("katex/contrib/auto-render");154 render_math_in_element(el, {155 delimiters: latex_delimiters,156 throwOnError: false157 });158 }159 }160 161 if (el) {162 const mermaidDivs = el.querySelectorAll(".mermaid");163 if (mermaidDivs.length > 0) {164 await tick();165 const { default: mermaid } = await import("mermaid");166 167 mermaid.initialize({168 startOnLoad: false,169 theme: theme_mode === "dark" ? "dark" : "default",170 securityLevel: "antiscript"171 });172 await mermaid.run({173 nodes: Array.from(mermaidDivs).map((node) => node as HTMLElement)174 });175 }176 }177 }178 179 $effect(() => {180 if (el && document.body.contains(el)) {181 render_html(message);182 } else {183 console.error("Element is not in the DOM");184 }185 });186</script>187 188<span class:chatbot bind:this={el} class="md" class:prose={render_markdown}>189 {@html html}190</span>191 192<style>193 span :global(div[class*="code_wrap"]) {194 position: relative;195 }196 197 /* KaTeX */198 span :global(span.katex) {199 font-size: var(--text-lg);200 direction: ltr;201 }202 203 span :global(div[class*="code_wrap"] > button) {204 z-index: 1;205 cursor: pointer;206 border-bottom-left-radius: var(--radius-sm);207 padding: var(--spacing-md);208 width: 25px;209 height: 25px;210 position: absolute;211 right: 0;212 }213 214 span :global(.check) {215 opacity: 0;216 z-index: var(--layer-top);217 transition: opacity 0.2s;218 background: var(--code-background-fill);219 color: var(--body-text-color);220 position: absolute;221 top: var(--size-1-5);222 left: var(--size-1-5);223 }224 225 span :global(p:not(:first-child)) {226 margin-top: var(--spacing-xxl);227 }228 229 span :global(.md-header-anchor) {230 /* position: absolute; */231 margin-left: -25px;232 padding-right: 8px;233 line-height: 1;234 color: var(--body-text-color-subdued);235 opacity: 0;236 }237 238 span :global(h1:hover .md-header-anchor),239 span :global(h2:hover .md-header-anchor),240 span :global(h3:hover .md-header-anchor),241 span :global(h4:hover .md-header-anchor),242 span :global(h5:hover .md-header-anchor),243 span :global(h6:hover .md-header-anchor) {244 opacity: 1;245 }246 247 span.md :global(.md-header-anchor > svg) {248 color: var(--body-text-color-subdued);249 }250 251 span :global(table) {252 word-break: break-word;253 }254</style>255 