/* Safe chat Markdown: parse tokens, build a restricted DOM, never insert model HTML. */ (()=>{'use strict'; const tags=new Set(['p','strong','em','s','blockquote','ul','ol','li','h1','h2','h3','h4','h5','h6','table','thead','tbody','tr','th','td','a']); let parser; function getParser(){ if(!parser)parser=window.markdownit({html:false,linkify:false,typographer:false,breaks:true}); return parser; } function safeHref(value){ if(typeof value!=='string'||/[\u0000-\u0020\u007f]/.test(value))return null; try{const url=new URL(value,location.href);return ['http:','https:','mailto:'].includes(url.protocol)?url.href:null}catch{return null} } function appendTokens(tokens,parent){ const stack=[parent]; const current=()=>stack[stack.length-1]; const text=value=>current().append(document.createTextNode(value||'')); for(const token of tokens){ if(token.hidden)continue; if(token.type==='inline'){appendTokens(token.children||[],current());continue} if(token.type==='text'||token.type==='html_inline'||token.type==='html_block'){text(token.content);continue} if(token.type==='softbreak'||token.type==='hardbreak'){current().append(document.createElement('br'));continue} if(token.type==='code_inline'){ const el=document.createElement('code');el.textContent=token.content;current().append(el);continue; } if(token.type==='fence'||token.type==='code_block'){ const pre=document.createElement('pre'),code=document.createElement('code');code.textContent=token.content;pre.append(code);current().append(pre);continue; } if(token.type==='image'){text(token.content||'图片');continue} if(token.type==='hr'){current().append(document.createElement('hr'));continue} if(token.nesting===-1){if(stack.length>1)stack.pop();continue} if(token.nesting===1){ const el=document.createElement(tags.has(token.tag)?token.tag:'span'); if(token.tag==='a'){ const href=safeHref(token.attrGet('href')); if(href){el.href=href;el.target='_blank';el.rel='noopener noreferrer';el.referrerPolicy='no-referrer'} } if(token.tag==='ol'){ const start=token.attrGet('start');if(start&&/^\d{1,9}$/.test(start))el.start=Number(start); } if(token.tag==='th'||token.tag==='td'){ const align=token.attrGet('style')||''; if(/^text-align:(left|center|right)$/.test(align))el.style.textAlign=align.slice(11); } current().append(el);stack.push(el);continue; } text(token.content); } } function render(element,source){ const value=typeof source==='string'?source:''; try{ const fragment=document.createDocumentFragment(); appendTokens(getParser().parse(value,{}),fragment); element.replaceChildren(fragment);element.classList.add('chat-markdown'); }catch{ element.textContent=value;element.classList.remove('chat-markdown'); } } function toSpeechText(source){ const root=document.createElement('div');render(root,source); const blocks=new Set(['P','LI','H1','H2','H3','H4','H5','H6','BLOCKQUOTE','PRE','TR','UL','OL']); function read(node){ if(node.nodeType===3)return node.nodeValue; if(node.nodeName==='BR')return '\n'; const value=Array.from(node.childNodes,read).join(''); return value+(blocks.has(node.nodeName)?'\n':node.nodeName==='TD'||node.nodeName==='TH'?',':''); } return read(root).trim(); } window.ZhiweiChatMarkdown=Object.freeze({render,toSpeechText}); })();