@quasiris/qsc-chatbot-ui
Version:
A plugin that injects a Chatbot UI with WS/REST support
4,393 lines • 206 kB
JavaScript
(function () {
'use strict';
/**
* marked v16.1.1 - a markdown parser
* Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed)
* https://github.com/markedjs/marked
*/
/**
* DO NOT EDIT THIS FILE
* The code in this file is generated from files in ./src/
*/
function L(){return {async:false,breaks:false,extensions:null,gfm:true,hooks:null,pedantic:false,renderer:null,silent:false,tokenizer:null,walkTokens:null}}var O=L();function H(l){O=l;}var E={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(r,i)=>{let s=typeof i=="string"?i:i.source;return s=s.replace(m.caret,"$1"),t=t.replace(r,s),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<a /i,endATag:/^<\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},xe=/^(?:[ \t]*(?:\n|$))+/,be=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Re=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,C=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Oe=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,j=/(?:[*+-]|\d{1,9}[.)])/,se=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,ie=h(se).replace(/bull/g,j).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Te=h(se).replace(/bull/g,j).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),F=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,we=/^[^\n]+/,Q=/(?!\s*\])(?:\\.|[^\[\]\\])+/,ye=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",Q).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Pe=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,j).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",U=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,Se=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",U).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),oe=h(F).replace("hr",C).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),$e=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",oe).getRegex(),K={blockquote:$e,code:be,def:ye,fences:Re,heading:Oe,hr:C,html:Se,lheading:ie,list:Pe,newline:xe,paragraph:oe,table:E,text:we},re=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",C).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),_e={...K,lheading:Te,table:re,paragraph:h(F).replace("hr",C).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",re).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Le={...K,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:"[^"]*"|'[^']*'|\\s[^'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",U).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:E,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(F).replace("hr",C).replace("heading",` *#{1,6} *[^
]`).replace("lheading",ie).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Me=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ze=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,Ae=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,D=/[\p{P}\p{S}]/u,X=/[\s\p{P}\p{S}]/u,le=/[^\s\p{P}\p{S}]/u,Ee=h(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,X).getRegex(),ue=/(?!~)[\p{P}\p{S}]/u,Ce=/(?!~)[\s\p{P}\p{S}]/u,Ie=/(?:[^\s\p{P}\p{S}]|~)/u,Be=/\[[^[\]]*?\]\((?:\\.|[^\\\(\)]|\((?:\\.|[^\\\(\)])*\))*\)|`[^`]*?`|<(?! )[^<>]*?>/g,pe=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,qe=h(pe,"u").replace(/punct/g,D).getRegex(),ve=h(pe,"u").replace(/punct/g,ue).getRegex(),ce="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",De=h(ce,"gu").replace(/notPunctSpace/g,le).replace(/punctSpace/g,X).replace(/punct/g,D).getRegex(),Ze=h(ce,"gu").replace(/notPunctSpace/g,Ie).replace(/punctSpace/g,Ce).replace(/punct/g,ue).getRegex(),Ge=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,le).replace(/punctSpace/g,X).replace(/punct/g,D).getRegex(),He=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),Ne=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),je=h(U).replace("(?:-->|$)","-->").getRegex(),Fe=h("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,Qe=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),he=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",Q).getRegex(),de=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",Q).getRegex(),Ue=h("reflink|nolink(?!\\()","g").replace("reflink",he).replace("nolink",de).getRegex(),W={_backpedal:E,anyPunctuation:He,autolink:Ne,blockSkip:Be,br:ae,code:ze,del:E,emStrongLDelim:qe,emStrongRDelimAst:De,emStrongRDelimUnd:Ge,escape:Me,link:Qe,nolink:de,punctuation:Ee,reflink:he,reflinkSearch:Ue,tag:Fe,text:Ae,url:E},Ke={...W,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},N={...W,emStrongRDelimAst:Ze,emStrongLDelim:ve,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/},Xe={...N,br:h(ae).replace("{2,}","*").getRegex(),text:h(N.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},I={normal:K,gfm:_e,pedantic:Le},M={normal:W,gfm:N,breaks:Xe,pedantic:Ke};var We={"&":"&","<":"<",">":">",'"':""","'":"'"},ke=l=>We[l];function w(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,ke)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,ke);return l}function J(l){try{l=encodeURI(l).replace(m.percentDecode,"%");}catch{return null}return l}function V(l,e){let t=l.replace(m.findPipe,(i,s,o)=>{let a=false,u=s;for(;--u>=0&&o[u]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),r=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length<e;)n.push("");for(;r<n.length;r++)n[r]=n[r].trim().replace(m.slashPipe,"|");return n}function z(l,e,t){let n=l.length;if(n===0)return "";let r=0;for(;r<n;){let i=l.charAt(n-r-1);if(i===e&&true)r++;else break}return l.slice(0,n-r)}function ge(l,e){if(l.indexOf(e[1])===-1)return -1;let t=0;for(let n=0;n<l.length;n++)if(l[n]==="\\")n++;else if(l[n]===e[0])t++;else if(l[n]===e[1]&&(t--,t<0))return n;return t>0?-2:-1}function fe(l,e,t,n,r){let i=e.href,s=e.title||null,o=l[1].replace(r.other.outputLinkReplace,"$1");n.state.inLink=true;let a={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:s,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=false,a}function Je(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let r=n[1];return e.split(`
`).map(i=>{let s=i.match(t.other.beginningSpace);if(s===null)return i;let[o]=s;return o.length>=r.length?i.slice(r.length):i}).join(`
`)}var y=class{options;rules;lexer;constructor(e){this.options=e||O;}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return {type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return {type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:z(n,`
`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],r=Je(n,t[3]||"",this.rules);return {type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:r}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let r=z(n,"#");(this.options.pedantic||!r||this.rules.other.endingSpaceChar.test(r))&&(n=r.trim());}return {type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return {type:"hr",raw:z(t[0],`
`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=z(t[0],`
`).split(`
`),r="",i="",s=[];for(;n.length>0;){let o=false,a=[],u;for(u=0;u<n.length;u++)if(this.rules.other.blockquoteStart.test(n[u]))a.push(n[u]),o=true;else if(!o)a.push(n[u]);else break;n=n.slice(u);let p=a.join(`
`),c=p.replace(this.rules.other.blockquoteSetextReplace,`
$1`).replace(this.rules.other.blockquoteSetextReplace2,"");r=r?`${r}
${p}`:p,i=i?`${i}
${c}`:c;let f=this.lexer.state.top;if(this.lexer.state.top=true,this.lexer.blockTokens(c,s,true),this.lexer.state.top=f,n.length===0)break;let k=s.at(-1);if(k?.type==="code")break;if(k?.type==="blockquote"){let x=k,g=x.raw+`
`+n.join(`
`),T=this.blockquote(g);s[s.length-1]=T,r=r.substring(0,r.length-x.raw.length)+T.raw,i=i.substring(0,i.length-x.text.length)+T.text;break}else if(k?.type==="list"){let x=k,g=x.raw+`
`+n.join(`
`),T=this.list(g);s[s.length-1]=T,r=r.substring(0,r.length-k.raw.length)+T.raw,i=i.substring(0,i.length-x.raw.length)+T.raw,n=g.substring(s.at(-1).raw.length).split(`
`);continue}}return {type:"blockquote",raw:r,tokens:s,text:i}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim(),r=n.length>1,i={type:"list",raw:"",ordered:r,start:r?+n.slice(0,-1):"",loose:false,items:[]};n=r?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=r?n:"[*+-]");let s=this.rules.other.listItemRegex(n),o=false;for(;e;){let u=false,p="",c="";if(!(t=s.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let f=t[2].split(`
`,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),k=e.split(`
`,1)[0],x=!f.trim(),g=0;if(this.options.pedantic?(g=2,c=f.trimStart()):x?g=t[1].length+1:(g=t[2].search(this.rules.other.nonSpaceChar),g=g>4?1:g,c=f.slice(g),g+=t[1].length),x&&this.rules.other.blankLine.test(k)&&(p+=k+`
`,e=e.substring(k.length+1),u=true),!u){let Z=this.rules.other.nextBulletRegex(g),ee=this.rules.other.hrRegex(g),te=this.rules.other.fencesBeginRegex(g),ne=this.rules.other.headingBeginRegex(g),me=this.rules.other.htmlBeginRegex(g);for(;e;){let G=e.split(`
`,1)[0],A;if(k=G,this.options.pedantic?(k=k.replace(this.rules.other.listReplaceNesting," "),A=k):A=k.replace(this.rules.other.tabCharGlobal," "),te.test(k)||ne.test(k)||me.test(k)||Z.test(k)||ee.test(k))break;if(A.search(this.rules.other.nonSpaceChar)>=g||!k.trim())c+=`
`+A.slice(g);else {if(x||f.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||te.test(f)||ne.test(f)||ee.test(f))break;c+=`
`+k;}!x&&!k.trim()&&(x=true),p+=G+`
`,e=e.substring(G.length+1),f=A.slice(g);}}i.loose||(o?i.loose=true:this.rules.other.doubleBlankLine.test(p)&&(o=true));let T=null,Y;this.options.gfm&&(T=this.rules.other.listIsTask.exec(c),T&&(Y=T[0]!=="[ ] ",c=c.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!T,checked:Y,loose:false,text:c,tokens:[]}),i.raw+=p;}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let u=0;u<i.items.length;u++)if(this.lexer.state.top=false,i.items[u].tokens=this.lexer.blockTokens(i.items[u].text,[]),!i.loose){let p=i.items[u].tokens.filter(f=>f.type==="space"),c=p.length>0&&p.some(f=>this.rules.other.anyLine.test(f.raw));i.loose=c;}if(i.loose)for(let u=0;u<i.items.length;u++)i.items[u].loose=true;return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return {type:"html",block:true,raw:t[0],pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),r=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return {type:"def",tag:n,raw:t[0],href:r,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=V(t[1]),r=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(`
`):[],s={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===r.length){for(let o of r)this.rules.other.tableAlignRight.test(o)?s.align.push("right"):this.rules.other.tableAlignCenter.test(o)?s.align.push("center"):this.rules.other.tableAlignLeft.test(o)?s.align.push("left"):s.align.push(null);for(let o=0;o<n.length;o++)s.header.push({text:n[o],tokens:this.lexer.inline(n[o]),header:true,align:s.align[o]});for(let o of i)s.rows.push(V(o,s.header.length).map((a,u)=>({text:a,tokens:this.lexer.inline(a),header:false,align:s.align[u]})));return s}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return {type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===`
`?t[1].slice(0,-1):t[1];return {type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return {type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return {type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return !this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=true:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=false),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=true:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=false),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:false,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let s=z(n.slice(0,-1),"\\");if((n.length-s.length)%2===0)return}else {let s=ge(t[2],"()");if(s===-2)return;if(s>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+s;t[2]=t[2].substring(0,s),t[0]=t[0].substring(0,a).trim(),t[3]="";}}let r=t[2],i="";if(this.options.pedantic){let s=this.rules.other.pedanticHrefTitle.exec(r);s&&(r=s[1],i=s[3]);}else i=t[3]?t[3].slice(1,-1):"";return r=r.trim(),this.rules.other.startAngleBracket.test(r)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?r=r.slice(1):r=r.slice(1,-1)),fe(t,{href:r&&r.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let r=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[r.toLowerCase()];if(!i){let s=n[0].charAt(0);return {type:"text",raw:s,text:s}}return fe(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let r=this.rules.inline.emStrongLDelim.exec(e);if(!r||r[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(r[1]||r[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let s=[...r[0]].length-1,o,a,u=s,p=0,c=r[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(c.lastIndex=0,t=t.slice(-1*e.length+s);(r=c.exec(t))!=null;){if(o=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!o)continue;if(a=[...o].length,r[3]||r[4]){u+=a;continue}else if((r[5]||r[6])&&s%3&&!((s+a)%3)){p+=a;continue}if(u-=a,u>0)continue;a=Math.min(a,a+u+p);let f=[...r[0]][0].length,k=e.slice(0,s+r.index+f+a);if(Math.min(s,a)%2){let g=k.slice(1,-1);return {type:"em",raw:k,text:g,tokens:this.lexer.inlineTokens(g)}}let x=k.slice(2,-2);return {type:"strong",raw:k,text:x,tokens:this.lexer.inlineTokens(x)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),r=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return r&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return {type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return {type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,r;return t[2]==="@"?(n=t[1],r="mailto:"+n):(n=t[1],r=n),{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,r;if(t[2]==="@")n=t[0],r="mailto:"+n;else {let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?r="http://"+t[0]:r=t[0];}return {type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return {type:"text",raw:t[0],text:t[0],escaped:n}}}};var b=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||O,this.options.tokenizer=this.options.tokenizer||new y,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:false,inRawBlock:false,top:true};let t={other:m,block:I.normal,inline:M.normal};this.options.pedantic?(t.block=I.pedantic,t.inline=M.pedantic):this.options.gfm&&(t.block=I.gfm,this.options.breaks?t.inline=M.breaks:t.inline=M.gfm),this.tokenizer.rules=t;}static get rules(){return {block:I,inline:M}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,`
`),this.blockTokens(e,this.tokens);for(let t=0;t<this.inlineQueue.length;t++){let n=this.inlineQueue[t];this.inlineTokens(n.src,n.tokens);}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=false){for(this.options.pedantic&&(e=e.replace(m.tabCharGlobal," ").replace(m.spaceLine,""));e;){let r;if(this.options.extensions?.block?.some(s=>(r=s.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),true):false))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let s=t.at(-1);r.raw.length===1&&s!==void 0?s.raw+=`
`:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=`
`+r.raw,s.text+=`
`+r.text,this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=`
`+r.raw,s.text+=`
`+r.raw,this.inlineQueue.at(-1).src=s.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title});continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let s=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(u=>{a=u.call({lexer:this},o),typeof a=="number"&&a>=0&&(s=Math.min(s,a));}),s<1/0&&s>=0&&(i=e.substring(0,s+1));}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let s=t.at(-1);n&&s?.type==="paragraph"?(s.raw+=`
`+r.raw,s.text+=`
`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="text"?(s.raw+=`
`+r.raw,s.text+=`
`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(e){let s="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(s);break}else throw new Error(s)}}return this.state.top=true,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,r=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(r=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(r[0].slice(r[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex));}for(;(r=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,r.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(r=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=false,s="";for(;e;){i||(s=""),i=false;let o;if(this.options.extensions?.inline?.some(u=>(o=u.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),true):false))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let u=t.at(-1);o.type==="text"&&u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,s)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let u=1/0,p=e.slice(1),c;this.options.extensions.startInline.forEach(f=>{c=f.call({lexer:this},p),typeof c=="number"&&c>=0&&(u=Math.min(u,c));}),u<1/0&&u>=0&&(a=e.substring(0,u+1));}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(s=o.raw.slice(-1)),i=true;let u=t.at(-1);u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(e){let u="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(u);break}else throw new Error(u)}}return t}};var P=class{options;parser;constructor(e){this.options=e||O;}space(e){return ""}code({text:e,lang:t,escaped:n}){let r=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+`
`;return r?'<pre><code class="language-'+w(r)+'">'+(n?i:w(i,true))+`</code></pre>
`:"<pre><code>"+(n?i:w(i,true))+`</code></pre>
`}blockquote({tokens:e}){return `<blockquote>
${this.parser.parse(e)}</blockquote>
`}html({text:e}){return e}heading({tokens:e,depth:t}){return `<h${t}>${this.parser.parseInline(e)}</h${t}>
`}hr(e){return `<hr>
`}list(e){let t=e.ordered,n=e.start,r="";for(let o=0;o<e.items.length;o++){let a=e.items[o];r+=this.listitem(a);}let i=t?"ol":"ul",s=t&&n!==1?' start="'+n+'"':"";return "<"+i+s+`>
`+r+"</"+i+`>
`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+w(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=true)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:true}):t+=n+" ";}return t+=this.parser.parse(e.tokens,!!e.loose),`<li>${t}</li>
`}checkbox({checked:e}){return "<input "+(e?'checked="" ':"")+'disabled="" type="checkbox">'}paragraph({tokens:e}){return `<p>${this.parser.parseInline(e)}</p>
`}table(e){let t="",n="";for(let i=0;i<e.header.length;i++)n+=this.tablecell(e.header[i]);t+=this.tablerow({text:n});let r="";for(let i=0;i<e.rows.length;i++){let s=e.rows[i];n="";for(let o=0;o<s.length;o++)n+=this.tablecell(s[o]);r+=this.tablerow({text:n});}return r&&(r=`<tbody>${r}</tbody>`),`<table>
<thead>
`+t+`</thead>
`+r+`</table>
`}tablerow({text:e}){return `<tr>
${e}</tr>
`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return (e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>
`}strong({tokens:e}){return `<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return `<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return `<code>${w(e,true)}</code>`}br(e){return "<br>"}del({tokens:e}){return `<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){let r=this.parser.parseInline(n),i=J(e);if(i===null)return r;e=i;let s='<a href="'+e+'"';return t&&(s+=' title="'+w(t)+'"'),s+=">"+r+"</a>",s}image({href:e,title:t,text:n,tokens:r}){r&&(n=this.parser.parseInline(r,this.parser.textRenderer));let i=J(e);if(i===null)return w(n);e=i;let s=`<img src="${e}" alt="${n}"`;return t&&(s+=` title="${w(t)}"`),s+=">",s}text(e){return "tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:w(e.text)}};var S=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return ""+e}image({text:e}){return ""+e}br(){return ""}};var R=class l{options;renderer;textRenderer;constructor(e){this.options=e||O,this.options.renderer=this.options.renderer||new P,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new S;}static parse(e,t){return new l(t).parse(e)}static parseInline(e,t){return new l(t).parseInline(e)}parse(e,t=true){let n="";for(let r=0;r<e.length;r++){let i=e[r];if(this.options.extensions?.renderers?.[i.type]){let o=i,a=this.options.extensions.renderers[o.type].call({parser:this},o);if(a!==false||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(o.type)){n+=a||"";continue}}let s=i;switch(s.type){case "space":{n+=this.renderer.space(s);continue}case "hr":{n+=this.renderer.hr(s);continue}case "heading":{n+=this.renderer.heading(s);continue}case "code":{n+=this.renderer.code(s);continue}case "table":{n+=this.renderer.table(s);continue}case "blockquote":{n+=this.renderer.blockquote(s);continue}case "list":{n+=this.renderer.list(s);continue}case "html":{n+=this.renderer.html(s);continue}case "paragraph":{n+=this.renderer.paragraph(s);continue}case "text":{let o=s,a=this.renderer.text(o);for(;r+1<e.length&&e[r+1].type==="text";)o=e[++r],a+=`
`+this.renderer.text(o);t?n+=this.renderer.paragraph({type:"paragraph",raw:a,text:a,tokens:[{type:"text",raw:a,text:a,escaped:true}]}):n+=a;continue}default:{let o='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}parseInline(e,t=this.renderer){let n="";for(let r=0;r<e.length;r++){let i=e[r];if(this.options.extensions?.renderers?.[i.type]){let o=this.options.extensions.renderers[i.type].call({parser:this},i);if(o!==false||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(i.type)){n+=o||"";continue}}let s=i;switch(s.type){case "escape":{n+=t.text(s);break}case "html":{n+=t.html(s);break}case "link":{n+=t.link(s);break}case "image":{n+=t.image(s);break}case "strong":{n+=t.strong(s);break}case "em":{n+=t.em(s);break}case "codespan":{n+=t.codespan(s);break}case "br":{n+=t.br(s);break}case "del":{n+=t.del(s);break}case "text":{n+=t.text(s);break}default:{let o='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}};var $=class{options;block;constructor(e){this.options=e||O;}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}provideLexer(){return this.block?b.lex:b.lexInline}provideParser(){return this.block?R.parse:R.parseInline}};var B=class{defaults=L();options=this.setOptions;parse=this.parseMarkdown(true);parseInline=this.parseMarkdown(false);Parser=R;Renderer=P;TextRenderer=S;Lexer=b;Tokenizer=y;Hooks=$;constructor(...e){this.use(...e);}walkTokens(e,t){let n=[];for(let r of e)switch(n=n.concat(t.call(this,r)),r.type){case "table":{let i=r;for(let s of i.header)n=n.concat(this.walkTokens(s.tokens,t));for(let s of i.rows)for(let o of s)n=n.concat(this.walkTokens(o.tokens,t));break}case "list":{let i=r;n=n.concat(this.walkTokens(i.items,t));break}default:{let i=r;this.defaults.extensions?.childTokens?.[i.type]?this.defaults.extensions.childTokens[i.type].forEach(s=>{let o=i[s].flat(1/0);n=n.concat(this.walkTokens(o,t));}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)));}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let r={...n};if(r.async=this.defaults.async||r.async||false,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let s=t.renderers[i.name];s?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===false&&(a=s.apply(this,o)),a}:t.renderers[i.name]=i.renderer;}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let s=t[i.level];s?s.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]));}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens);}),r.extensions=t),n.renderer){let i=this.defaults.renderer||new P(this.defaults);for(let s in n.renderer){if(!(s in i))throw new Error(`renderer '${s}' does not exist`);if(["options","parser"].includes(s))continue;let o=s,a=n.renderer[o],u=i[o];i[o]=(...p)=>{let c=a.apply(i,p);return c===false&&(c=u.apply(i,p)),c||""};}r.renderer=i;}if(n.tokenizer){let i=this.defaults.tokenizer||new y(this.defaults);for(let s in n.tokenizer){if(!(s in i))throw new Error(`tokenizer '${s}' does not exist`);if(["options","rules","lexer"].includes(s))continue;let o=s,a=n.tokenizer[o],u=i[o];i[o]=(...p)=>{let c=a.apply(i,p);return c===false&&(c=u.apply(i,p)),c};}r.tokenizer=i;}if(n.hooks){let i=this.defaults.hooks||new $;for(let s in n.hooks){if(!(s in i))throw new Error(`hook '${s}' does not exist`);if(["options","block"].includes(s))continue;let o=s,a=n.hooks[o],u=i[o];$.passThroughHooks.has(s)?i[o]=p=>{if(this.defaults.async)return Promise.resolve(a.call(i,p)).then(f=>u.call(i,f));let c=a.call(i,p);return u.call(i,c)}:i[o]=(...p)=>{let c=a.apply(i,p);return c===false&&(c=u.apply(i,p)),c};}r.hooks=i;}if(n.walkTokens){let i=this.defaults.walkTokens,s=n.walkTokens;r.walkTokens=function(o){let a=[];return a.push(s.call(this,o)),i&&(a=a.concat(i.call(this,o))),a};}this.defaults={...this.defaults,...r};}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return b.lex(e,t??this.defaults)}parser(e,t){return R.parse(e,t??this.defaults)}parseMarkdown(e){return (n,r)=>{let i={...r},s={...this.defaults,...i},o=this.onError(!!s.silent,!!s.async);if(this.defaults.async===true&&i.async===false)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));s.hooks&&(s.hooks.options=s,s.hooks.block=e);let a=s.hooks?s.hooks.provideLexer():e?b.lex:b.lexInline,u=s.hooks?s.hooks.provideParser():e?R.parse:R.parseInline;if(s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(n):n).then(p=>a(p,s)).then(p=>s.hooks?s.hooks.processAllTokens(p):p).then(p=>s.walkTokens?Promise.all(this.walkTokens(p,s.walkTokens)).then(()=>p):p).then(p=>u(p,s)).then(p=>s.hooks?s.hooks.postprocess(p):p).catch(o);try{s.hooks&&(n=s.hooks.preprocess(n));let p=a(n,s);s.hooks&&(p=s.hooks.processAllTokens(p)),s.walkTokens&&this.walkTokens(p,s.walkTokens);let c=u(p,s);return s.hooks&&(c=s.hooks.postprocess(c)),c}catch(p){return o(p)}}}onError(e,t){return n=>{if(n.message+=`
Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error occurred:</p><pre>"+w(n.message+"",true)+"</pre>";return t?Promise.resolve(r):r}if(t)return Promise.reject(n);throw n}}};var _=new B;function d(l,e){return _.parse(l,e)}d.options=d.setOptions=function(l){return _.setOptions(l),d.defaults=_.defaults,H(d.defaults),d};d.getDefaults=L;d.defaults=O;d.use=function(...l){return _.use(...l),d.defaults=_.defaults,H(d.defaults),d};d.walkTokens=function(l,e){return _.walkTokens(l,e)};d.parseInline=_.parseInline;d.Parser=R;d.parser=R.parse;d.Renderer=P;d.TextRenderer=S;d.Lexer=b;d.lexer=b.lex;d.Tokenizer=y;d.Hooks=$;d.parse=d;d.options;d.setOptions;d.use;d.walkTokens;d.parseInline;R.parse;b.lex;
/*! @license DOMPurify 3.2.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.6/LICENSE */
const {
entries,
setPrototypeOf,
isFrozen,
getPrototypeOf,
getOwnPropertyDescriptor
} = Object;
let {
freeze,
seal,
create
} = Object; // eslint-disable-line import/no-mutable-exports
let {
apply,
construct
} = typeof Reflect !== 'undefined' && Reflect;
if (!freeze) {
freeze = function freeze(x) {
return x;
};
}
if (!seal) {
seal = function seal(x) {
return x;
};
}
if (!apply) {
apply = function apply(fun, thisValue, args) {
return fun.apply(thisValue, args);
};
}
if (!construct) {
construct = function construct(Func, args) {
return new Func(...args);
};
}
const arrayForEach = unapply(Array.prototype.forEach);
const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf);
const arrayPop = unapply(Array.prototype.pop);
const arrayPush = unapply(Array.prototype.push);
const arraySplice = unapply(Array.prototype.splice);
const stringToLowerCase = unapply(String.prototype.toLowerCase);
const stringToString = unapply(String.prototype.toString);
const stringMatch = unapply(String.prototype.match);
const stringReplace = unapply(String.prototype.replace);
const stringIndexOf = unapply(String.prototype.indexOf);
const stringTrim = unapply(String.prototype.trim);
const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty);
const regExpTest = unapply(RegExp.prototype.test);
const typeErrorCreate = unconstruct(TypeError);
/**
* Creates a new function that calls the given function with a specified thisArg and arguments.
*
* @param func - The function to be wrapped and called.
* @returns A new function that calls the given function with a specified thisArg and arguments.
*/
function unapply(func) {
return function (thisArg) {
if (thisArg instanceof RegExp) {
thisArg.lastIndex = 0;
}
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
return apply(func, thisArg, args);
};
}
/**
* Creates a new function that constructs an instance of the given constructor function with the provided arguments.
*
* @param func - The constructor function to be wrapped and called.
* @returns A new function that constructs an instance of the given constructor function with the provided arguments.
*/
function unconstruct(func) {
return function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return construct(func, args);
};
}
/**
* Add properties to a lookup table
*
* @param set - The set to which elements will be added.
* @param array - The array containing elements to be added to the set.
* @param transformCaseFunc - An optional function to transform the case of each element before adding to the set.
* @returns The modified set with added elements.
*/
function addToSet(set, array) {
let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase;
if (setPrototypeOf) {
// Make 'in' and truthy checks like Boolean(set.constructor)
// independent of any properties defined on Object.prototype.
// Prevent prototype setters from intercepting set as a this value.
setPrototypeOf(set, null);
}
let l = array.length;
while (l--) {
let element = array[l];
if (typeof element === 'string') {
const lcElement = transformCaseFunc(element);
if (lcElement !== element) {
// Config presets (e.g. tags.js, attrs.js) are immutable.
if (!isFrozen(array)) {
array[l] = lcElement;
}
element = lcElement;
}
}
set[element] = true;
}
return set;
}
/**
* Clean up an array to harden against CSPP
*
* @param array - The array to be cleaned.
* @returns The cleaned version of the array
*/
function cleanArray(array) {
for (let index = 0; index < array.length; index++) {
const isPropertyExist = objectHasOwnProperty(array, index);
if (!isPropertyExist) {
array[index] = null;
}
}
return array;
}
/**
* Shallow clone an object
*
* @param object - The object to be cloned.
* @returns A new object that copies the original.
*/
function clone(object) {
const newObject = create(null);
for (const [property, value] of entries(object)) {
const isPropertyExist = objectHasOwnProperty(object, property);
if (isPropertyExist) {
if (Array.isArray(value)) {
newObject[property] = cleanArray(value);
} else if (value && typeof value === 'object' && value.constructor === Object) {
newObject[property] = clone(value);
} else {
newObject[property] = value;
}
}
}
return newObject;
}
/**
* This method automatically checks if the prop is function or getter and behaves accordingly.
*
* @param object - The object to look up the getter function in its prototype chain.
* @param prop - The property name for which to find the getter function.
* @returns The getter function found in the prototype chain or a fallback function.
*/
function lookupGetter(object, prop) {
while (object !== null) {
const desc = getOwnPropertyDescriptor(object, prop);
if (desc) {
if (desc.get) {
return unapply(desc.get);
}
if (typeof desc.value === 'function') {
return unapply(desc.value);
}
}
object = getPrototypeOf(object);
}
function fallbackValue() {
return null;
}
return fallbackValue;
}
const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']);
const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']);
const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']);
// List of SVG elements that are disallowed by default.
// We still need to know them so that we can do namespace
// checks properly in case one wants to add them to
// allow-list.
const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']);
const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']);
// Similarly to SVG, we want to know all MathML elements,
// even those that we disallow by default.
const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);
const text = freeze(['#text']);
const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']);
const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']);
const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']);
const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);
// eslint-disable-next-line unicorn/better-regex
const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode
const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm);
const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex
const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape
const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
);
const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i);
const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
);
const DOCTYPE_NAME = seal(/^html$/i);
const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i);
var EXPRESSIONS = /*#__PURE__*/Object.freeze({
__proto__: null,
ARIA_ATTR: ARIA_ATTR,
ATTR_WHITESPACE: ATTR_WHITESPACE,
CUSTOM_ELEMENT: CUSTOM_ELEMENT,
DATA_ATTR: DATA_ATTR,
DOCTYPE_NAME: DOCTYPE_NAME,
ERB_EXPR: ERB_EXPR,
IS_ALLOWED_URI: IS_ALLOWED_URI,
IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA,
MUSTACHE_EXPR: MUSTACHE_EXPR,
TMPLIT_EXPR: TMPLIT_EXPR
});
/* eslint-disable @typescript-eslint/indent */
// https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
const NODE_TYPE = {
element: 1,
text: 3,
// Deprecated
progressingInstruction: 7,
comment: 8,
document: 9};
const getGlobal = function getGlobal() {
return typeof window === 'undefined' ? null : window;
};
/**
* Creates a no-op policy for internal use only.
* Don't export this function outside this module!
* @param trustedTypes The policy factory.
* @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix).
* @return The policy created (or null, if Trusted Types
* are not supported or creating the policy failed).
*/
const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) {
if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') {
return null;
}
// Allow the callers to control the unique policy name
// by adding a data-tt-policy-suffix to the script element with the DOMPurify.
// Policy creation with duplicate names throws in Trusted Types.
let suffix = null;
const ATTR_NAME = 'data-tt-policy-suffix';
if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) {
suffix = purifyHostElement.getAttribute(ATTR_NAME);
}
const policyName = 'dompurify' + (suffix ? '#' + suffix : '');
try {
return trustedTypes.createPolicy(policyName, {
createHTML(html) {
return html;
},
createScriptURL(scriptUrl) {
return scriptUrl;
}
});
} catch (_) {
// Policy creation failed (most likely another DOMPurify script has
// already run). Skip creating the policy, as this will only cause errors
// if TT are enforced.
console.warn('TrustedTypes policy ' + policyName + ' could not be created.');
return null;
}
};
const _createHooksMap = function _createHooksMap() {
return {
afterSanitizeAttributes: [],
afterSanitizeElements: [],
afterSanitizeShadowDOM: [],
beforeSanitizeAttributes: [],
beforeSanitizeElements: [],
beforeSanitizeShadowDOM: [],
uponSanitizeAttribute: [],
uponSanitizeElement: [],
uponSanitizeShadowNode: []
};
};
function createDOMPurify() {
let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();
const DOMPurify = root => createDOMPurify(root);
DOMPurify.version = '3.2.6';
DOMPurify.removed = [];
if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) {
// Not running in a browser, provide a factory function
// so that you can pass your own Window
DOMPurify.isSupported = false;
return DOMPurify;
}
let {
document
} = window;
const originalDocument = document;
const currentScript = originalDocument.currentScript;
const {
DocumentFragment,
HTMLTemplateElement,
Node,
Element,
NodeFilter,
NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap,
HTMLFormElement,
DOMParser,
trustedTypes
} = window;
const ElementPrototype = Element.prototype;
const cloneNode = lookupGetter(ElementPrototype, 'cloneNode');
const remove = lookupGetter(ElementPrototype, 'remove');
const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');
const getChildNodes = lookupGetter(ElementPrototype, 'childNodes');
const getParentNode = lookupGetter(ElementPrototype, 'parentNode');
// As per issue #47, the web-components registry is inherited by a
// new document created via createHTMLDocument. As per the spec
// (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries)
// a new empty registry is used when creating a template contents owner
// document, so we use that as our parent document to ensure nothing
// is inherited.
if (typeof HTMLTemplateElement === 'function') {
const template = document.createElement('template');
if (template.content && template.content.ownerDocument) {
document = template.content.ownerDocument;
}
}
let trustedTypesPolicy;
let emptyHTML = '';
const {
implementation,
createNodeIterator,
createDocumentFragment,
getElementsByTagName
} = document;
const {
importNode
} = originalDocument;
let hooks = _createHooksMap();
/**
* Expose whether this browser supports running the full DOMPurify.
*/
DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined;
const {
MUSTACHE_EXPR,
ERB_EXPR,
TMPLIT_EXPR,
DATA_ATTR,
ARIA_ATTR,
IS_SCRIPT_OR_DATA,
ATTR_WHITESPACE,
CUSTOM_ELEMENT
} = EXPRESSIONS;
let {
IS_ALLOWED_URI: IS_ALLOWED_URI$1
} = EXPRESSIONS;
/**
* We consider the elements and attributes below to be safe. Ideally
* don't add any new ones but feel free to remove unwanted ones.
*/
/* allowed element names */
let ALLOWED_TAGS = null;
const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]);
/* Allowed attribute names */
let ALLOWED_ATTR = null;
const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]);
/*
* Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements.
* @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements)
* @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list)
* @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`.
*/
let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, {
tagNameCheck: {
writable: true,
configurable: false,
enumerable: true,
value: null
},
attributeNameCheck: {
writable: true,
configurable: false,
enumerable: true,
value: null
},
allowCustomizedBuiltInElements: {
writable: true,
configurable: false,
enumerable: true,
value: false
}
}));
/* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */
let FORBID_TAGS = null;
/* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */
let FORBID_ATTR = null;
/* Decide if ARIA attributes are okay */
let ALLOW_ARIA_ATTR = true;
/* Decide if custom data attributes are okay */
let ALLOW_DATA_ATTR = true;
/* Decide if unknown protocols are okay */
let ALLOW_UNKNOWN_PROTOCOLS = false;
/* Decide if self-closing tags in attributes are allowed.
* Usually removed due to a mXSS issue in jQuery 3.0 */
let ALLOW_SELF_CLOSE_IN_ATTR = true;
/* Output should be safe for common template engines.
* This means, DOMPurify removes data attributes, mustaches and ERB
*/
let SAFE_FOR_TEMPLATES = false;
/* Output should be safe even for XML used within HTML and alike.
* This means, DOMPurify removes comments when containing risky content.
*/
let SAFE_FOR_XML = true;
/* Decide if document with <html>... should be returned */
let WHOLE_DOCUMENT = false;
/* Track whether config is already set on this instance of DOMPurify. */
let SET_CONFIG = false;
/* Decide if all elements (e.g. style, script) must be children of
* document.body. By default, browsers might move them to document.head */
let FORCE_BODY = false;
/* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html
* string (or a TrustedHTML object if Trusted Types are supported).
* If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead
*/
let RETURN_DOM = false;
/* Decide if a DOM `DocumentFragment` should be returned, instead of a html
* string (or a TrustedHTML object if Trusted Types are supported) */
let RETURN_DOM_FRAGMENT = false;
/* Try to return a Trusted Type object instead of a string, return a string in
* case Trusted Types are not supported */
let RETURN_TRUSTED_TYPE = false;
/* Output should be free from DOM clobbering attacks?
* This sanitizes markups named with colliding, clobberable built-in DOM APIs.
*/
let SANITIZE_DOM = true;
/* Achieve full DOM Clobbering protection by isolating the namespace of named
* properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules.
*
* HTML/DOM spec rules that enable DOM Clobbering:
* - Named Access on Window (§7.3.3)
* - DOM Tree Accessors (§3.1.5)
* - Form Element Parent-Child Relations (§4.10.3)
* - Iframe srcdoc / Nested WindowProxies (§4.8.5)
* - HTMLCollection (§4.2.10.2)
*
* Namespace isolation is implemented by prefixing `id` and `name` attributes
* with a constant string, i.e., `user-content-`
*/
let SANITIZE_NAMED_PROPS = false;
const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-';
/* Keep element content when removing element? */
let KEEP_CONTENT = true;
/* If a `Node` is passed to sanitize(), then performs sanitization in-place instead
* of importing it into a new Document and returning a sanitized copy */
let IN_PLACE = false;
/* Allow usage of profiles like html, svg and mathMl */
let USE_PROFILES = {};
/* Tags to ignore content of when KEEP_CONTENT is true */
let FORBID_CONTENTS = null;
const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']);
/* Tags that are safe for data: URIs */
let DATA_URI_TAGS = null;
const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);
/* Attributes safe for values like "javascript:" */
let URI_SAFE_ATTRIBUTES = null;
const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);
const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
/* Document namespace */
let NAMESPACE = HTML_NAMESPACE;
let IS_EMPTY_INPUT = false;
/* Allowed XHTML+XML namespaces */
let ALLOWED_NAMESPACES = null;
const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString);
let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);
let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']);
// Certain elements are allowed in both SVG and HTML
// namespace. We need to specify them explicitly
// so that they don't get erroneously deleted from
// HTML namespace.
const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']);
/* Parsing of strict XHTML documents */
let PARSER_MEDIA_TYPE = null;
const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html'];
const DEFAULT_PARSER_MEDIA_TYPE = 'text/html';
let transformCaseFunc = null;
/* Keep a reference to config to pass to hooks */
let CONFIG = null;
/* Ideally, do not touch anything below this line */
/* ______________________________________________ */
const formElement = document.createElement('form');
const isRegexOrFunction = function isRegexOrFunction(testValue) {
return testValue instanceof RegExp || testValue instanceof Function;
};
/**
* _parseConfig
*
* @param cfg optional config literal
*/
// eslint-disable-next-line complexity
const _parseConfig = function _parseConfig() {
let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (CONFIG && CONFIG === cfg) {
return;
}
/* Shield configuration object from tampering */
if (!cfg || typeof cfg !== 'object') {
cfg = {};
}
/* Shield configuration object from prototype pollution */
cfg = clone(cfg);
PARSER_MEDIA_TYPE =
// eslint-disable-next-line unicorn/prefer-includes
SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE;
// HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is.
transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase;
/* Set configuration parameters */
ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS;
ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR;
ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES;
URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES;
DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS;
FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS;
FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({});
FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({});
USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false;
ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true
ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true
ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false
ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true
SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false
SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true
WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false
RETURN_DOM = cfg.RETURN_DOM || false; // Default false
RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false
RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false
FORCE_BODY = cfg.FORCE_BODY || false; // Default false
SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true
SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false
KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true
IN_PLACE = cfg.IN_PLACE || false; // Default false
IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI;
NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;
MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS;
HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS;
CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING || {};
if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) {
CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck;
}
if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) {
CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck;
}
if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') {
CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements;
}
if (SAFE_FOR_TEMPLATES) {
ALLOW_DATA_ATTR = false;
}
if (RETURN_DOM_FRAGMENT) {
RETURN_DOM = true;
}
/* Parse profile info */
if (USE_PROFILES) {
ALLOWED_TAGS = addToSet({}, text);
ALLOWED_ATTR = [];
if (USE_PROFILES.html === true) {
addToSet(ALLOWED_TAGS, html$1);
addToSet(ALLOWED_ATTR, html);
}
if (USE_PROFILES.svg === true) {
addToSet(ALLOWED_TAGS, svg$1);
addToSet(ALLOWED_ATTR, svg);
addToSet(ALLOWED_ATTR, xml);
}
if (USE_PROFILES.svgFilters === true) {
addToSet(ALLOWED_TAGS, svgFilters);
addToSet(ALLOWED_ATTR, svg);
addToSet(ALLOWED_ATTR, xml);
}
if (USE_PROFILES.mathMl === true) {
addToSet(ALLOWED_TAGS, mathMl$1);
addToSet(ALLOWED_ATTR, mathMl);
addToSet(ALLOWED_ATTR, xml);
}
}
/* Merge configuration parameters */
if (cfg.ADD_TAGS) {
if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) {
ALLOWED_TAGS = clone(ALLOWED_TAGS);
}
addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc);
}
if (cfg.ADD_ATTR) {
if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {
ALLOWED_ATTR = clone(ALLOWED_ATTR);
}
addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc);
}
if (cfg.ADD_URI_SAFE_ATTR) {
addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc);
}
if (cfg.FORBID_CONTENTS) {
if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {
FORBID_CONTENTS = clone(FORBID_CONTENTS);
}
addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc);
}
/* Add #text in case KEEP_CONTENT is set to true */
if (KEEP_CONTENT) {
ALLOWED_TAGS['#text'] = true;
}
/* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */
if (WHOLE_DOCUMENT) {
addToSet(ALLOWED_TAGS, ['html', 'head', 'body']);
}
/* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */
if (ALLOWED_TAGS.table) {
addToSet(ALLOWED_TAGS, ['tbody']);
delete FORBID_TAGS.tbody;
}
if (cfg.TRUSTED_TYPES_POLICY) {
if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') {
throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');
}
if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') {
throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');
}
// Overwrite existing TrustedTypes policy.
trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY;
// Sign local variables required by `sanitize`.
emptyHTML = trustedTypesPolicy.createHTML('');
} else {
// Uninitialized policy, attempt to initialize the internal dompurify policy.
if (trustedTypesPolicy === undefined) {
trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript);
}
// If creating the internal policy succeeded sign internal variables.
if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') {
emptyHTML = trustedTypesPolicy.createHTML('');
}
}
// Prevent further manipulation of configuration.
// Not available in IE8, Safari 5, etc.
if (freeze) {
freeze(cfg);
}
CONFIG = cfg;
};
/* Keep track of all possible SVG and MathML tags
* so that we can perform the namespace checks
* correctly. */
const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]);
const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]);
/**
* @param element a DOM element whose namespace is being checked
* @returns Return false if the element has a
* namespace that a spec-compliant parser would never
* return. Return true otherwise.
*/
const _checkValidNamespace = function _checkValidNamespace(element) {
let parent = getParentNode(element);
// In JSDOM, if we're inside shadow DOM, then parentNode
// can be null. We just simulate parent in this case.
if (!parent || !parent.tagName) {
parent = {
namespaceURI: NAMESPACE,
tagName: 'template'
};
}
const tagName = stringToLowerCase(element.tagName);
const parentTagName = stringToLowerCase(parent.tagName);
if (!ALLOWED_NAMESPACES[element.namespaceURI]) {
return false;
}
if (element.namespaceURI === SVG_NAMESPACE) {
// The only way to switch from HTML namespace to SVG
// is via <svg>. If it happens via any other tag, then
// it should be killed.
if (parent.namespaceURI === HTML_NAMESPACE) {
return tagName === 'svg';
}
// The only way to switch from MathML to SVG is via`
// svg if parent is either <annotation-xml> or MathML
// text integration points.
if (parent.namespaceURI === MATHML_NAMESPACE) {
return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]);
}
// We only allow elements that are defined in SVG
// spec. All others are disallowed in SVG namespace.
return Boolean(ALL_SVG_TAGS[tagName]);
}
if (element.namespaceURI === MATHML_NAMESPACE) {
// The only way to switch from HTML namespace to MathML
// is via <math>. If it happens via any other tag, then
// it should be killed.
if (parent.namespaceURI === HTML_NAMESPACE) {
return tagName === 'math';
}
// The only way to switch from SVG to MathML is via
// <math> and HTML integration points
if (parent.namespaceURI === SVG_NAMESPACE) {
return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName];
}
// We only allow elements that are defined in MathML
// spec. All others are disallowed in MathML namespace.
return Boolean(ALL_MATHML_TAGS[tagName]);
}
if (element.namespaceURI === HTML_NAMESPACE) {
// The only way to switch from SVG to HTML is via
// HTML integration points, and from MathML to HTML
// is via MathML text integration points
if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) {
return false;
}
if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {
return false;
}
// We disallow tags that are specific for MathML
// or SVG and should never appear in HTML namespace
return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]);
}
// For XHTML and XML documents that support custom namespaces
if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) {
return true;
}
// The code should never reach this place (this means
// that the element somehow got namespace that is not
// HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES).
// Return false just in case.
return false;
};
/**
* _forceRemove
*
* @param node a DOM node
*/
const _forceRemove = function _forceRemove(node) {
arrayPush(DOMPurify.removed, {
element: node
});
try {
// eslint-disable-next-line unicorn/prefer-dom-node-remove
getParentNode(node).removeChild(node);
} catch (_) {
remove(node);
}
};
/**
* _removeAttribute
*
* @param name an Attribute name
* @param element a DOM node
*/
const _removeAttribute = function _removeAttribute(name, element) {
try {
arrayPush(DOMPurify.removed, {
attribute: element.getAttributeNode(name),
from: element
});
} catch (_) {
arrayPush(DOMPurify.removed, {
attribute: null,
from: element
});
}
element.removeAttribute(name);
// We void attribute values for unremovable "is" attributes
if (name === 'is') {
if (RETURN_DOM || RETURN_DOM_FRAGMENT) {
try {
_forceRemove(element);
} catch (_) {}
} else {
try {
element.setAttribute(name, '');
} catch (_) {}
}
}
};
/**
* _initDocument
*
* @param dirty - a string of dirty markup
* @return a DOM, filled with the dirty markup
*/
const _initDocument = function _initDocument(dirty) {
/* Create a HTML document */
let doc = null;
let leadingWhitespace = null;
if (FORCE_BODY) {
dirty = '<remove></remove>' + dirty;
} else {
/* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */
const matches = stringMatch(dirty, /^[\r\n\t ]+/);
leadingWhitespace = matches && matches[0];
}
if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) {
// Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict)
dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>';
}
const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty;
/*
* Use the DOMParser API by default, fallback later if needs be
* DOMParser not work for svg when has multiple root element.
*/
if (NAMESPACE === HTML_NAMESPACE) {
try {
doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE);
} catch (_) {}
}
/* Use createHTMLDocument in case DOMParser is not available */
if (!doc || !doc.documentElement) {
doc = implementation.createDocument(NAMESPACE, 'template', null);
try {
doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload;
} catch (_) {
// Syntax error if dirtyPayload is invalid xml
}
}
const body = doc.body || doc.documentElement;
if (dirty && leadingWhitespace) {
body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null);
}
/* Work on whole document or just its body */
if (NAMESPACE === HTML_NAMESPACE) {
return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0];
}
return WHOLE_DOCUMENT ? doc.documentElement : body;
};
/**
* Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document.
*
* @param root The root element or node to start traversing on.
* @return The created NodeIterator
*/
const _createNodeIterator = function _createNodeIterator(root) {
return createNodeIterator.call(root.ownerDocument || root, root,
// eslint-disable-next-line no-bitwise
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null);
};
/**
* _isClobbered
*
* @param element element to check for clobbering attacks
* @return true if clobbered, false if safe
*/
const _isClobbered = function _isClobbered(element) {
return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function');
};
/**
* Checks whether the given object is a DOM node.
*
* @param value object to check whether it's a DOM node
* @return true is object is a DOM node
*/
const _isNode = function _isNode(value) {
return typeof Node === 'function' && value instanceof Node;
};
function _executeHooks(hooks, currentNode, data) {
arrayForEach(hooks, hook => {
hook.call(DOMPurify, currentNode, data, CONFIG);
});
}
/**
* _sanitizeElements
*
* @protect nodeName
* @protect textContent
* @protect removeChild
* @param currentNode to check for permission to exist
* @return true if node was killed, false if left alive
*/
const _sanitizeElements = function _sanitizeElements(currentNode) {
let content = null;
/* Execute a hook if present */
_executeHooks(hooks.beforeSanitizeElements, currentNode, null);
/* Check if element is clobbered or can clobber */
if (_isClobbered(currentNode)) {
_forceRemove(currentNode);
return true;
}
/* Now let's check the element's type and name */
const tagName = transformCaseFunc(currentNode.nodeName);
/* Execute a hook if present */
_executeHooks(hooks.uponSanitizeElement, currentNode, {
tagName,
allowedTags: ALLOWED_TAGS
});
/* Detect mXSS attempts abusing namespace confusion */
if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) {
_forceRemove(currentNode);
return true;
}
/* Remove any occurrence of processing instructions */
if (currentNode.nodeType === NODE_TYPE.progressingInstruction) {
_forceRemove(currentNode);
return true;
}
/* Remove any kind of possibly harmful comments */
if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) {
_forceRemove(currentNode);
return true;
}
/* Remove element if anything forbids its presence */
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
/* Check if we have a custom element to handle */
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) {
return false;
}
if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) {
return false;
}
}
/* Keep content except for bad-listed elements */
if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {
const parentNode = getParentNode(currentNode) || currentNode.parentNode;
const childNodes = getChildNodes(currentNode) || currentNode.childNodes;
if (childNodes && parentNode) {
const childCount = childNodes.length;
for (let i = childCount - 1; i >= 0; --i) {
const childClone = cloneNode(childNodes[i], true);
childClone.__removalCount = (currentNode.__removalCount || 0) + 1;
parentNode.insertBefore(childClone, getNextSibling(currentNode));
}
}
}
_forceRemove(currentNode);
return true;
}
/* Check whether element has a valid namespace */
if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) {
_forceRemove(currentNode);
return true;
}
/* Make sure that older browsers don't get fallback-tag mXSS */
if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) {
_forceRemove(currentNode);
return true;
}
/* Sanitize element content to be template-safe */
if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) {
/* Get the element's text content */
content = currentNode.textContent;
arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
content = stringReplace(content, expr, ' ');
});
if (currentNode.textContent !== content) {
arrayPush(DOMPurify.removed, {
element: currentNode.cloneNode()
});
currentNode.textContent = content;
}
}
/* Execute a hook if present */
_executeHooks(hooks.afterSanitizeElements, currentNode, null);
return false;
};
/**
* _isValidAttribute
*
* @param lcTag Lowercase tag name of containing element.
* @param lcName Lowercase attribute name.
* @param value Attribute value.
* @return Returns true if `value` is valid, otherwise false.
*/
// eslint-disable-next-line complexity
const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) {
/* Make sure attribute cannot clobber */
if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) {
return false;
}
/* Allow valid data-* attributes: At least one character after "-"
(https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes)
XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804)
We don't need to check the value; it's always URI safe. */
if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {
if (
// First condition does a very basic check if a) it's basically a valid custom element tagname AND
// b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
// and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck
_isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) ||
// Alternative, second condition checks if it's an `is`-attribute, AND
// the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck
lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else {
return false;
}
/* Check value is safe. First, is attr inert? If so, is safe */
} else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) {
return false;
} else ;
return true;
};
/**
* _isBasicCustomElement
* checks if at least one dash is included in tagName, and it's not the first char
* for more sophisticated checking see https://github.com/sindresorhus/validate-element-name
*
* @param tagName name of the tag of the node to sanitize
* @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false.
*/
const _isBasicCustomElement = function _isBasicCustomElement(tagName) {
return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT);
};
/**
* _sanitizeAttributes
*
* @protect attributes
* @protect nodeName
* @protect removeAttribute
* @protect setAttribute
*
* @param currentNode to sanitize
*/
const _sanitizeAttributes = function _sanitizeAttributes(currentNode) {
/* Execute a hook if present */
_executeHooks(hooks.beforeSanitizeAttributes, currentNode, null);
const {
attributes
} = currentNode;
/* Check if we have attributes; if not we might have a text node */
if (!attributes || _isClobbered(currentNode)) {
return;
}
const hookEvent = {
attrName: '',
attrValue: '',
keepAttr: true,
allowedAttributes: ALLOWED_ATTR,
forceKeepAttr: undefined
};
let l = attributes.length;
/* Go backwards over all attributes; safely remove bad ones */
while (l--) {
const attr = attributes[l];
const {
name,
namespaceURI,
value: attrValue
} = attr;
const lcName = transformCaseFunc(name);
const initValue = attrValue;
let value = name === 'value' ? initValue : stringTrim(initValue);
/* Execute a hook if present */
hookEvent.attrName = lcName;
hookEvent.attrValue = value;
hookEvent.keepAttr = true;
hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set
_executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent);
value = hookEvent.attrValue;
/* Full DOM Clobbering protection via namespace isolation,
* Prefix id and name attributes with `user-content-`
*/
if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) {
// Remove the attribute with this value
_removeAttribute(name, currentNode);
// Prefix the value and later re-create the attribute with the sanitized value
value = SANITIZE_NAMED_PROPS_PREFIX + value;
}
/* Work around a security issue with comments inside attributes */
if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title)/i, value)) {
_removeAttribute(name, currentNode);
continue;
}
/* Did the hooks approve of the attribute? */
if (hookEvent.forceKeepAttr) {
continue;
}
/* Did the hooks approve of the attribute? */
if (!hookEvent.keepAttr) {
_removeAttribute(name, currentNode);
continue;
}
/* Work around a security issue in jQuery 3.0 */
if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) {
_removeAttribute(name, currentNode);
continue;
}
/* Sanitize attribute content to be template-safe */
if (SAFE_FOR_TEMPLATES) {
arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
value = stringReplace(value, expr, ' ');
});
}
/* Is `value` valid for this attribute? */
const lcTag = transformCaseFunc(currentNode.nodeName);
if (!_isValidAttribute(lcTag, lcName, value)) {
_removeAttribute(name, currentNode);
continue;
}
/* Handle attributes that require Trusted Types */
if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') {
if (namespaceURI) ; else {
switch (trustedTypes.getAttributeType(lcTag, lcName)) {
case 'TrustedHTML':
{
value = trustedTypesPolicy.createHTML(value);
break;
}
case 'TrustedScriptURL':
{
value = trustedTypesPolicy.createScriptURL(value);
break;
}
}
}
}
/* Handle invalid data-* attribute set by try-catching it */
if (value !== initValue) {
try {
if (namespaceURI) {
currentNode.setAttributeNS(namespaceURI, name, value);
} else {
/* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */
currentNode.setAttribute(name, value);
}
if (_isClobbered(currentNode)) {
_forceRemove(currentNode);
} else {
arrayPop(DOMPurify.removed);
}
} catch (_) {
_removeAttribute(name, currentNode);
}
}
}
/* Execute a hook if present */
_executeHooks(hooks.afterSanitizeAttributes, currentNode, null);
};
/**
* _sanitizeShadowDOM
*
* @param fragment to iterate over recursively
*/
const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {
let shadowNode = null;
const shadowIterator = _createNodeIterator(fragment);
/* Execute a hook if present */
_executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null);
while (shadowNode = shadowIterator.nextNode()) {
/* Execute a hook if present */
_executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null);
/* Sanitize tags and elements */
_sanitizeElements(shadowNode);
/* Check attributes next */
_sanitizeAttributes(shadowNode);
/* Deep shadow DOM detected */
if (shadowNode.content instanceof DocumentFragment) {
_sanitizeShadowDOM(shadowNode.content);
}
}
/* Execute a hook if present */
_executeHooks(hooks.afterSanitizeShadowDOM, fragment, null);
};
// eslint-disable-next-line complexity
DOMPurify.sanitize = function (dirty) {
let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
let body = null;
let importedNode = null;
let currentNode = null;
let returnNode = null;
/* Make sure we have a string to sanitize.
DO NOT return early, as this will return the wrong type if
the user has requested a DOM object rather than a string */
IS_EMPTY_INPUT = !dirty;
if (IS_EMPTY_INPUT) {
dirty = '<!-->';
}
/* Stringify, in case dirty is an object */
if (typeof dirty !== 'string' && !_isNode(dirty)) {
if (typeof dirty.toString === 'function') {
dirty = dirty.toString();
if (typeof dirty !== 'string') {
throw typeErrorCreate('dirty is not a string, aborting');
}
} else {
throw typeErrorCreate('toString is not a function');
}
}
/* Return dirty HTML if DOMPurify cannot run */
if (!DOMPurify.isSupported) {
return dirty;
}
/* Assign config vars */
if (!SET_CONFIG) {
_parseConfig(cfg);
}
/* Clean up removed elements */
DOMPurify.removed = [];
/* Check if dirty is correctly typed for IN_PLACE */
if (typeof dirty === 'string') {
IN_PLACE = false;
}
if (IN_PLACE) {
/* Do some early pre-sanitization to avoid unsafe root nodes */
if (dirty.nodeName) {
const tagName = transformCaseFunc(dirty.nodeName);
if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) {
throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place');
}
}
} else if (dirty instanceof Node) {
/* If dirty is a DOM element, append to an empty document to avoid
elements being stripped by the parser */
body = _initDocument('<!---->');
importedNode = body.ownerDocument.importNode(dirty, true);
if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') {
/* Node is already a body, use as is */
body = importedNode;
} else if (importedNode.nodeName === 'HTML') {
body = importedNode;
} else {
// eslint-disable-next-line unicorn/prefer-dom-node-append
body.appendChild(importedNode);
}
} else {
/* Exit directly if we have nothing to do */
if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT &&
// eslint-disable-next-line unicorn/prefer-includes
dirty.indexOf('<') === -1) {
return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty;
}
/* Initialize the document to work on */
body = _initDocument(dirty);
/* Check we have a DOM node from the data */
if (!body) {
return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : '';
}
}
/* Remove first element node (ours) if FORCE_BODY is set */
if (body && FORCE_BODY) {
_forceRemove(body.firstChild);
}
/* Get node iterator */
const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body);
/* Now start iterating over the created document */
while (currentNode = nodeIterator.nextNode()) {
/* Sanitize tags and elements */
_sanitizeElements(currentNode);
/* Check attributes next */
_sanitizeAttributes(currentNode);
/* Shadow DOM detected, sanitize it */
if (currentNode.content instanceof DocumentFragment) {
_sanitizeShadowDOM(currentNode.content);
}
}
/* If we sanitized `dirty` in-place, return it. */
if (IN_PLACE) {
return dirty;
}
/* Return sanitized string or DOM */
if (RETURN_DOM) {
if (RETURN_DOM_FRAGMENT) {
returnNode = createDocumentFragment.call(body.ownerDocument);
while (body.firstChild) {
// eslint-disable-next-line unicorn/prefer-dom-node-append
returnNode.appendChild(body.firstChild);
}
} else {
returnNode = body;
}
if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) {
/*
AdoptNode() is not used because internal state is not reset
(e.g. the past names map of a HTMLFormElement), this is safe
in theory but we would rather not risk another attack vector.
The state that is cloned by importNode() is explicitly defined
by the specs.
*/
returnNode = importNode.call(originalDocument, returnNode, true);
}
return returnNode;
}
let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;
/* Serialize doctype if allowed */
if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) {
serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\n' + serializedHTML;
}
/* Sanitize final string template-safe */
if (SAFE_FOR_TEMPLATES) {
arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => {
serializedHTML = stringReplace(serializedHTML, expr, ' ');
});
}
return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML;
};
DOMPurify.setConfig = function () {
let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
_parseConfig(cfg);
SET_CONFIG = true;
};
DOMPurify.clearConfig = function () {
CONFIG = null;
SET_CONFIG = false;
};
DOMPurify.isValidAttribute = function (tag, attr, value) {
/* Initialize shared config vars if necessary. */
if (!CONFIG) {
_parseConfig({});
}
const lcTag = transformCaseFunc(tag);
const lcName = transformCaseFunc(attr);
return _isValidAttribute(lcTag, lcName, value);
};
DOMPurify.addHook = function (entryPoint, hookFunction) {
if (typeof hookFunction !== 'function') {
return;
}
arrayPush(hooks[entryPoint], hookFunction);
};
DOMPurify.removeHook = function (entryPoint, hookFunction) {
if (hookFunction !== undefined) {
const index = arrayLastIndexOf(hooks[entryPoint], hookFunction);
return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0];
}
return arrayPop(hooks[entryPoint]);
};
DOMPurify.removeHooks = function (entryPoint) {
hooks[entryPoint] = [];
};
DOMPurify.removeAllHooks = function () {
hooks = _createHooksMap();
};
return DOMPurify;
}
var purify = createDOMPurify();
const DOMPurify = purify(window);
class QscChatbot extends HTMLElement {
constructor() {
super();
this.attachShadow({mode:'open'});
this.isOpen=false;
this.isFullscreen=false;
this.isMinimized=false;
this.messages=[];
this.unreadCount=0;
this.editingId = null;
this._initialFetched = false;
this._copyTimers = {};
this.sessionId = null;
this.inputValue = "";
this.receivedModels = [];
this.selectedModel = null;
this.showModelMenu = false;
this.serverStreamingEnabled = false;
this._streamingUpdateTimeouts = {};
this._copySVG = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9"></path>
</svg>
`;
this._checkSVG = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6L9 17l-5-5"></path>
</svg>
`;
}
_isUserAtBottom() {
const messagesDiv = this.shadowRoot.querySelector('.messages');
if (!messagesDiv) return true;
const threshold = 100;
const position = messagesDiv.scrollTop + messagesDiv.clientHeight;
const height = messagesDiv.scrollHeight;
return position >= height - threshold;
}
_scrollToBottom() {
const messagesDiv = this.shadowRoot.querySelector('.messages');
if (messagesDiv) {
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
}
_debouncedStreamingUpdate(id, rawMarkdown, streaming = true, delay = 16) {
if (this._streamingUpdateTimeouts[id]) {
clearTimeout(this._streamingUpdateTimeouts[id]);
}
this._streamingUpdateTimeouts[id] = setTimeout(() => {
requestAnimationFrame(() => {
this._updateStreamingMessage(id, rawMarkdown, streaming);
});
}, delay);
}
_updateStreamingMessage(id, rawMarkdown, isStreaming = true) {
const messageRow = this.shadowRoot.querySelector(`[data-msg-id="${id}"]`);
if (!messageRow) return;
const messageText = messageRow.querySelector('.message-text');
const timestampContainer = messageRow.querySelector('.timestamp-container');
if (!messageText) return;
try {
const messageIndex = this.messages.findIndex(m => m.id === id);
if (messageIndex !== -1) {
this.messages[messageIndex].text = rawMarkdown;
}
if (!rawMarkdown || rawMarkdown.trim() === '') {
if (!messageText.querySelector('.typing-indicator')) {
messageText.innerHTML = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
}
} else {
const typingIndicator = messageText.querySelector('.typing-indicator');
if (typingIndicator) {
typingIndicator.remove();
}
const html = this._processMarkdown(rawMarkdown, isStreaming);
const currentContent = messageText.innerHTML;
if (currentContent !== html || isStreaming) {
messageText.innerHTML = html;
}
if (!isStreaming) {
setTimeout(() => {
this._splitAndRenderMultipleJsons();
this._formatAndHighlightJson();
this._attachCodeCopyButtons(id);
}, 50);
}
}
if (timestampContainer) {
if (isStreaming) {
if (!timestampContainer.querySelector('.streaming-indicator') && !messageText.querySelector('.typing-indicator')) {
timestampContainer.innerHTML = '<div class="streaming-indicator"><span></span><span></span><span></span></div>';
}
} else {
const message = this.messages.find(m => m.id === id);
if (message) {
timestampContainer.innerHTML = `<div class="timestamp">${this.formatTime(message.timestamp)}</div>`;
}
}
}
} catch (e) {
console.warn("Markdown parse error:", e);
messageText.innerHTML = `<pre>${this._escapeHtml(rawMarkdown)}</pre>`;
}
}
_startNewChat() {
this.sessionId = null;
try {
localStorage.removeItem('qsc_session_id');
} catch (e) {
}
this.messages = [];
this._initialFetched = false;
this._fetchInitialMessage().catch(err => console.error(err));
this.renderMessages({ autoScroll: 'bottom' });
this.render();
}
async connectedCallback() {
this.logoPath=this.getAttribute('logo-path');
this.headerTitle=this.getAttribute('header-title') || 'QSC Chatbot';
this.errMsg=this.getAttribute('error-msg') || 'Connection to the server failed';
this.attachBtn=this.getAttribute('attach-btn') === 'true';
this.restUrl = this.getAttribute('rest-url') || 'no-rest-url-provided';
this.render();
}
async _fetchInitialMessage() {
if (this._initialFetched) return;
this._initialFetched = true;
const loadingId = `initial-loading-${Date.now()}`;
this.messages.push({
id: loadingId,
text: `
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
`,
sender: 'bot',
timestamp: new Date(),
isLoading: true
});
this.renderMessages({ autoScroll: 'bottom' });
const payload = {
type: 'init',
sessionId: null
};
try {
const res = await fetch(this.restUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
throw new Error(`HTTP error! Status: ${res.status}`);
}
const data = await res.json();
if (Array.isArray(data.models) && data.models.length > 0) {
this._setModels(data.models);
}
this.handleRestBotResponse(data);
} catch (err) {
// remove loading indicator
this.messages = this.messages.filter(m => !m.isLoading);
this._pushSystem(this.errMsg);
console.error('Initial fetch failed', err);
}
}
_setModels(modelsFromServer) {
if (!modelsFromServer || !Array.isArray(modelsFromServer) || modelsFromServer.length === 0) {
this.receivedModels = [];
this.selectedModel = null;
this.showModelMenu = false;
return;
}
this.receivedModels = modelsFromServer.map((m, i) => {
if (typeof m === 'string') {
return { model: m, label: String(m), selected: m.selected || false };
} else if (m && typeof m === 'object') {
return { model: m.model, label: String(m.label) || String(m), selected: m.selected || false};
} else {
return { model: m, label: String(m),selected: m.selected || false };
}
});
if (this.selectedModel) {
const keep = this.receivedModels.find(x => String(x.model) === String(this.selectedModel.model)
|| String(x.label) === String(this.selectedModel.label));
if (keep) {
this.selectedModel = keep;
return;
}
}
this.selectedModel = this.receivedModels.find(x => x.selected === true) || null;
this.render();
}
_selectModel(model) {
this.selectedModel = model;
this.showModelMenu = false;
this.render();
}
_removeSelectedModel() {
this.selectedModel = null;
this.render();
}
_pushSystem(txt) {
this.messages.push({id:Date.now(),text:txt,sender:'system',timestamp:new Date()});
if(!this.isOpen){ this.showBroadcastPopup(txt); this.unreadCount++; }
this.renderMessages({ autoScroll: 'bottom' });
}
async _sendActionPrompt(promptText, uiPrompt = null, isSilent = false) {
if (!promptText) throw new Error('Empty prompt');
const modelToSend = this.selectedModel ? (this.selectedModel.model || this.selectedModel.name) : undefined;
if (!isSilent) {
const displayText = uiPrompt || promptText;
if (displayText) {
this.messages.push({
id: `action-${Date.now()}`,
text: displayText,
sender: 'user',
timestamp: new Date()
});
}
}
const botMsgId = 'bot-action-' + Date.now();
this.messages.push({
id: botMsgId,
text: `
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
`,
sender: 'bot',
timestamp: new Date(),
isLoading: true,
isStreaming: false
});
this.renderMessages({ autoScroll: 'bottom' });
try {
const res = await fetch(this.restUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'message',
text: promptText,
model: modelToSend,
sessionId: this.sessionId,
id: `rest-${Date.now()}`
})
});
if (!res.ok) {
throw new Error(`HTTP error! Status: ${res.status}`);
}
if (this.serverStreamingEnabled && res.body) {
// Remove loading and create streaming message
this.messages = this.messages.filter(m => m.id !== botMsgId);
const streamingBotMsgId = 'bot-action-stream-' + Date.now();
this.messages.push({
id: streamingBotMsgId,
text: '',
sender: 'bot',
timestamp: new Date(),
isLoading: false,
isStreaming: true
});
this.renderMessages({ autoScroll: 'bottom' });
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
try {
const json = JSON.parse(line);
if (json.type === 'broadcast') {
this._pushSystem(json.data);
}
if (json.session_id) {
this.sessionId = json.session_id;
localStorage.setItem('qsc_session_id', this.sessionId);
}
if (json.data !== undefined) {
fullText += json.data;
const index = this.messages.findIndex(m => m.id === streamingBotMsgId);
if (index !== -1) {
this.messages[index].text = fullText;
this.messages[index].timestamp = new Date();
this._debouncedStreamingUpdate(streamingBotMsgId, fullText, true, 16);
if (this._isUserAtBottom()) {
this._scrollToBottom();
}
}
}
} catch (e) {
console.warn('Stream parse error in action:', e);
}
}
}
} finally {
// Mark as done streaming
const index = this.messages.findIndex(m => m.id === streamingBotMsgId);
if (index !== -1) {
this.messages[index].isStreaming = false;
this._updateStreamingMessage(streamingBotMsgId, fullText, false);
}
}
} else {
const data = await res.json();
this.messages = this.messages.filter(m => m.id !== botMsgId);
this.handleRestBotResponse(data);
}
} catch (err) {
this.messages = this.messages.filter(m => !m.isLoading && !m.isStreaming);
this._pushSystem(this.errMsg);
console.error('Action fetch failed', err);
}
}
_renderModelDropdown() {
const dropdownContainer = this.shadowRoot.querySelector('.model-dropdown-container');
if (!dropdownContainer) return;
if (this.receivedModels && this.receivedModels.length > 0 && this.showModelMenu) {
dropdownContainer.innerHTML = `
<div class="model-dropdown">
${this.receivedModels.map(model => `
<button class="model-item ${this.selectedModel && this.selectedModel.model === model.model ? 'selected' : ''}"
data-model="${model.model}">
<span>${model.label}</span>
${this.selectedModel && this.selectedModel.model === model.model ? '✓' : ''}
</button>
`).join('')}
</div>
`;
dropdownContainer.querySelectorAll('.model-item').forEach(option => {
option.addEventListener('click', (e) => {
e.stopPropagation();
const modelValue = option.getAttribute('data-model');
const selected = this.receivedModels.find(m => m.model === modelValue);
if (selected)
this._selectModel(selected);
});
});
} else {
dropdownContainer.innerHTML = '';
}
}
async handleSend() {
const input = this.shadowRoot.querySelector('.chat-input');
const value = input.value.trim();
if (!value) return;
if (this.editingId) {
await this._handleEditSend(value);
return;
}
// Regular send case
this.messages.push({
id: Date.now(),
text: value,
sender: 'user',
timestamp: new Date()
});
const botMsgId = 'bot-' + Date.now();
this.messages.push({
id: botMsgId,
text: `
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
`,
sender: 'bot',
timestamp: new Date(),
isLoading: true,
isStreaming: false
});
this.renderMessages({ autoScroll: 'bottom' });
input.value = '';
this.inputValue = '';
input.style.height = 'auto';
await this._sendMessageToServer(value, botMsgId);
}
async _handleEditSend(value) {
const idx = this._findIndexById(this.editingId);
if (idx === -1) {
this.editingId = null;
return;
}
this.messages[idx].text = value;
this.messages[idx].timestamp = new Date();
this._removeMessagesAfterIndex(idx);
const botMsgId = 'bot-edit-' + Date.now();
this.messages.push({
id: botMsgId,
text: `
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
`,
sender: 'bot',
timestamp: new Date(),
isLoading: true,
isStreaming: false
});
this.renderMessages({ autoScroll: 'bottom' });
const originalEditId = this.editingId;
this.editingId = null;
const input = this.shadowRoot.querySelector('.chat-input');
input.value = '';
input.style.height = '1px';
input.placeholder = 'Type a message...';
await this._sendMessageToServer(value, botMsgId, originalEditId);
}
async _sendMessageToServer(text, botMsgId, editedFrom = null) {
const modelToSend = this.selectedModel ? (this.selectedModel.model || this.selectedModel.name) : undefined;
const sessionId = localStorage.getItem('qsc_session_id') || this.sessionId || this._getSessionId();
try {
const res = await fetch(this.restUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'message',
text: text,
sessionId: sessionId,
id: `rest-${Date.now()}`,
model: modelToSend,
editedFrom: editedFrom
})
});
if (!res.ok) {
throw new Error(`HTTP error! Status: ${res.status}`);
}
if (this.serverStreamingEnabled && res.body) {
// Remove loading message and create streaming message
this.messages = this.messages.filter(m => m.id !== botMsgId);
const streamingBotMsgId = 'bot-stream-' + Date.now();
this.messages.push({
id: streamingBotMsgId,
text: '',
sender: 'bot',
timestamp: new Date(),
isLoading: false,
isStreaming: true
});
this.renderMessages({ autoScroll: 'bottom' });
// Get the reader
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = decoder.decode(value, { stream: true });
// Split by newlines and process each line
const lines = chunk.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
try {
const json = JSON.parse(line);
// Handle different message types
if (json.type === 'broadcast') {
this._pushSystem(json.data);
}
if (json.session_id) {
this.sessionId = json.session_id;
localStorage.setItem('qsc_session_id', this.sessionId);
}
if (json.data !== undefined) {
fullText += json.data;
const index = this.messages.findIndex(m => m.id === streamingBotMsgId);
if (index !== -1) {
this.messages[index].text = fullText;
this.messages[index].timestamp = new Date();
this._debouncedStreamingUpdate(streamingBotMsgId, fullText, true, 16);
if (this._isUserAtBottom()) {
this._scrollToBottom();
}
}
}
} catch (e) {
console.warn('JSON parse error:', e);
}
}
}
} finally {
const index = this.messages.findIndex(m => m.id === streamingBotMsgId);
if (index !== -1) {
this.messages[index].isStreaming = false;
if (fullText.trim() !== '') {
this._updateStreamingMessage(streamingBotMsgId, fullText, false);
} else {
// If still empty after stream ends, show a fallback message
const messageText = this.shadowRoot.querySelector(`.message-text[data-msg-id="${streamingBotMsgId}"]`);
if (messageText) {
messageText.innerHTML = `<em>No response received</em>`;
}
}
// Remove streaming styling
const bubble = this.shadowRoot.querySelector(`[data-msg-id="${streamingBotMsgId}"] .bubble.bot`);
if (bubble) {
bubble.classList.remove('message-streaming');
}
}
}
} else {
const data = await res.json();
// Remove loading message
this.messages = this.messages.filter(m => m.id !== botMsgId);
this.handleRestBotResponse(data);
}
} catch (err) {
console.error('Send fetch failed:', err);
this.messages = this.messages.filter(m => !m.isLoading && !m.isStreaming);
this._pushSystem(this.errMsg);
}
}
handleClick(e) {
const toggleBtn = e.target.closest('.toggle-btn');
if (toggleBtn) {
this.isOpen = !this.isOpen;
if (this.isOpen) this.unreadCount = 0;
this.render();
if (this.isOpen) {
this.scrollToBottom();
this._fetchInitialMessage().catch(err => console.error(err));
}
return;
}
if (e.target.closest('.send-btn')) {
this.handleSend();
}
}
_autoResizeChatInput() {
const input = this.shadowRoot && this.shadowRoot.querySelector('.chat-input');
if (!input) return;
let computed = window.getComputedStyle(input);
let maxHeight = parseFloat(computed.maxHeight) || 150;
input.style.height = 'auto';
// measure needed height
const needed = input.scrollHeight;
if (needed <= maxHeight) {
input.style.overflowY = 'hidden';
input.style.height = needed + 'px';
} else {
input.style.overflowY = 'auto';
input.style.height = maxHeight + 'px';
}
}
handleKeyDown(e) {
if (!e.target.classList.contains('chat-input')) return;
if (e.key === 'Enter') {
if (e.shiftKey) {
setTimeout(() => this._autoResizeChatInput(), 0);
} else {
e.preventDefault();
this.handleSend();
}
}
}
handleRestBotResponse(data) {
this.messages = this.messages.filter(m => !m.isLoading && !m.isStreaming);
if (data.session_id && !this.sessionId) {
this.sessionId = data.session_id;
localStorage.setItem('qsc_session_id', this.sessionId);
}
if (data.streaming !== undefined) {
this.serverStreamingEnabled = data.streaming;
}
if (data.type === 'markdown' && data.data) {
const html = this._processMarkdown(data.data);
this.messages.push({
id: Date.now(),
text: html,
sender: 'bot',
timestamp: new Date(),
isHtml: true
});
this.renderMessages({ autoScroll: 'bottom' });
} else if (data.type === 'broadcast') {
let msg = data.text || data.message || data.data;
this._pushSystem(msg);
} else if (data.type === 'image') {
const html = `<img src="${data.data}" alt="server image" style="max-width:200px;max-height:200px;">`;
this.messages.push({
id: Date.now(),
text: html,
sender: 'bot',
timestamp: new Date()
});
this.renderMessages({ autoScroll: 'bottom' });
} else if (data.data) {
const html = this._processMarkdown(data.data);
this.messages.push({
id: Date.now(),
text: html,
sender: 'bot',
timestamp: new Date()
});
this.renderMessages({ autoScroll: 'bottom' });
}
}
_processMarkdown(raw, isStreaming = false) {
try {
// Check if we have incomplete HTML tags
const incompleteTagRegex = /<[^>]*$/;
let processedRaw = raw;
let hasIncompleteTag = false;
if (incompleteTagRegex.test(raw)) {
// Find the last complete part
const lastCompleteIndex = raw.lastIndexOf('>');
if (lastCompleteIndex !== -1) {
processedRaw = raw.substring(0, lastCompleteIndex + 1);
hasIncompleteTag = true;
} else {
// No complete tags at all, use everything up to the first
const firstOpenIndex = raw.indexOf('<');
if (firstOpenIndex !== -1) {
processedRaw = raw.substring(0, firstOpenIndex);
hasIncompleteTag = true;
}
}
}
// Check for incomplete QSCACTION tags
const incompleteQscRegex = /\[\[QSCACTION[^\]]*$/;
if (incompleteQscRegex.test(processedRaw)) {
// Remove incomplete QSCACTION tag
processedRaw = processedRaw.replace(incompleteQscRegex, '');
}
// Check for incomplete markdown images
const incompleteImageRegex = /!\[(?:[^\]]*(?:\][^)]*)?)?$/;
if (incompleteImageRegex.test(processedRaw)) {
processedRaw = processedRaw.replace(incompleteImageRegex, '');
}
const hasJsonCodeBlock = processedRaw.includes('```json') || processedRaw.includes('```') ||
(processedRaw.trim().startsWith('{') && processedRaw.trim().endsWith('}')) ||
(processedRaw.trim().startsWith('[') && processedRaw.trim().endsWith(']'));
if (isStreaming && hasJsonCodeBlock) {
let escaped = this._escapeHtml(processedRaw);
return `<pre><code>${escaped}</code></pre>`;
}
function escapeAttr(s) {
if (s == null) return "";
return String(s)
.replace(/&/g, "&")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/</g, "<")
.replace(/>/g, ">");
}
processedRaw = processedRaw.replace(
/\[\[QSCACTION\s+([^\]]+)\]\]/g,
function (_, attributes) {
const paramMap = {};
const attrRegex = /(\w+)=("([^"\\]*(\\.[^"\\]*)*)"|'([^'\\]*(\\.[^'\\]*)*)')/g;
let match;
while ((match = attrRegex.exec(attributes)) !== null) {
const key = match[1].trim();
let value = '';
if (match[2].startsWith('"')) {
value = match[3] || '';
} else {
value = match[5] || '';
}
value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
paramMap[key] = value.trim();
}
const type = escapeAttr(paramMap.type || '');
const label = escapeAttr(paramMap.label || '');
const prompt = escapeAttr(paramMap.prompt || '');
const uiPrompt = escapeAttr(paramMap.uiPrompt || '');
const url = escapeAttr(paramMap.url || '');
const style = escapeAttr(paramMap.style || '');
let buttonHtml = `<button class="action-button" data-action-type="${type}"`;
// Add data attributes based on type
if (type === 'submit' || type === 'submit-silent') {
buttonHtml += ` data-action="${prompt}"`;
if (uiPrompt) {
buttonHtml += ` data-ui-prompt="${uiPrompt}"`;
}
} else if (type === 'url-open') {
buttonHtml += ` data-url="${url}"`;
}
// Add style if provided
if (style) {
buttonHtml += ` style="${style}"`;
}
buttonHtml += `>${label}</button>`;
return buttonHtml;
}
);
// Parse with marked
const rawHtml = d.parse(processedRaw);
const safeHtml = DOMPurify.sanitize(rawHtml);
let finalHtml = `<div class="markdown">${safeHtml}</div>`;
return finalHtml;
} catch (error) {
console.warn("Markdown parse error:", error);
// fallback for unexpected parse errors
return `<pre class="markdown">${this._escapeHtml(raw)}</pre>`;
}
}
scrollToBottom() {
setTimeout(() => {
const messagesDiv = this.shadowRoot.querySelector('.messages');
if (messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight;
}, 100);
}
showBroadcastPopup(message) {
const oldPopup = this.shadowRoot.querySelector('.broadcast-popup');
if (oldPopup) oldPopup.remove();
const popup = document.createElement('div');
popup.className = 'broadcast-popup';
popup.innerHTML = `
<div class="broadcast-icon">📢</div>
<span>${message}</span>
`;
this.shadowRoot.appendChild(popup);
setTimeout(() => {
popup.classList.add('fade-out');
setTimeout(() => popup.remove(), 500);
}, 4000);
}
formatTime(date) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
_stripHtml(html) {
const tmp = document.createElement('div');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || '';
}
_getSessionId() {
try {
const key = 'qsc_session_id';
let id = localStorage.getItem(key);
if (!id) {
localStorage.removeItem(key);
}
return id;
} catch (e) {
return null;
}
}
_splitIntoJsonAndTextSegments(raw) {
const n = raw.length;
let i = 0;
let buf = '';
const segments = [];
const pushTextBuf = () => {
if (buf.length) {
segments.push({ type: 'text', content: buf });
buf = '';
}
};
while (i < n) {
const ch = raw[i];
if ((ch === '{' || ch === '[')) {
pushTextBuf();
const start = i;
const stack = [ch];
i++;
let inString = false;
let escaped = false;
for (; i < n; i++) {
const c = raw[i];
if (inString) {
if (escaped) {
escaped = false;
continue;
}
if (c === '\\') {
escaped = true;
continue;
}
if (c === '"') {
inString = false;
continue;
}
continue;
} else {
if (c === '"') {
inString = true;
continue;
}
if (c === '{' || c === '[') {
stack.push(c);
continue;
}
if (c === '}' || c === ']') {
const last = stack[stack.length - 1];
if ((c === '}' && last === '{') || (c === ']' && last === '[')) {
stack.pop();
} else {
stack.pop();
}
if (stack.length === 0) {
i++;
break;
}
continue;
}
}
}
const jsonSub = raw.slice(start, i);
segments.push({ type: 'json', content: jsonSub });
continue;
}
buf += ch;
i++;
}
pushTextBuf();
return segments
.map(s => ({ type: s.type, content: (s.content || '') }))
.filter(s => s.content.trim().length > 0);
}
_splitAndRenderMultipleJsons() {
const container = this.shadowRoot.querySelector('.messages');
if (!container) return;
const streamingMessages = container.querySelectorAll('.message-streaming');
if (streamingMessages.length > 0) {
return; // Don't process JSON during streaming
}
const codeNodes = Array.from(container.querySelectorAll('pre code'))
.filter(code => {
const cls = String(code.className || '').toLowerCase();
const txt = (code.textContent || '').trim();
return cls.includes('language-json') || txt.startsWith('{') || txt.startsWith('[');
});
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
codeNodes.forEach(codeEl => {
const preEl = codeEl.closest('pre');
if (!preEl) return;
const raw = codeEl.textContent || '';
const segments = this._splitIntoJsonAndTextSegments(raw);
if (segments.length <= 1) return;
let html = '';
segments.forEach(seg => {
if (seg.type === 'text') {
const text = seg.content.split(/\n{2,}/).map(p => p.trim()).filter(Boolean);
if (text.length === 0) {
html += `<p>${esc(seg.content)}</p>`;
} else {
text.forEach(p => { html += `<p>${esc(p)}</p>`; });
}
} else if (seg.type === 'json') {
let pretty = seg.content;
try {
const parsed = JSON.parse(seg.content);
pretty = JSON.stringify(parsed, null, 2);
} catch (e) {
pretty = seg.content.trim();
}
html += `<pre style="position: relative;"><code class="language-json">${esc(pretty)}</code></pre>\n`;
}
});
const wrapper = document.createElement('div');
wrapper.innerHTML = html;
Array.from(wrapper.childNodes).forEach(node => {
preEl.parentNode.insertBefore(node, preEl);
});
// remove the original pre (and its code child)
preEl.remove();
});
}
_formatAndHighlightJson() {
const container = this.shadowRoot.querySelector('.messages');
if (!container) return;
const streamingMessages = container.querySelectorAll('.message-streaming');
if (streamingMessages.length > 0) {
return; // Don't format JSON during streaming
}
const codeEls = Array.from(container.querySelectorAll('pre code'))
.filter(code => {
const cls = String(code.className || '').toLowerCase();
const txt = (code.textContent || '').trim();
return cls.includes('language-json') || txt.startsWith('{') || txt.startsWith('[');
});
const esc = s => String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
codeEls.forEach(codeEl => {
if (codeEl._qsc_json_highlighted) return;
let raw = codeEl.textContent || '';
let pretty = raw;
try {
const parsed = JSON.parse(raw);
pretty = JSON.stringify(parsed, null, 2);
} catch {
pretty = raw.trim();
}
const keyStore = [];
const strStore = [];
pretty = pretty.replace(/"((?:\\.|[^"\\])*)"\s*(?=:)/g, (m) => {
const idx = keyStore.length;
keyStore.push(m);
return `@@KEY${idx}@@`;
});
pretty = pretty.replace(/"((?:\\.|[^"\\])*)"/g, (m) => {
const idx = strStore.length;
strStore.push(m);
return `@@STR${idx}@@`;
});
let html = esc(pretty);
html = html.replace(/\b-?\d+(\.\d+)?([eE][+\-]?\d+)?\b/g, `<span class="json-number">$&</span>`);
html = html.replace(/\b(true|false|null)\b/g, `<span class="json-boolean">$1</span>`);
html = html.replace(/([{}\[\],:])/g, `<span class="json-punctuation">$1</span>`);
keyStore.forEach((orig, i) => {
const replacement = `<span class="json-key">${esc(orig)}</span>`;
html = html.replace(new RegExp(esc(`@@KEY${i}@@`), 'g'), replacement);
});
strStore.forEach((orig, i) => {
const replacement = `<span class="json-string">${esc(orig)}</span>`;
html = html.replace(new RegExp(esc(`@@STR${i}@@`), 'g'), replacement);
});
codeEl.innerHTML = html;
codeEl._qsc_json_highlighted = true;
});
}
_attachCodeCopyButtons() {
const container = this.shadowRoot.querySelector('.messages');
if (!container) return;
const pres = Array.from(container.querySelectorAll('.message-text .markdown pre'));
pres.forEach(pre => {
if (pre._qsc_copy_attached) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'copy-code-btn';
btn.title = 'Copy code';
btn.innerHTML = `
${this._copySVG}
`;
btn.addEventListener('click', async (evt) => {
evt.stopPropagation();
try {
await this._copyCodeFromElement(pre);
btn.classList.add('copied');
const iconContainer = btn.querySelector('svg');
if (iconContainer) iconContainer.outerHTML = this._checkSVG;
setTimeout(() => {
btn.classList.remove('copied');
if (btn.querySelector('svg')) btn.querySelector('svg').outerHTML = this._copySVG;
}, 1200);
} catch (err) {
btn.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
`;
btn.classList.add('copy-failed');
setTimeout(() => {
btn.classList.remove('copy-failed');
btn.innerHTML = this._copySVG;
}, 1200);
}
});
pre.style.position = pre.style.position || 'relative';
pre.appendChild(btn);
pre._qsc_copy_attached = true;
});
}
async _copyCodeFromElement(preEl) {
const codeEl = preEl.querySelector('code') || preEl;
const text = codeEl.textContent || codeEl.innerText || '';
if (!text) throw new Error('No code to copy');
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
} else {
return new Promise((resolve, reject) => {
try {
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
ta.remove();
ok ? resolve() : reject(new Error('execCommand failed'));
} catch (e) {
reject(e);
}
});
}
}
_findIndexById(id) {
return this.messages.findIndex(m => String(m.id) === String(id));
}
_removeMessagesAfterIndex(idx) {
if (idx < 0) return;
this.messages = this.messages.slice(0, idx + 1);
}
_createXSVG() {
return `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
`;
}
_escapeHtml(str = '') {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
async _handleSave(id) {
const idx = this._findIndexById(id);
if (idx === -1) return;
const ta = this.shadowRoot.querySelector(`.inline-edit-textarea[data-msg-id="${id}"]`);
if (!ta) return;
const newVal = ta.value.trim();
if (!newVal) {
return;
}
this.messages[idx].text = newVal;
this.messages[idx].timestamp = new Date();
this._removeMessagesAfterIndex(idx);
const botMsgId = 'bot-save-' + Date.now();
this.messages.push({
id: botMsgId,
text: `
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
`,
sender: 'bot',
timestamp: new Date(),
isLoading: true,
isStreaming: false
});
const originalEditId = id;
this.editingId = null;
this.renderMessages({ autoScroll: 'bottom' });
const modelToSend = this.selectedModel ? (this.selectedModel.model || this.selectedModel.name) : undefined;
try {
const res = await fetch(this.restUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'message', text: newVal, id: `rest-${Date.now()}`, editedFrom: originalEditId, model: modelToSend, sessionId: this.sessionId || localStorage.getItem('qsc_session_id') || this._getSessionId() })
});
if (!res.ok) {
throw new Error(`HTTP error! Status: ${res.status}`);
}
if (this.serverStreamingEnabled && res.body) {
// Remove loading message and create streaming message
this.messages = this.messages.filter(m => m.id !== botMsgId);
const streamingBotMsgId = 'bot-stream-' + Date.now();
this.messages.push({
id: streamingBotMsgId,
text: '',
sender: 'bot',
timestamp: new Date(),
isLoading: false,
isStreaming: true
});
this.renderMessages({ autoScroll: 'bottom' });
// Get the reader
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = decoder.decode(value, { stream: true });
// Split by newlines and process each line
const lines = chunk.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
try {
const json = JSON.parse(line);
// Handle different message types
if (json.type === 'broadcast') {
this._pushSystem(json.data);
}
if (json.session_id) {
this.sessionId = json.session_id;
localStorage.setItem('qsc_session_id', this.sessionId);
}
if (json.data !== undefined) {
fullText += json.data;
const index = this.messages.findIndex(m => m.id === streamingBotMsgId);
if (index !== -1) {
this.messages[index].text = fullText;
this.messages[index].timestamp = new Date();
this._debouncedStreamingUpdate(streamingBotMsgId, fullText, true, 16);
if (this._isUserAtBottom()) {
this._scrollToBottom();
}
}
}
} catch (e) {
console.warn('JSON parse error:', e);
}
}
}
} finally {
const index = this.messages.findIndex(m => m.id === streamingBotMsgId);
if (index !== -1) {
this.messages[index].isStreaming = false;
if (fullText.trim() !== '') {
this._updateStreamingMessage(streamingBotMsgId, fullText, false);
} else {
// If still empty after stream ends, show a fallback message
const messageText = this.shadowRoot.querySelector(`.message-text[data-msg-id="${streamingBotMsgId}"]`);
if (messageText) {
messageText.innerHTML = `<em>No response received</em>`;
}
}
// Remove streaming styling
const bubble = this.shadowRoot.querySelector(`[data-msg-id="${streamingBotMsgId}"] .bubble.bot`);
if (bubble) {
bubble.classList.remove('message-streaming');
}
}
}
} else {
const data = await res.json();
// Remove loading message
this.messages = this.messages.filter(m => m.id !== botMsgId);
this.handleRestBotResponse(data);
}
} catch (err) {
this.messages = this.messages.filter(m => !m.isLoading);
this._pushSystem(this.errMsg);
console.error('Send fetch failed', err);
}
}
_handleCancel(id) {
if (String(this.editingId) !== String(id)) {
this.editingId = null;
} else {
this.editingId = null;
}
this.renderMessages({ autoScroll: 'bottom' });
}
_handleEdit(id) {
const idx = this._findIndexById(id);
if (idx === -1) return;
const msg = this.messages[idx];
if (msg.sender !== 'user') return;
if (this.editingId && this.editingId !== id) {
this.editingId = null;
}
this.editingId = (this.editingId === id) ? null : id;
this.renderMessages({ autoScroll: 'intoView', targetId: id });
const msgEl = this.shadowRoot.querySelector(`.message-text[data-msg-id="${id}"]`);
if (!msgEl) {
return;
}
msgEl.dataset._originalHtml = msgEl.innerHTML;
const plain = this._stripHtml(msg.text);
msgEl.innerHTML = '';
const ta = document.createElement('textarea');
ta.className = 'inline-edit';
ta.value = plain;
ta.rows = 4;
ta.setAttribute('aria-label', 'Edit message');
ta.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
saveBtn.click();
} else if (e.key === 'Escape') {
e.preventDefault();
cancelBtn.click();
}
});
const actionsWrap = document.createElement('div');
actionsWrap.className = 'inline-edit-actions';
const saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.className = 'message-action-btn save-btn copied';
saveBtn.title = 'Save';
saveBtn.setAttribute('aria-label', 'Save message');
saveBtn.innerHTML = this._checkSVG;
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'message-action-btn cancel-btn';
cancelBtn.title = 'Cancel';
cancelBtn.setAttribute('aria-label', 'Cancel edit');
cancelBtn.innerHTML = this._createXSVG();
actionsWrap.appendChild(saveBtn);
actionsWrap.appendChild(cancelBtn);
msgEl.appendChild(ta);
msgEl.appendChild(actionsWrap);
ta.focus();
ta.selectionStart = ta.selectionEnd = ta.value.length;
cancelBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
this.editingId = null;
// restore original content
if (msgEl && msgEl.dataset && msgEl.dataset._originalHtml) {
msgEl.innerHTML = msgEl.dataset._originalHtml;
delete msgEl.dataset._originalHtml;
}
this.renderMessages({ autoScroll: 'intoView', targetId: id });
});
saveBtn.addEventListener('click', async (ev) => {
ev.stopPropagation();
const newVal = ta.value.trim();
if (!newVal) {
return;
}
this.messages[idx].text = newVal;
this.messages[idx].timestamp = new Date();
this._removeMessagesAfterIndex(idx);
const botMsgId = 'bot-save-' + Date.now();
this.messages.push({
id: botMsgId,
text: `
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
`,
sender: 'bot',
timestamp: new Date(),
isLoading: true
});
this.renderMessages({ autoScroll: 'bottom'});
const originalEditId = id;
this.editingId = null;
const modelToSend = this.selectedModel ? (this.selectedModel.model || this.selectedModel.name) : undefined;
try {
const res = await fetch(this.restUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'message', text: newVal, id: `rest-${Date.now()}`, editedFrom: originalEditId,model:modelToSend, sessionId: this.sessionId || localStorage.getItem('qsc_session_id') || this._getSessionId() })
});
if (!res.ok) {
throw new Error(`HTTP error! Status: ${res.status}`);
}
if (this.serverStreamingEnabled && res.body) {
// Remove loading message and create streaming message
this.messages = this.messages.filter(m => m.id !== botMsgId);
const streamingBotMsgId = 'bot-stream-' + Date.now();
this.messages.push({
id: streamingBotMsgId,
text: '',
sender: 'bot',
timestamp: new Date(),
isLoading: false,
isStreaming: true
});
this.renderMessages({ autoScroll: 'bottom' });
// Get the reader
const reader = res.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
const chunk = decoder.decode(value, { stream: true });
// Split by newlines and process each line
const lines = chunk.split('\n');
for (const line of lines) {
if (!line.trim()) continue;
try {
const json = JSON.parse(line);
// Handle different message types
if (json.type === 'broadcast') {
this._pushSystem(json.data);
}
if (json.session_id) {
this.sessionId = json.session_id;
localStorage.setItem('qsc_session_id', this.sessionId);
}
if (json.data !== undefined) {
fullText += json.data;
const index = this.messages.findIndex(m => m.id === streamingBotMsgId);
if (index !== -1) {
this.messages[index].text = fullText;
this.messages[index].timestamp = new Date();
this._debouncedStreamingUpdate(streamingBotMsgId, fullText, true, 16);
if (this._isUserAtBottom()) {
this._scrollToBottom();
}
}
}
} catch (e) {
console.warn('JSON parse error:', e);
}
}
}
} finally {
const index = this.messages.findIndex(m => m.id === streamingBotMsgId);
if (index !== -1) {
this.messages[index].isStreaming = false;
if (fullText.trim() !== '') {
this._updateStreamingMessage(streamingBotMsgId, fullText, false);
} else {
// If still empty after stream ends, show a fallback message
const messageText = this.shadowRoot.querySelector(`.message-text[data-msg-id="${streamingBotMsgId}"]`);
if (messageText) {
messageText.innerHTML = `<em>No response received</em>`;
}
}
// Remove streaming styling
const bubble = this.shadowRoot.querySelector(`[data-msg-id="${streamingBotMsgId}"] .bubble.bot`);
if (bubble) {
bubble.classList.remove('message-streaming');
}
}
}
} else {
const data = await res.json();
// Remove loading message
this.messages = this.messages.filter(m => m.id !== botMsgId);
this.handleRestBotResponse(data);
}
} catch (err) {
this.messages = this.messages.filter(m => !m.isLoading);
this._pushSystem(this.errMsg);
console.error('Send fetch failed', err);
}
});
if (this.editingId) {
const bubble = this.shadowRoot.querySelector(`[data-msg-id="${id}"]`);
if (bubble) {
bubble.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}
async _handleCopy(id) {
const idx = this._findIndexById(id);
if (idx === -1) return;
const msg = this.messages[idx];
const textToCopy = this._stripHtml(msg.text);
let success = false;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(textToCopy);
success = true;
} else {
const ta = document.createElement('textarea');
ta.value = textToCopy;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try {
success = document.execCommand('copy');
} catch (e) {
success = false;
}
ta.remove();
}
} catch (err) {
try {
const ta = document.createElement('textarea');
ta.value = textToCopy;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
success = document.execCommand('copy');
ta.remove();
} catch (e) {
success = false;
}
}
const btn = this.shadowRoot && this.shadowRoot.querySelector(`.copy-btn[data-msg-id="${id}"]`);
if (!btn) return;
if (this._copyTimers[id]) {
clearTimeout(this._copyTimers[id]);
delete this._copyTimers[id];
}
const originalHtml = btn.innerHTML;
const originalClass = btn.className;
if (success) {
btn.innerHTML = this._checkSVG;
btn.className = originalClass + ' copied';
} else {
btn.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
`;
btn.className = originalClass + ' copy-failed';
}
this._copyTimers[id] = setTimeout(() => {
try {
btn.innerHTML = originalHtml;
btn.className = originalClass;
} catch (e) {
}
delete this._copyTimers[id];
}, 1000);
}
renderMessages(opts = {}) {
const { autoScroll = 'bottom', targetId = null } = opts;
const messagesDiv = this.shadowRoot.querySelector('.messages');
if (!messagesDiv) return;
// Always re-render all messages
messagesDiv.innerHTML = this.messages.map(m => {
const isEditing = String(m.id) === String(this.editingId);
const isStreaming = m.isStreaming;
let messageContent = '';
if (m.sender === 'user' && isEditing) {
messageContent = `<textarea class="inline-edit inline-edit-textarea" data-msg-id="${m.id}" rows="3">${this._stripHtml(m.text)}</textarea>`;
} else if (m.sender === 'user') {
messageContent = `<div class="message-text" data-msg-id="${m.id}">${
m.isHtml ? m.text : `<p>${this._escapeHtml(m.text)}</p>`
}</div>`;
} else if (m.sender === 'bot') {
let displayText = '';
if (m.isLoading) {
displayText = `
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
`;
} else if (m.isStreaming && (!m.text || m.text.trim() === '')) {
// Show typing indicator for empty streaming messages
displayText = `
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
`;
} else if (m.isStreaming && m.text) {
const html = this._processMarkdown(m.text,true);
displayText = html + `
<div class="streaming-indicator">
<span></span><span></span><span></span>
</div>
`;
} else if (m.text) {
displayText = this._processMarkdown(m.text,false);
}
messageContent = `<div class="message-text ${isStreaming ? 'streaming-update' : ''}" data-msg-id="${m.id}">${displayText}</div>`;
} else {
messageContent = `<div class="message-text" data-msg-id="${m.id}">${m.text}</div>`;
}
let actionsHtml = '';
const showCopy = (!m.isLoading && !m.isStreaming) || Boolean(m.copied);
if (m.sender === 'user' && isEditing) {
actionsHtml = `
<div class="message-actions" data-msg-id="${m.id}">
<button class="message-action-btn save-btn" data-msg-id="${m.id}" data-action="save" title="Save" aria-label="Save">
${this._checkSVG}
</button>
<button class="message-action-btn cancel-btn" data-msg-id="${m.id}" data-action="cancel" title="Cancel" aria-label="Cancel">
${this._createXSVG()}
</button>
</div>
`;
} else {
actionsHtml = `
<div class="message-actions" data-msg-id="${m.id}">
${showCopy ? `<button class="message-action-btn copy-btn" data-msg-id="${m.id}" title="Copy" aria-label="Copy message">${this._copySVG}</button>` : ''}
${m.sender === 'user' && !m.isLoading && !m.isStreaming ? `<button class="message-action-btn edit-btn" data-msg-id="${m.id}" title="Edit" aria-label="Edit">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"></path>
</svg>
</button>` : ''}
</div>
`;
}
if (m.sender === 'system') {
return `
<div class="message-row system" data-msg-id="${m.id}">
<div class="bubble system">
<div class="message-text" data-msg-id="${m.id}">${m.text}</div>
<div class="timestamp">${this.formatTime(m.timestamp)}</div>
</div>
</div>
`;
}
if (m.sender === 'bot') {
const streamingClass = m.isStreaming ? 'message-streaming' : '';
return `
<div class="message-row bot" data-msg-id="${m.id}">
<div class="bubble bot ${streamingClass}">
${messageContent}
<div class="timestamp-container">
${m.isStreaming ?
'<div class="streaming-indicator"><span></span><span></span><span></span></div>' :
`<div class="timestamp">${this.formatTime(m.timestamp)}</div>`
}
</div>
</div>
${actionsHtml}
</div>
`;
}
return `
<div class="message-row user" data-msg-id="${m.id}">
<div class="bubble user">
${messageContent}
<div class="timestamp">${this.formatTime(m.timestamp)}</div>
</div>
${actionsHtml}
</div>
`;
}).join('');
const container = this.shadowRoot.querySelector('.messages');
if (container && !container._qsc_actions_bound) {
container.addEventListener('click', (e) => {
const saveBtn = e.target.closest('.save-btn') || e.target.closest('[data-action="save"]');
if (saveBtn) { this._handleSave(saveBtn.dataset.msgId); return; }
const cancelBtn = e.target.closest('.cancel-btn') || e.target.closest('[data-action="cancel"]');
if (cancelBtn) { this._handleCancel(cancelBtn.dataset.msgId); return; }
const copyBtn = e.target.closest('.copy-btn');
if (copyBtn) { this._handleCopy(copyBtn.dataset.msgId); return; }
const actionBtn = e.target.closest('.action-button');
if (actionBtn) {
const type = actionBtn.getAttribute('data-action-type');
switch (type) {
case 'submit':
const action = actionBtn.getAttribute('data-action');
const uiPrompt = actionBtn.getAttribute('data-ui-prompt');
if (action && uiPrompt) {
this._sendActionPrompt(action, uiPrompt, false);
} else if (action) {
this._sendActionPrompt(action, null, false);
}
break;
case 'submit-silent':
const silentAction = actionBtn.getAttribute('data-action');
if (silentAction) {
this._sendActionPrompt(silentAction, null, true);
}
break;
case 'url-open':
const url = actionBtn.getAttribute('data-url');
if (url) {
window.open(url, '_blank');
}
break;
default:
console.warn('Unknown action type:', type);
}
return;
}
const editBtn = e.target.closest('.edit-btn');
if (editBtn) { this._handleEdit(editBtn.dataset.msgId); return; }
});
container._qsc_actions_bound = true;
}
if (this.editingId) {
const ta = this.shadowRoot.querySelector(`.inline-edit-textarea[data-msg-id="${this.editingId}"]`);
if (ta) {
ta.focus();
ta.selectionStart = ta.selectionEnd = ta.value.length;
const keyHandler = (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
this._handleSave(this.editingId);
} else if (e.key === 'Escape') {
e.preventDefault();
this._handleCancel(this.editingId);
}
};
ta.removeEventListener('keydown', ta._qsc_keyHandler);
ta.addEventListener('keydown', keyHandler);
ta._qsc_keyHandler = keyHandler;
}
}
this._splitAndRenderMultipleJsons();
this._formatAndHighlightJson();
this._attachCodeCopyButtons();
if (autoScroll === 'bottom') {
this._scrollToBottom();
} else if (autoScroll === 'intoView' && targetId) {
const el = this.shadowRoot.querySelector(`[data-msg-id="${targetId}"]`);
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
--primary: #0078d4;
--primary-light: #e1f0fa;
--secondary: #605e5c;
--background: #ffffff;
--background-alt: #f5f5f5;
--text-primary: #323130;
--text-secondary: #605e5c;
--success: #107c10;
--warning: #d83b01;
--error: #a80000;
--border-radius: 12px;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
--transition: all 0.3s ease;
font-family: 'Segoe UI', system-ui, sans-serif;
}
.message-streaming {
position: relative;
animation: subtle-glow 2s ease-in-out infinite;
}
@keyframes subtle-glow {
0%, 100% { box-shadow: 0 0 10px rgba(0, 120, 212, 0.1); }
50% { box-shadow: 0 0 20px rgba(0, 120, 212, 0.2); }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.streaming-indicator {
display: inline-flex !important;
margin-left: 8px !important;
vertical-align: middle !important;
background: rgba(0, 120, 212, 0.1);
border-radius: 12px;
padding: 4px 8px;
height: 20px;
align-items: center;
}
.streaming-indicator span {
width: 4px;
height: 4px;
margin: 0 1px;
background-color: var(--primary);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
opacity: 0.7;
}
.streaming-indicator span:nth-child(1) { animation-delay: -0.32s; }
.streaming-indicator span:nth-child(2) { animation-delay: -0.16s; }
.streaming-indicator span:nth-child(3) { animation-delay: 0s; }
.typing-indicator.streaming {
display: flex;
align-items: center;
gap: 4px;
color: var(--primary);
font-size: 12px;
}
.streaming-dots {
display: flex;
gap: 2px;
}
.streaming-dots span {
width: 4px;
height: 4px;
background-color: var(--primary);
border-radius: 50%;
animation: streaming-bounce 1.4s infinite ease-in-out both;
}
.streaming-dots span:nth-child(1) { animation-delay: -0.32s; }
.streaming-dots span:nth-child(2) { animation-delay: -0.16s; }
@keyframes streaming-bounce {
0%, 80%, 100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.messages::-webkit-scrollbar {
width: 8px;
}
.messages::-webkit-scrollbar-track {
background: var(--background-alt);
}
.messages::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
.messages::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.2);
}
.message-text {
transition: all 0.2s ease;
}
.message-text.streaming-update {
animation: subtle-pulse 0.3s ease;
}
@keyframes subtle-pulse {
0% { opacity: 0.9; }
50% { opacity: 1; }
100% { opacity: 0.9; }
}
@keyframes smoothJump {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
.jumpLoop {
animation: smoothJump 1s ease-in-out infinite;
}
.chatbot-container {
position: fixed;
bottom: 30px;
right: 24px;
z-index: 9999;
transition: transform 0.3s ease;
}
.typing-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
height: 20px;
padding: 0 10px;
background: #f1f1f1;
border-radius: 20px;
}
.action-button {
padding: 4px 8px;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s ease;
margin: 4px;
white-space: nowrap;
}
.markdown img {
width: 220px !important;
height: auto;
object-fit: cover;
border: 1px solid #e6eef9;
border-radius: 8px;
padding: 4px;
display: block;
margin: 0 auto 8px auto;
}
.markdown table td{
border-radius: 14px;
padding: 12px;
border: none;
}
.markdown ul li::marker {
content: none;
}
.markdown table td:has(strong) {
border: 1px solid rgba(0, 0, 0, 0.04);
}
.model-selection-container {
display: flex;
align-items: center;
}
.new-chat-btn {
background: transparent;
border: 1.5px solid rgba(255,255,255,0.3);
color: white;
padding: 6px 8px;
border-radius: 20px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 5px;
transition: var(--transition);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.new-chat-btn:hover {
background: rgba(255,255,255,0.15);
border-color: rgba(255,255,255,0.5);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.new-chat-btn:active {
transform: translateY(0);
}
.new-chat-icon {
width: 16px;
height: 16px;
position: relative;
}
.new-chat-icon::before,
.new-chat-icon::after {
content: '';
position: absolute;
background: currentColor;
border-radius: 1px;
}
.new-chat-icon::before {
width: 10px;
height: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.new-chat-icon::after {
width: 2px;
height: 10px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.btn-disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.model-toggle-btn.model-selected {
background-color: rgb(0, 120, 212) !important;
}
.model-toggle-btn {
width: 30px;
height: 32px;
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.1);
background: var(--background);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
color: var(--text-secondary);
transition: var(--transition);
flex-shrink: 0;
}
.model-toggle-btn:hover {
background: var(--background-alt);
border-color: var(--primary);
color: var(--primary);
}
.model-dropdown {
position: absolute;
bottom: 12%;
left: 0;
background: var(--background);
border: 1px solid rgba(0,0,0,0.1);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 6px;
z-index: 1000;
min-width: 180px;
max-height: 200px;
overflow-y: auto;
}
.model-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border: none;
background: transparent;
width: 100%;
text-align: left;
cursor: pointer;
border-radius: 6px;
font-size: 13px;
color: var(--text-primary);
transition: background 0.2s;
}
.model-item:hover {
background: rgba(0,0,0,0.04);
}
.model-item.selected {
background: var(--primary-light);
color: var(--primary);
font-weight: 600;
}
.model-chip-container {
position: relative;
flex: 1;
}
.json-key { color: #a71d5d; }
.json-string { color: #183691; }
.json-number { color: #0086b3; }
.json-boolean { color: #795da3; }
.json-punctuation { color: #333; }
.timestamp-container {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 6px;
}
.typing-indicator span {
display: inline-block;
width: 5px;
height: 5px;
margin: 0 2px;
background-color: var(--primary);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) {
animation-delay: 0s;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
opacity: 0.3;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.message-actions {
display: inline-flex;
gap: 1px;
pointer-events: none;
height: fit-content !important;
opacity: 1 !important;
transform: translateY(4px);
transition: opacity .14s ease, transform .14s ease;
}
.message-action-btn svg {
width: 14px;
height: 14px;
display: block;
}
.message-action-btn {
background: transparent;
border: none;
cursor: pointer;
padding: 4px;
width: 28px;
height: 22px;
border-radius: 6px;
font-size: 12px;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
}
.message-action-btn.copied {
background: rgba(16,124,16,0.08);
color: #107c10;
}
.message-action-btn.copy-failed {
background: rgba(168,0,0,0.06);
color: var(--error);
}
.message-action-btn:hover { background: rgba(0,0,0,0.04); }
.message-row:hover .message-actions,
.message-row:focus-within .message-actions {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
@media (max-width: 480px) {
.message-actions {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
}
.toggle-btn {
position: relative;
background: var(--primary);
color: white;
border: none;
border-radius: 50%;
width: 64px;
height: 64px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--shadow);
transition: var(--transition);
font-weight: 600;
font-size: 18px;
}
.toggle-btn:hover {
transform: scale(1.05);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}
.toggle-btn img.jumpLoop {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
animation: smoothJump 1s ease-in-out infinite;
}
.toggle-btn .qsc-span.jumpLoop {
display: inline-block;
animation: smoothJump 1s ease-in-out infinite;
font-size: 24px;
}
.unread-count {
position: absolute;
top: -8px;
right: -8px;
background: var(--error);
color: white;
border-radius: 50%;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: bold;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.inline-edit {
width: 100%;
min-height: 64px;
max-height: 220px;
resize: vertical;
padding: 8px 10px;
font-size: 14px;
border-radius: 10px;
border: 1px solid rgba(0,0,0,0.08);
box-sizing: border-box;
background: var(--background-alt);
color: var(--text-primary);
font-family: inherit;
}
.inline-edit:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0,120,212,0.08);
}
.inline-edit-actions {
display: flex;
gap: 8px;
margin-top: 8px;
align-items: center;
}
.cancel-btn {
background: rgba(0,0,0,0.04);
border-radius: 6px;
padding: 6px;
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.inline-edit-actions svg,
.inline-edit textarea svg {
width: 14px;
height: 14px;
display: block;
}
.chat-window {
background: var(--background);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
width: 380px;
height: 480px;
display: flex;
flex-direction: column;
overflow: hidden;
transform: translateY(20px);
opacity: 0;
animation: fadeInUp 0.3s ease forwards;
}
@keyframes fadeInUp {
to {
opacity: 1;
transform: translateY(0);
}
}
.header {
background: var(--primary);
color: white;
padding: 14px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 10;
}
.header-title {
font-weight: 600;
font-size: 18px;
}
.header-controls {
display: flex;
align-items: center;
gap: 7px;
}
.close-btn {
background: transparent;
border: none;
color: white;
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
font-size: 20px;
font-weight: 300;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.messages {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 15px;
display: flex;
flex-direction: column;
gap: 10px;
background: var(--background-alt);
}
.message-row {
display: flex;
}
.message-row.bot {
justify-content: flex-start;
}
.message-row.user {
justify-content: flex-end;
}
.message-row.system {
justify-content: center;
}
.bubble {
padding: 10px 12px;
border-radius: 18px;
max-width: 87%;
position: relative;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
line-height: 1.5;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.bubble.user {
background: var(--primary);
color: white;
border-bottom-right-radius: 4px;
}
.bubble.bot {
background: var(--background);
color: var(--text-primary);
border-bottom-left-radius: 4px;
border: 1px solid rgba(0,0,0,0.05);
}
.bubble.system {
background: #fff8e1;
color: #856404;
border-radius: var(--border-radius);
display: flex;
align-items: center;
gap: 10px;
max-width: 95%;
}
.message-text {
flex: 1;
font-size: 13px;
font-family: monospace;
}
.message-text p {
white-space: pre-wrap;
margin: 0 !important;
}
.timestamp {
font-size: 11px;
color: var(--text-secondary);
margin-top: 6px;
text-align: right;
}
.user .timestamp {
color: rgba(255, 255, 255, 0.7);
}
.system .timestamp {
text-align: right;
}
.input-area {
display: flex;
border-top: 1px solid rgba(0,0,0,0.08);
padding: 11px;
background: var(--background);
gap: 6px;
align-items: center;
}
.chat-input {
flex: 1;
border: 1px solid rgba(0,0,0,0.1);
border-radius: 9px;
padding: 8px;
line-height: 18px;
font-size: 12px;
box-sizing: border-box;
background: var(--background-alt);
color: var(--text-primary);
transition: border 0.2s;
resize: none;
max-height: 140px;
}
.chat-input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.2);
}
.send-btn {
background: var(--primary);
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.send-btn:hover {
background: #106ebe;
transform: scale(1.05);
}
.send-btn:active {
transform: scale(0.95);
}
.send-btn svg {
width: 17px;
height: 17px;
}
.attach-btn {
background: transparent;
border: none;
cursor: pointer;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin-right: 4px;
}
.attach-btn svg {
width: 20px;
height: 20px;
stroke: #222;
fill: none;
display: block;
}
.attach-btn:hover {
background: rgba(0,0,0,0.04);
}
.broadcast-popup {
position: fixed;
bottom: 127px;
right: 40px;
background: #fff8e1;
color: #856404;
padding: 14px 20px;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
font-size: 14px;
z-index: 10000;
display: flex;
align-items: center;
gap: 10px;
max-width: 300px;
transition: opacity 0.5s ease;
border-left: 4px solid #ffb900;
}
.broadcast-icon {
min-width: 24px;
height: 24px;
background: #ffb900;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: #856404;
}
.fade-out {
opacity: 0;
}
.markdown {
background: #fff;
border-radius: 8px;
padding: 2px;
font-family: monospace;
max-width: 100%;
overflow-x: auto;
color: black !important;
}
.markdown pre {
position: relative;
padding: 5px;
background-color: #f0f0f0;
border-radius: 15px;
overflow-x: auto;
}
.copy-code-btn {
position: absolute;
top: 6px;
right: 6px;
background: rgba(0,0,0,0.06);
border: none;
padding: 6px 8px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 6px;
z-index: 10;
}
.copy-code-btn svg { width: 14px; height: 14px; }
.copy-code-btn.copied {
background: rgba(16,124,16,0.08);
color: #107c10;
}
@media (max-width: 480px) {
.chatbot-container {
bottom: 16px;
right: 16px;
}
.chat-window {
width: 100vw;
height: 100vh;
border-radius: 0;
bottom: 0;
right: 0;
position: fixed;
}
.broadcast-popup {
bottom: 90px;
right: 20px;
left: 20px;
max-width: none;
}
.fullscreen-btn,
.minimize-btn {
display: none !important;
}
}
.chat-window.fullscreen .message-actions {
opacity: 1 !important;
transform: translateY(0) !important;
pointer-events: auto !important;
z-index: 9999 !important;
gap: 6px;
font-size: 12px;
}
.chat-window.fullscreen .message-row:hover .message-actions,
.chat-window.fullscreen .message-row:focus-within .message-actions {
opacity: 1 !important;
transform: translateY(0) !important;
pointer-events: auto !important;
}
.chat-window.fullscreen {
width: 100vw !important;
height: auto !important;
border-radius: 0 !important;
position: fixed !important;
top: 0;left: 0; right: 0; bottom: 0;
z-index: 2147483647 !important;
overflow: visible !important;
}
.chat-window.minimized {
min-height: 0 !important;
overflow: hidden !important;
}
.fullscreen-btn, .minimize-btn {
background: transparent;
border: none;
color: white;
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.fullscreen-btn:hover, .minimize-btn:hover {
background: rgba(255,255,255,0.2);
}
</style>
<div class="chatbot-container">
${this.isOpen ? `
<div class="chat-window${this.isFullscreen ? ' fullscreen' : ''}${this.isMinimized ? ' minimized' : ''}">
<div class="header">
<div class="header-title">${this.headerTitle}</div>
<div class="header-controls">
<button class="new-chat-btn" title="Start new chat">
<span class="new-chat-icon"></span>
<span>New</span>
</button>
${this.isFullscreen
? `<button class="minimize-btn" title="Minimize">–</button>`
: `<button class="fullscreen-btn" title="Fullscreen">⛶</button>`
}
<button class="close-btn" title="Close">×</button>
</div>
</div>
<div class="messages"></div>
<div class="input-area" role="region" aria-label="Chat input area">
<div class="model-selection-container">
<button class="model-toggle-btn ${this.selectedModel ? 'model-selected' : ''} ${!this.receivedModels || this.receivedModels.length === 0 ? 'btn-disabled' : ''}" title="Select model">
💡
</button>
<div class="model-dropdown-container">
</div>
</div>
<textarea class="chat-input" rows="1" placeholder="Type a message..." autofocus></textarea>
${this.attachBtn ? `
<input class="file-input" type="file" accept="image/*,.md,.markdown" style="display:none">
<button class="attach-btn" title="Attach file">
<svg viewBox="0 0 24 24" fill="none" width="20" height="20" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21.44 11.05l-9.19 9.19a5 5 0 01-7.07-7.07l9.19-9.19a3 3 0 014.24 4.24l-9.19 9.19a1 1 0 01-1.41-1.41l9.19-9.19" />
</svg>
</button> ` : ``}
<button class="send-btn">
<svg class="send-icon" viewBox="0 0 24 24" fill="white">
<path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path>
</svg>
</button>
</div>
</div>
` : `
<button class="toggle-btn">
${
this.logoPath
? `<img src="${this.logoPath}" alt="Qsc" class="jumpLoop">`
: `<span class="qsc-span jumpLoop">Qsc</span>`
}
${this.unreadCount > 0 ? `<div class="unread-count">${this.unreadCount}</div>` : ''}
</button>
`}
</div>
`;
this.renderMessages();
// Add event listeners for new chat buttons
const newChatButtons = this.shadowRoot.querySelectorAll('.new-chat-btn');
newChatButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
this._startNewChat();
});
});
// Close dropdown when clicking outside
if (this.showModelMenu) {
setTimeout(() => {
const closeDropdown = (e) => {
if (!this.shadowRoot.contains(e.target)) {
this.showModelMenu = false;
this.render();
document.removeEventListener('click', closeDropdown);
}
};
document.addEventListener('click', closeDropdown);
}, 0);
}
const toggleBtn = this.shadowRoot.querySelector('.toggle-btn');
if (toggleBtn) {
toggleBtn.addEventListener('click', this.handleClick.bind(this));
}
const sendBtn = this.shadowRoot.querySelector('.send-btn');
if (sendBtn) {
sendBtn.addEventListener('click', this.handleClick.bind(this));
}
const chatInput = this.shadowRoot.querySelector('.chat-input');
if (chatInput) {
chatInput.value = this.inputValue || '';
if (!chatInput._qsc_handlers_attached) {
chatInput.addEventListener('input', (e) => {
this.inputValue = e.target.value;
try { this._autoResizeChatInput(); } catch (err) {}
});
chatInput.addEventListener('keydown', this.handleKeyDown.bind(this));
chatInput._qsc_handlers_attached = true;
}
// initial resize
setTimeout(() => this._autoResizeChatInput(), 0);
if (this.isOpen) chatInput.focus();
}
const closeBtn = this.shadowRoot.querySelector('.close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
this.isOpen = false;
this.render();
});
}
const fullscreenBtn = this.shadowRoot.querySelector('.fullscreen-btn');
if (fullscreenBtn) {
fullscreenBtn.addEventListener('click', () => {
this.isFullscreen = !this.isFullscreen;
if (this.isFullscreen) this.isMinimized = false;
this.render();
});
}
const minimizeBtn = this.shadowRoot.querySelector('.minimize-btn');
if (minimizeBtn) {
minimizeBtn.addEventListener('click', () => {
this.isMinimized = !this.isMinimized;
if (this.isMinimized) this.isFullscreen = false;
this.render();
});
}
const fileInput = this.shadowRoot.querySelector('.file-input');
const attachBtn = this.shadowRoot.querySelector('.attach-btn');
if (attachBtn && fileInput) {
attachBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = async () => {
const base64 = reader.result;
this.messages.push({
id: Date.now(),
text: `<img src="${base64}" alt="user upload" style="max-width:200px;max-height:200px;">`,
sender: 'user',
timestamp: new Date(),
isHtml: true
});
this.renderMessages({ autoScroll: 'bottom' });
const sessionId = localStorage.getItem('qsc_session_id') || this.sessionId || this._getSessionId();
const modelToSend = this.selectedModel ? (this.selectedModel.model || this.selectedModel.name) : undefined;
try {
const response = await fetch(this.restUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: `rest-${Date.now()}`, type: 'image', data: base64, filename: file.name,session_id: sessionId,model :modelToSend })
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
this.handleRestBotResponse(data);
} catch (err) {
this.messages = this.messages.filter(m => !m.isLoading);
this._pushSystem(this.errMsg);
console.error('Send fetch failed', err);
}
};
reader.readAsDataURL(file);
} else if (file.name.endsWith('.md') || file.name.endsWith('.markdown')) {
const reader = new FileReader();
reader.onload = async () => {
const content = reader.result;
let html;
try {
const raw = d.parse(content);
html = `<div class="markdown">${DOMPurify.sanitize(raw)}</div>`;
} catch {
html = `<pre class="markdown">${content}</pre>`;
}
this.messages.push({
id: Date.now(),
text: html,
sender: 'user',
timestamp: new Date(),
isHtml: true
});
this.renderMessages({ autoScroll: 'bottom' });
const sessionId = localStorage.getItem('qsc_session_id') || this.sessionId || this._getSessionId();
const modelToSend = this.selectedModel ? (this.selectedModel.model || this.selectedModel.name) : undefined;
try {
const response = await fetch(this.restUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: Date.now(), type: 'markdown', data: content, filename: file.name, session_id: sessionId , model:modelToSend})
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
this.handleRestBotResponse(data);
} catch (err) {
this.messages = this.messages.filter(m => !m.isLoading);
this._pushSystem(this.errMsg);
console.error('Send fetch failed', err);
}
};
reader.readAsText(file);
}
fileInput.value = '';
});
}
// Model dropdown toggle
const modelToggleBtn = this.shadowRoot.querySelector('.model-toggle-btn');
if (modelToggleBtn) {
modelToggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.showModelMenu = !this.showModelMenu;
this._renderModelDropdown();
});
}
document.addEventListener('click', () => {
if (this.showModelMenu) {
this.showModelMenu = false;
this._renderModelDropdown(); // Only render the dropdown part
}
});
}
}
customElements.define('qsc-chatbot', QscChatbot);
})();