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