// 轻量 Markdown 渲染(AI 控制台回答用)——不引外部库/CDN(生产 220 在内网)。 // 安全:**先 esc() 转义,再套标记**——模型输出里的原始 HTML 只会以文本显示, // 不会变成可执行标签。 // 支持:标题 / 段落 / 换行 / 粗体 / 斜体 / 删除线 / 行内代码 / 围栏代码块 / // 有序与无序列表(含一层以上嵌套)/ 引用 / 表格 / 分隔线 / 链接。 // 依赖:esc()(base.js,本文件须在其后加载) // 行内代码占位符(控制字符,正文几乎不可能出现) const _MD_PH = '\u0001'; function _mdInline(s){ const codes = []; // 行内代码先摘出,避免其中的 ** * ~~ 被继续当标记解析 s = s.replace(/`([^`]+)`/g, (m, c)=>{ codes.push(c); return _MD_PH + (codes.length - 1) + _MD_PH; }); s = s.replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/~~([^~]+)~~/g, '$1') .replace(/(^|[^*])\*([^*\n]+)\*/g, '$1$2') .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '$1'); return s.replace(new RegExp(_MD_PH + '(\\d+)' + _MD_PH, 'g'), (m, i)=>'' + codes[+i] + ''); } // 表格行 → 单元格数组 function _mdCells(line){ return line.trim().replace(/^\|/, '').replace(/\|$/, '') .split('|').map(c=>c.trim()); } // 列表项递归渲染(缩进 = 层级;同类标记合并,类型变了外层另起一个列表) function _mdList(items, out, idx, indent){ const ordered = items[idx].ordered; const tag = ordered ? 'ol' : 'ul'; out.push('<' + tag + ' class="md-list">'); while(idx < items.length && items[idx].indent >= indent){ const it = items[idx]; if(it.indent > indent) break; // 更深缩进块由外层另起列表(避免非法嵌套) if(it.ordered !== ordered) break; // 同级但标记类型变了 → 交回外层 out.push('
  • ' + _mdInline(esc(it.text))); if(idx + 1 < items.length && items[idx + 1].indent > indent){ idx = _mdList(items, out, idx + 1, items[idx + 1].indent); out.push('
  • '); continue; } out.push(''); idx++; } out.push(''); return idx; } // 主入口:Markdown 文本 → HTML 字符串 function renderMarkdown(src){ const lines = String(src == null ? '' : src).replace(/\r\n?/g, '\n').split('\n'); const out = []; let para = []; let i = 0; const flushPara = ()=>{ if(!para.length) return; out.push('

    ' + para.map(l=>_mdInline(esc(l))).join('
    ') + '

    '); para = []; }; while(i < lines.length){ const ln = lines[i]; let m; // 围栏代码块 ```lang … ``` if(/^\s*```/.test(ln)){ flushPara(); const body = []; i++; while(i < lines.length && !/^\s*```/.test(lines[i])){ body.push(lines[i]); i++; } if(i < lines.length) i++; // 跳过结束围栏 out.push('
    ' + esc(body.join('\n')) + '
    '); continue; } // 表格:表头行 + 分隔行(|---|---|) if(/^\s*\|.*\|\s*$/.test(ln) && i + 1 < lines.length && /^\s*\|[\s:|-]+\|\s*$/.test(lines[i + 1])){ flushPara(); const head = _mdCells(ln); i += 2; const rows = []; while(i < lines.length && /^\s*\|.*\|\s*$/.test(lines[i])){ rows.push(_mdCells(lines[i])); i++; } out.push('' + head.map(c=>'').join('') + '' + rows.map(r=>'' + r.map(c=>'').join('') + '').join('') + '
    ' + _mdInline(esc(c)) + '
    ' + _mdInline(esc(c)) + '
    '); continue; } // 标题 #~###### m = ln.match(/^\s{0,3}(#{1,6})\s+(.*)$/); if(m){ flushPara(); const lv = m[1].length; out.push('' + _mdInline(esc(m[2])) + ''); i++; continue; } // 分隔线 --- / *** / ___ if(/^\s{0,3}([-*_])\s*(\1\s*){2,}$/.test(ln)){ flushPara(); out.push('
    '); i++; continue; } // 引用 > if(/^\s{0,3}>\s?/.test(ln)){ flushPara(); const body = []; while(i < lines.length && /^\s{0,3}>\s?/.test(lines[i])){ body.push(lines[i].replace(/^\s{0,3}>\s?/, '')); i++; } out.push('
    ' + body.map(l=>_mdInline(esc(l))).join('
    ') + '
    '); continue; } // 列表(- * + / 1. 1))——含缩进续行 m = ln.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/); if(m){ flushPara(); const items = []; while(i < lines.length){ const mm = lines[i].match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/); if(!mm) break; items.push({indent: mm[1].replace(/\t/g, ' ').length, ordered: /\d/.test(mm[2]), text: mm[3]}); i++; // 紧跟的纯缩进行算上一条的续行 while(i < lines.length && /^\s+\S/.test(lines[i]) && !/^\s*([-*+]|\d+[.)])\s+/.test(lines[i])){ items[items.length - 1].text += ' ' + lines[i].trim(); i++; } } let k = 0; while(k < items.length){ const n = _mdList(items, out, k, items[k].indent); if(n <= k) break; // 防死循环 k = n; } continue; } // 空行 = 段落分隔 if(!ln.trim()){ flushPara(); i++; continue; } para.push(ln); i++; } flushPara(); return out.join(''); }