- docs 后端从 go run 改为 build+spawn(编译成功才切换,与 fun run 同语义) - watchLoop 复用:.go/go.mod/go.sum 变更→重采元数据+重启后端 - 新增 /fun-version 端点;文档页轮询版本变化自动刷新(编辑中推迟) - snapshot 跳过 fun_gen_helper_test.go,避免采集辅助文件自触发 - -from 静态模式不启用;-no-run 仍刷新元数据(后端由外部管理) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
693 lines
37 KiB
HTML
693 lines
37 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>fun docs</title>
|
||
<style>
|
||
:root{
|
||
--bg:#f4f5f8;--panel:#ffffff;--bg2:#eef0f4;
|
||
--line:#e4e7ed;--line2:#d4d9e1;
|
||
--fg:#1b2230;--fg2:#5d6575;--fg3:#97a0af;
|
||
--acc:#6366f1;--acc-strong:#4f46e5;--accbg:rgba(99,102,241,.11);
|
||
--ok:#16a34a;--okbg:rgba(22,163,74,.12);
|
||
--err:#dc2626;--errbg:rgba(220,38,38,.10);
|
||
--stream:#b45309;--streambg:rgba(217,119,6,.13);
|
||
--code-bg:#10162a;--code-fg:#d7deeb;
|
||
--tk-key:#82aaff;--tk-str:#c3e88d;--tk-num:#f78c6c;--tk-lit:#c792ea;
|
||
--shadow:0 1px 2px rgba(16,24,40,.05),0 4px 16px -4px rgba(16,24,40,.06);
|
||
--mono:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;
|
||
}
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
html,body{height:100%}
|
||
body{background:var(--bg);color:var(--fg);font:14px/1.65 -apple-system,'Segoe UI',system-ui,'PingFang SC','Microsoft YaHei',sans-serif;display:flex;flex-direction:column;overflow:hidden}
|
||
::selection{background:var(--accbg)}
|
||
/* 顶栏 */
|
||
header{display:flex;align-items:center;gap:10px;height:52px;padding:0 18px;background:var(--panel);border-bottom:1px solid var(--line);flex-shrink:0}
|
||
header .logo{font-family:var(--mono);font-weight:700;font-size:15px;letter-spacing:-.01em}
|
||
header .logo b{color:var(--acc);font-weight:700}
|
||
header .meta{color:var(--fg3);font-size:12.5px}
|
||
.dot{width:8px;height:8px;border-radius:50%;background:var(--ok);position:relative;flex-shrink:0}
|
||
.dot::after{content:'';position:absolute;inset:-4px;border-radius:50%;border:2px solid var(--ok);opacity:0;animation:pulse 2.4s ease-out infinite}
|
||
@keyframes pulse{0%{transform:scale(.4);opacity:.7}70%,100%{transform:scale(1.1);opacity:0}}
|
||
.spacer{flex:1}
|
||
|
||
#layout{flex:1;display:flex;min-height:0}
|
||
/* 侧栏 */
|
||
aside{width:272px;min-width:272px;border-right:1px solid var(--line);display:flex;flex-direction:column;background:var(--panel)}
|
||
.searchbox{position:relative;margin:14px 14px 8px}
|
||
.searchbox>svg{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:14px;height:14px;color:var(--fg3);pointer-events:none}
|
||
.searchbox .kbd{position:absolute;right:8px;top:50%;transform:translateY(-50%)}
|
||
.kbd{font-family:var(--mono);font-size:10px;line-height:16px;border:1px solid var(--line);border-bottom-width:2px;border-radius:5px;padding:0 5px;color:var(--fg3);background:var(--bg)}
|
||
#search{width:100%;padding:7px 30px 7px 31px;border:1px solid var(--line);border-radius:8px;font-size:13px;outline:none;background:var(--bg);color:var(--fg);transition:border-color .15s,box-shadow .15s}
|
||
#search::placeholder{color:var(--fg3)}
|
||
#search:focus{border-color:var(--acc);box-shadow:0 0 0 3px var(--accbg)}
|
||
#tree{flex:1;overflow-y:auto;padding:4px 10px 20px}
|
||
.svc{display:flex;align-items:center;justify-content:space-between;margin:18px 8px 5px;font-size:11px;color:var(--fg3);font-weight:700;letter-spacing:.09em;text-transform:uppercase}
|
||
.svc .cnt{font-family:var(--mono);font-weight:600;font-size:10px;color:var(--fg3);background:var(--bg2);border-radius:8px;padding:0 6px;line-height:16px}
|
||
.mth{display:flex;align-items:center;gap:7px;padding:5px 10px;border-radius:8px;cursor:pointer;font-size:13.5px;color:var(--fg2);transition:background .12s,color .12s;position:relative}
|
||
.mth:hover{background:var(--bg2);color:var(--fg)}
|
||
.mth.on{background:var(--accbg);color:var(--fg)}
|
||
.mth.on::before{content:'';position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:16px;background:var(--acc);border-radius:0 3px 3px 0}
|
||
.mth .nm{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:13px}
|
||
.mth.on .nm{font-weight:600;color:var(--acc-strong)}
|
||
.badge{font-size:9px;padding:0 5px;border-radius:6px;font-family:var(--mono);font-weight:600;letter-spacing:.04em;line-height:15px}
|
||
.b-stream{color:var(--stream);background:var(--streambg)}
|
||
/* 正文 */
|
||
main{flex:1;overflow-y:auto;padding:30px 40px 90px}
|
||
.doc{max-width:880px}
|
||
.crumb{font-size:12px;color:var(--fg3);font-family:var(--mono);margin-bottom:6px}
|
||
.titlerow{display:flex;align-items:center;gap:14px}
|
||
.titlerow>div{min-width:0;flex:1}
|
||
h1{font-size:23px;font-weight:700;letter-spacing:-.02em;font-family:var(--mono);display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||
.tag{font-size:10.5px;font-weight:600;padding:2px 9px;border-radius:9px;font-family:var(--mono);letter-spacing:.02em}
|
||
.t-stream{color:var(--stream);background:var(--streambg)}
|
||
.endpoint{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:16px 0 8px;padding:11px 14px;background:var(--panel);border:1px solid var(--line);border-radius:10px;box-shadow:var(--shadow);font-family:var(--mono);font-size:12.5px;color:var(--fg2)}
|
||
.http-method{background:var(--ok);color:#fff;font-weight:700;font-size:10.5px;padding:2.5px 8px;border-radius:6px;letter-spacing:.06em}
|
||
.endpoint .path{color:var(--fg);font-weight:600}
|
||
.endpoint .sep{color:var(--line2)}
|
||
.endpoint b{color:var(--acc-strong);font-weight:600}
|
||
h2{font-size:15px;font-weight:650;margin:32px 0 12px;display:flex;align-items:baseline;gap:8px}
|
||
h2::before{content:'';align-self:center;width:4px;height:14px;border-radius:2px;background:var(--acc);flex-shrink:0}
|
||
h2 .sub{font-weight:400;color:var(--fg3);font-size:12px;margin-left:2px}
|
||
/* 参数表 */
|
||
.tbl{border:1px solid var(--line);border-radius:10px;background:var(--panel);box-shadow:var(--shadow)}
|
||
table.params{width:100%;border-collapse:collapse;font-size:13px}
|
||
table.params tr:first-child th:first-child{border-top-left-radius:10px}
|
||
table.params tr:first-child th:last-child{border-top-right-radius:10px}
|
||
table.params th{text-align:left;color:var(--fg3);font-weight:600;font-size:11px;letter-spacing:.04em;text-transform:uppercase;padding:8px 12px;background:var(--bg2);border-bottom:1px solid var(--line);white-space:nowrap}
|
||
table.params td{padding:8px 12px;border-bottom:1px solid var(--line);vertical-align:top}
|
||
table.params tr:last-child>td{border-bottom:none}
|
||
table.params td.name{font-family:var(--mono);white-space:nowrap;font-size:12.5px}
|
||
table.params td.type{font-family:var(--mono);color:var(--acc-strong);font-size:12px;white-space:nowrap}
|
||
table.params td.name .req{color:var(--err);font-weight:700}
|
||
table.params td.name .opt{color:var(--fg3);font-size:10.5px;border:1px solid var(--line);border-radius:6px;padding:0 4px;margin-left:4px}
|
||
input[type=text],textarea{width:100%;background:var(--panel);border:1px solid var(--line);border-radius:7px;color:var(--fg);padding:6px 10px;font-size:12.5px;font-family:var(--mono);outline:none;transition:border-color .15s,box-shadow .15s}
|
||
input[type=text]:hover,textarea:hover{border-color:var(--line2)}
|
||
input:focus,textarea:focus{border-color:var(--acc);box-shadow:0 0 0 3px var(--accbg)}
|
||
textarea{min-height:56px;resize:vertical}
|
||
/* 自定义下拉 */
|
||
.xselect{position:relative}
|
||
.xsel-btn{width:100%;display:flex;align-items:center;gap:8px;background:var(--panel);border:1px solid var(--line);border-radius:7px;padding:6px 10px;font:12.5px var(--mono);color:var(--fg);cursor:pointer;text-align:left;outline:none;transition:border-color .15s,box-shadow .15s}
|
||
.xsel-btn:hover{border-color:var(--line2)}
|
||
.xselect.open .xsel-btn{border-color:var(--acc);box-shadow:0 0 0 3px var(--accbg)}
|
||
.xsel-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.xsel-label.ph{color:var(--fg3)}
|
||
.xsel-btn>svg{width:12px;height:12px;color:var(--fg3);flex-shrink:0;transition:transform .15s}
|
||
.xselect.open .xsel-btn>svg{transform:rotate(180deg)}
|
||
.xsel-pop{display:none;position:absolute;z-index:60;top:calc(100% + 4px);left:0;right:0;background:var(--panel);border:1px solid var(--line);border-radius:9px;box-shadow:0 10px 28px -8px rgba(16,24,40,.22),0 2px 6px rgba(16,24,40,.08);padding:4px;max-height:230px;overflow:auto}
|
||
.xselect.up .xsel-pop{top:auto;bottom:calc(100% + 4px)}
|
||
.xselect.open .xsel-pop{display:block}
|
||
.xopt{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:6px;font-size:12.5px;font-family:var(--mono);color:var(--fg);cursor:pointer;user-select:none}
|
||
.xopt:hover{background:var(--accbg)}
|
||
.xopt.sel{color:var(--acc-strong);font-weight:600}
|
||
.xopt.sel::after{content:'✓';margin-left:auto;color:var(--acc)}
|
||
.xopt.empty{color:var(--fg3)}
|
||
.nest{border-left:2px solid var(--accbg);margin:4px 0 2px;padding-left:14px}
|
||
.nest .tbl{box-shadow:none}
|
||
.subhdr{font-family:var(--mono);font-size:11.5px;color:var(--fg3);margin:8px 0 6px}
|
||
/* 发送 */
|
||
#sendBar{display:flex;gap:10px;align-items:center;margin:4px 0 4px}
|
||
.btn{border:none;border-radius:8px;padding:8px 22px;font-size:13.5px;font-weight:600;cursor:pointer;transition:all .15s}
|
||
#send{background:var(--acc);color:#fff;box-shadow:0 1px 2px rgba(0,0,0,.12)}
|
||
#send:hover{background:var(--acc-strong)}
|
||
#send:active{transform:translateY(1px)}
|
||
#send:disabled{opacity:.55;cursor:wait}
|
||
#send.stop{background:var(--err)}
|
||
.ghost{background:var(--panel);border:1px solid var(--line);color:var(--fg2);border-radius:8px;padding:7px 14px;cursor:pointer;font-size:12.5px;font-weight:500;transition:all .15s}
|
||
.ghost:hover{border-color:var(--acc);color:var(--acc)}
|
||
.sharebtn{display:flex;align-items:center;gap:6px;padding:5px 12px;font-size:12.5px}
|
||
.sharebtn svg{width:13px;height:13px}
|
||
.mini{background:none;border:1px solid var(--line);border-radius:6px;color:var(--fg2);font-size:11px;padding:1px 9px;cursor:pointer;transition:all .15s}
|
||
.mini:hover{border-color:var(--acc);color:var(--acc)}
|
||
#sendHint{color:var(--fg3);font-size:12.5px}
|
||
/* state */
|
||
#stateRows .srow{display:grid;grid-template-columns:1fr 1.4fr 30px;gap:8px;margin-bottom:6px;max-width:560px}
|
||
#stateRows .srow button{background:none;border:none;color:var(--fg3);cursor:pointer;font-size:16px;border-radius:6px;transition:color .12s}
|
||
#stateRows .srow button:hover{color:var(--err)}
|
||
#addState{background:var(--panel);border:1px dashed var(--line2);color:var(--fg2);border-radius:8px;padding:4px 14px;cursor:pointer;font-size:12.5px;transition:all .15s}
|
||
#addState:hover{border-color:var(--acc);color:var(--acc);background:var(--accbg)}
|
||
/* 请求/响应块——深色编辑器风格 */
|
||
.block{border:1px solid var(--line);border-radius:12px;margin-top:8px;overflow:hidden;box-shadow:var(--shadow)}
|
||
.bhead{display:flex;align-items:center;gap:8px;padding:8px 14px;background:var(--panel);border-bottom:1px solid var(--line);font-family:var(--mono);font-size:12px;color:var(--fg2)}
|
||
.chips{display:flex;align-items:center;gap:8px;flex:1;min-width:0;flex-wrap:wrap}
|
||
.chip{padding:1.5px 9px;border-radius:9px;font-weight:600;font-size:11px;white-space:nowrap}
|
||
.c-ok{color:var(--ok);background:var(--okbg)}
|
||
.c-err{color:var(--err);background:var(--errbg)}
|
||
.c-info{color:var(--acc-strong);background:var(--accbg)}
|
||
textarea.body{width:100%;border:none;outline:none;background:var(--code-bg);padding:15px 17px;font-family:var(--mono);font-size:12.5px;line-height:1.65;overflow:auto;min-height:120px;max-height:50vh;white-space:pre;resize:vertical;color:var(--code-fg);display:block;border-radius:0}
|
||
textarea.body:focus{box-shadow:none}
|
||
pre.body{padding:15px 17px;font-family:var(--mono);font-size:12.5px;line-height:1.65;overflow:auto;max-height:50vh;white-space:pre-wrap;word-break:break-all;color:var(--code-fg);background:var(--code-bg)}
|
||
.tk-key{color:var(--tk-key)}
|
||
.tk-str{color:var(--tk-str)}
|
||
.tk-num{color:var(--tk-num)}
|
||
.tk-lit{color:var(--tk-lit)}
|
||
.empty{color:var(--fg3);font-size:12.5px;font-family:var(--mono)}
|
||
.note{color:var(--fg2);font-size:12.5px;margin-top:8px}
|
||
.note code,td.hint code{font-family:var(--mono);background:var(--bg2);border:1px solid var(--line);border-radius:5px;padding:0 5px;font-size:11.5px;color:var(--acc-strong)}
|
||
/* 可编辑说明 */
|
||
#mnote{min-height:64px;font-family:inherit;font-size:13px;background:var(--panel);border-radius:10px;padding:10px 13px;box-shadow:var(--shadow);resize:vertical}
|
||
#mnote::placeholder{color:var(--fg3)}
|
||
.fnote{font-size:11.5px;padding:4px 8px;background:var(--bg);border-style:dashed;color:var(--fg)}
|
||
.fnote::placeholder{color:var(--fg3)}
|
||
#editHint{margin-left:auto;font-weight:400;letter-spacing:0;text-transform:none;font-size:11.5px;color:var(--ok)}
|
||
/* 类型树 */
|
||
.tnode{display:inline-flex;align-items:center;gap:5px;cursor:pointer;user-select:none;color:var(--acc-strong)}
|
||
.tnode:hover{text-decoration:underline}
|
||
.tnode .caret{width:9px;color:var(--fg3);transition:transform .15s;display:inline-block;font-size:10px}
|
||
.tnode.open .caret{transform:rotate(90deg)}
|
||
.tkids{display:none;margin:2px 0 2px 7px;padding-left:9px;border-left:1px solid var(--line);font-size:11.5px;line-height:1.75}
|
||
.tnode.open+.tkids{display:block}
|
||
.tkids .tkids{display:block}
|
||
.tkid{color:var(--fg);white-space:nowrap}
|
||
.tkid .tv{color:var(--fg3)}
|
||
.tkid b{font-weight:500;font-family:var(--mono)}
|
||
.rettype{display:inline-block;font-family:var(--mono);font-size:13px;color:var(--acc-strong);background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:8px 16px;box-shadow:var(--shadow)}
|
||
::-webkit-scrollbar{width:10px;height:10px}
|
||
::-webkit-scrollbar-thumb{background:var(--line2);border-radius:5px;border:2px solid var(--bg)}
|
||
::-webkit-scrollbar-thumb:hover{background:var(--fg3)}
|
||
#empty{margin-top:22vh;text-align:center;color:var(--fg3)}
|
||
#empty .eico{width:46px;height:46px;margin:0 auto 14px;border:1px dashed var(--line2);border-radius:12px;display:flex;align-items:center;justify-content:center;color:var(--fg3)}
|
||
#empty .eico svg{width:22px;height:22px}
|
||
#empty p{font-size:13.5px}
|
||
#empty .kbd{margin-left:4px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<span class="logo">fun<b>·docs</b></span>
|
||
<span class="dot" id="live"></span>
|
||
<span class="meta" id="hmeta"></span>
|
||
<span class="spacer"></span>
|
||
</header>
|
||
<div id="layout">
|
||
<aside>
|
||
<div class="searchbox">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||
<input id="search" placeholder="搜索服务或方法…" autofocus>
|
||
<span class="kbd">/</span>
|
||
</div>
|
||
<div id="tree"></div>
|
||
</aside>
|
||
<main id="main"><div id="empty">
|
||
<div class="eico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg></div>
|
||
<p>在左侧选择一个方法查看文档</p>
|
||
<p style="margin-top:6px;font-size:12px">按<span class="kbd">/</span>快速搜索</p>
|
||
</div></main>
|
||
</div>
|
||
<script>
|
||
let META=null, CUR=null, ABORT=null, EDITS={}, ACTIVE='';
|
||
const $=s=>document.querySelector(s);
|
||
const esc=s=>String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||
|
||
document.addEventListener('keydown',e=>{
|
||
if(e.key==='/'&&!/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)){e.preventDefault();$('#search').focus()}
|
||
});
|
||
|
||
/* 点击委托(绑一次):类型树展开、自定义下拉、点空白收起下拉 */
|
||
document.addEventListener('click',e=>{
|
||
const tn=e.target.closest('.tnode');
|
||
if(tn){tn.classList.toggle('open');return}
|
||
const btn=e.target.closest('.xsel-btn');
|
||
if(btn){
|
||
const xs=btn.parentElement;
|
||
const was=xs.classList.contains('open');
|
||
document.querySelectorAll('.xselect.open').forEach(x=>x.classList.remove('open'));
|
||
if(!was){
|
||
const r=btn.getBoundingClientRect();
|
||
xs.classList.toggle('up',r.bottom+250>innerHeight&&r.top>250);
|
||
xs.classList.add('open');
|
||
}
|
||
return;
|
||
}
|
||
const opt=e.target.closest('.xopt');
|
||
if(opt){
|
||
const xs=opt.closest('.xselect'),inp=xs.querySelector('input'),lbl=xs.querySelector('.xsel-label');
|
||
inp.value=opt.dataset.v;
|
||
inp.dispatchEvent(new Event('input',{bubbles:true}));
|
||
lbl.textContent=opt.dataset.v===''?'请选择…':opt.textContent;
|
||
lbl.classList.toggle('ph',opt.dataset.v==='');
|
||
xs.querySelectorAll('.xopt').forEach(o=>o.classList.toggle('sel',o===opt));
|
||
xs.classList.remove('open');
|
||
return;
|
||
}
|
||
if(!e.target.closest('.xselect'))document.querySelectorAll('.xselect.open').forEach(x=>x.classList.remove('open'));
|
||
});
|
||
|
||
Promise.all([
|
||
fetch('/fun-meta.json').then(r=>r.json()),
|
||
fetch('/fun-docs-edits.json').then(r=>r.json()).catch(()=>({}))
|
||
]).then(([m,e])=>{
|
||
META=m;EDITS=e||{};
|
||
renderTree('');
|
||
selectFromHash();
|
||
}).catch(e=>{const h=$('#hmeta');h.textContent='元数据加载失败: '+e.message;h.style.color='var(--err)';$('#live').style.background='var(--err)'});
|
||
|
||
/* 热更新:源码变更触发后端重建+元数据刷新后,文档页自动重载;
|
||
正在输入时推迟一轮,避免打断编辑(防抖保存会先把备注落盘) */
|
||
let DOC_V=null;
|
||
setInterval(async()=>{
|
||
try{
|
||
const r=await fetch('/fun-version').then(r=>r.json());
|
||
if(DOC_V===null){DOC_V=r.v;return}
|
||
if(r.v!==DOC_V){
|
||
const ae=document.activeElement;
|
||
if(ae&&/INPUT|TEXTAREA/.test(ae.tagName))return;
|
||
location.reload();
|
||
}
|
||
}catch{}
|
||
},1500);
|
||
|
||
window.addEventListener('hashchange',selectFromHash);
|
||
/* 深链:#Svc.Method 或 #Svc.Method?req=<base64url 请求体> */
|
||
function selectFromHash(){
|
||
let h=decodeURIComponent(location.hash.slice(1));
|
||
if(!h||!META)return;
|
||
const qi=h.indexOf('?');
|
||
const mpart=qi<0?h:h.slice(0,qi);
|
||
const i=mpart.lastIndexOf('.');
|
||
const svc=META.services.find(s=>s.name===mpart.slice(0,i));
|
||
const m=svc&&svc.methods.find(x=>x.name===mpart.slice(i+1));
|
||
if(!m)return;
|
||
if(!(mpart===ACTIVE&&CUR)){ACTIVE=mpart;renderTree($('#search').value);showMethod(svc,m)}
|
||
if(qi>=0){
|
||
const req=new URLSearchParams(h.slice(qi+1)).get('req');
|
||
if(req){try{
|
||
reqDirty=true;
|
||
$('#reqBody').value=pretty(b64uDec(req));
|
||
fitReq();
|
||
$('#reqHead').querySelector('.chip').textContent='来自分享链接';
|
||
$('#resetReq').style.display='';
|
||
}catch{}}
|
||
}
|
||
}
|
||
const b64u=s=>btoa(unescape(encodeURIComponent(s))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
|
||
const b64uDec=s=>decodeURIComponent(escape(atob(s.replace(/-/g,'+').replace(/_/g,'/'))));
|
||
|
||
function share(){
|
||
if(!CUR)return;
|
||
let url=location.origin+location.pathname+'#'+encodeURIComponent(ACTIVE);
|
||
try{url+='?req='+b64u(JSON.stringify(JSON.parse($('#reqBody').value)))}catch{}
|
||
navigator.clipboard.writeText(url);
|
||
const b=$('#shareBtn'),old=b.innerHTML;
|
||
b.innerHTML='已复制 ✓';b.style.color='var(--ok)';b.style.borderColor='var(--ok)';
|
||
setTimeout(()=>{b.innerHTML=old;b.style.color='';b.style.borderColor=''},1500);
|
||
}
|
||
|
||
$('#search').addEventListener('input',e=>renderTree(e.target.value));
|
||
|
||
function renderTree(q){
|
||
q=(q||'').toLowerCase();
|
||
const t=$('#tree');t.innerHTML='';
|
||
META.services.forEach(svc=>{
|
||
const ms=svc.methods.filter(m=>!q||m.name.toLowerCase().includes(q)||svc.name.toLowerCase().includes(q));
|
||
if(!ms.length)return;
|
||
const h=document.createElement('div');h.className='svc';
|
||
h.innerHTML=`<span>${esc(svc.name)}</span><span class="cnt">${ms.length}</span>`;
|
||
t.appendChild(h);
|
||
ms.forEach(m=>{
|
||
const d=document.createElement('div');d.className='mth';
|
||
if(svc.name+'.'+m.name===ACTIVE)d.classList.add('on');
|
||
d.innerHTML=`<span class="nm">${esc(m.name)}</span>`+
|
||
(m.isStream?'<span class="badge b-stream">stream</span>':'');
|
||
d.onclick=()=>{ACTIVE=svc.name+'.'+m.name;history.replaceState(null,'','#'+encodeURIComponent(ACTIVE));document.querySelectorAll('.mth').forEach(x=>x.classList.remove('on'));d.classList.add('on');showMethod(svc,m)};
|
||
t.appendChild(d);
|
||
});
|
||
});
|
||
}
|
||
|
||
function typeLabel(t){
|
||
if(!t)return '—';
|
||
if(t.kind==='struct'||t.kind==='enum')return t.name+(t.optional?'?':'');
|
||
if(t.kind==='slice')return typeLabel(t.elem)+'[]';
|
||
return (t.optional?t.name+'?':t.name);
|
||
}
|
||
|
||
function showMethod(svc,m){
|
||
CUR={svc,m};reqDirty=false;ABORT&&ABORT.abort();ABORT=null;
|
||
const badges=m.isStream?'<span class="tag t-stream">NDJSON 流式</span>':'';
|
||
const retLine = m.isStream
|
||
? (m.returns?`首条消息 <b>${esc(typeLabel(m.returns))}</b>,其后逐行推送 NDJSON`:`持续推送 NDJSON 行`)
|
||
: (m.void?`无数据返回,仅 <b>result.void</b>`:(m.returns?`<b>${esc(typeLabel(m.returns))}</b>`:''));
|
||
const dtoName = m.dto?esc(m.dto.name):'无';
|
||
$('#main').innerHTML=`<div class="doc">
|
||
<div class="titlerow"><div>
|
||
<div class="crumb">${esc(svc.name)} / ${esc(m.name)}</div>
|
||
<h1>${esc(svc.name)}.${esc(m.name)} ${badges}</h1>
|
||
</div>
|
||
<button class="ghost sharebtn" id="shareBtn" title="复制分享链接(含当前方法与请求体)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4"/></svg>分享</button>
|
||
</div>
|
||
<div class="endpoint"><span class="http-method">POST</span><span class="path">/cell</span><span class="sep">·</span><span>serviceName=<b>${esc(svc.name)}</b></span><span class="sep">·</span><span>methodName=<b>${esc(m.name)}</b></span></div>
|
||
<div class="note">返回:${retLine} · 请求 DTO:${dtoName}${m.dto&&m.dto.fields?'':'(该方法不接收参数)'}</div>
|
||
|
||
<h2>说明<span class="sub">可编辑 · 自动保存到本地 JSON 存档</span><span id="editHint"></span></h2>
|
||
<textarea id="mnote" placeholder="为这个接口写点说明…" spellcheck="false"></textarea>
|
||
|
||
${m.dto?`
|
||
<h2>请求参数<span class="sub">${esc(m.dto.name)}</span></h2>
|
||
${paramsTable(m.dto,[])}
|
||
<div class="note">带 <code>*</code> 为必填;可空字段留空即不发送;切片按 JSON 数组填写。字段名以小写开头随请求传输。</div>
|
||
`:''}
|
||
|
||
${m.returns?`
|
||
<h2>返回类型<span class="sub">${esc(typeLabel(m.returns))}${m.isStream?' · 首条消息类型':''}</span></h2>
|
||
${m.returns.fields?typeTable(m.returns):`<div class="rettype">${esc(typeLabel(m.returns))}</div>`}
|
||
`:''}
|
||
|
||
<h2>State<span class="sub">随请求携带 · Guard 可读 · 存于 localStorage</span></h2>
|
||
<div id="stateRows"></div>
|
||
<button id="addState">+ 添加字段</button>
|
||
|
||
<h2>试一下<span class="sub">填好参数点发送,请求与响应都会展示在下方</span></h2>
|
||
<div id="sendBar">
|
||
<button class="btn" id="send">发送请求</button>
|
||
<span id="sendHint"></span>
|
||
</div>
|
||
|
||
<h2>请求<span class="sub">表单填充自动生成,可直接编辑后发送</span></h2>
|
||
<div class="note" style="margin:0 0 8px">请求体即发送到 <code>POST /cell</code> 的 JSON,State 值由上方字段注入,也可手动编辑后发送。</div>
|
||
<div class="block"><div class="bhead"><span class="chips" id="reqHead"><span class="chip c-info">表单生成</span></span><button class="mini" id="resetReq" style="display:none">按表单重置</button></div><textarea class="body edit" id="reqBody" spellcheck="false"></textarea></div>
|
||
|
||
<h2>响应</h2>
|
||
<div class="block"><div class="bhead"><span class="chips" id="respHead"><span class="chip c-info">未发送</span></span><button class="mini" id="copyResp">复制</button></div><pre class="body" id="respBody"><span class="empty">发送后在此展示格式化的响应${m.isStream?'(流式方法逐行追加)':''}</span></pre></div>
|
||
</div>`;
|
||
$('#addState').onclick=()=>addStateRow('','');
|
||
const st=JSON.parse(localStorage.getItem('fun-docs-state')||'{}');
|
||
Object.entries(st).forEach(([k,v])=>addStateRow(k,v));
|
||
$('#send').onclick=send;
|
||
$('#shareBtn').onclick=share;
|
||
$('#copyResp').onclick=copyResp;
|
||
// 说明/字段备注:默认空,只回显存档里的用户编辑
|
||
const key=curKey();
|
||
const ta=$('#mnote');ta.value=(EDITS[key]&&EDITS[key].note)||'';
|
||
ta.addEventListener('input',()=>{(EDITS[key]=EDITS[key]||{}).note=ta.value;queueSave(key)});
|
||
document.querySelectorAll('#main .fnote').forEach(el=>{
|
||
el.addEventListener('input',()=>{
|
||
const e=EDITS[key]=EDITS[key]||{};
|
||
const bag=el.dataset.rn!=null?'retFields':'fields';
|
||
(e[bag]=e[bag]||{})[el.dataset.fn||el.dataset.rn]=el.value;
|
||
if(!el.value.trim())delete e[bag][el.dataset.fn||el.dataset.rn];
|
||
queueSave(key);
|
||
});
|
||
});
|
||
|
||
// 表单一变就同步请求体(手动编辑过则不动,可点"按表单重置"恢复)
|
||
document.querySelectorAll('#main input,#main select').forEach(el=>{
|
||
el.addEventListener('input',previewRequest);el.addEventListener('change',previewRequest);
|
||
});
|
||
|
||
document.querySelectorAll('#main textarea[data-t]').forEach(el=>{
|
||
el.addEventListener('input',previewRequest);el.addEventListener('change',previewRequest);
|
||
});
|
||
$('#reqBody').addEventListener('input',()=>{
|
||
reqDirty=true;
|
||
$('#reqHead').querySelector('.chip').textContent='手动编辑';
|
||
$('#resetReq').style.display='';
|
||
fitReq();
|
||
});
|
||
$('#resetReq').onclick=()=>{reqDirty=false;$('#resetReq').style.display='none';$('#reqHead').querySelector('.chip').textContent='表单生成';previewRequest();};
|
||
previewRequest();
|
||
}
|
||
|
||
/* ---------- 参数表 ---------- */
|
||
function paramsTable(t,path){
|
||
let h=`<div class="tbl"><table class="params"><tr><th style="width:190px">字段</th><th style="width:130px">类型</th><th style="width:270px">示例值</th><th>说明</th></tr>`;
|
||
h+=paramRows(t,path);
|
||
return h+'</table></div>';
|
||
}
|
||
function paramRows(t,path){
|
||
let h='';
|
||
t.fields.forEach(f=>{
|
||
const p=[...path,f.name];
|
||
const id='f-'+p.join('.');
|
||
const lp=[...path.map(firstLower),firstLower(f.name)].join('.');
|
||
h+=`<tr><td class="name">${esc(firstLower(f.name))}${f.type.optional?'<span class="opt">可空</span>':'<span class="req">*</span>'}</td><td class="type">${typeCellHTML(f.type)}</td>`;
|
||
h+=`<td>${controlHTML(f.type,id)}</td>`;
|
||
h+=`<td class="hint"><input type="text" class="fnote" data-fn="${esc(lp)}" placeholder="备注…" value="${esc(fieldNote(lp))}"></td></tr>`;
|
||
if(f.type.kind==='struct')h+=`<tr><td colspan="4"><div class="nest"><div class="subhdr">${esc(f.type.name)} 字段:</div>${paramsTable(f.type,p)}</div></td></tr>`;
|
||
});
|
||
return h;
|
||
}
|
||
/* 返回类型表(类型树 + 可编辑备注) */
|
||
function typeTable(t){
|
||
return `<div class="tbl"><table class="params"><tr><th style="width:200px">字段</th><th style="width:170px">类型</th><th>说明</th></tr>${typeRows(t,0,[])}</table></div>`;
|
||
}
|
||
function typeRows(t,depth,path){
|
||
let h='';
|
||
(t.fields||[]).forEach(f=>{
|
||
const lp=[...path.map(firstLower),firstLower(f.name)].join('.');
|
||
h+=`<tr><td class="name">${esc(firstLower(f.name))}</td><td class="type">${typeCellHTML(f.type)}</td><td class="hint"><input type="text" class="fnote" data-rn="${esc(lp)}" placeholder="备注…" value="${esc(retNote(lp))}"></td></tr>`;
|
||
if(f.type.kind==='struct'&&depth<3)h+=`<tr><td colspan="3"><div class="nest"><div class="subhdr">${esc(f.type.name)} 字段:</div><div class="tbl"><table class="params">${typeRows(f.type,depth+1,[...path,f.name])}</table></div></div></td></tr>`;
|
||
});
|
||
return h;
|
||
}
|
||
/* 类型树:enum 成员 / struct 字段(递归)/ slice 取元素类型 */
|
||
function typeCellHTML(t){
|
||
const kids=typeKids(t),label=esc(typeLabel(t));
|
||
if(!kids)return label;
|
||
return `<div class="tnode"><span class="caret">▸</span><span>${label}</span></div><div class="tkids">${kids}</div>`;
|
||
}
|
||
function typeKids(t){
|
||
if(!t)return '';
|
||
if(t.kind==='enum')return (t.names||[]).map((n,i)=>`<div class="tkid">${esc((t.displayNames&&t.displayNames[i])?t.displayNames[i]:n)}<span class="tv"> · ${esc(n)} = ${i}</span></div>`).join('');
|
||
if(t.kind==='struct'&&t.fields)return structKids(t,0);
|
||
if(t.kind==='slice')return typeKids(t.elem);
|
||
return '';
|
||
}
|
||
function structKids(t,depth){
|
||
if(depth>=3)return `<div class="tkid tv">…</div>`;
|
||
return (t.fields||[]).map(f=>{
|
||
const sub=typeKids(f.type);
|
||
return `<div class="tkid"><b>${esc(firstLower(f.name))}</b><span class="tv">: ${esc(typeLabel(f.type))}</span>${sub?`<div class="tkids" style="display:block">${sub}</div>`:''}</div>`;
|
||
}).join('');
|
||
}
|
||
/* 编辑存档 */
|
||
const curKey=()=>CUR.svc.name+'.'+CUR.m.name;
|
||
function fieldNote(lp){
|
||
const e=EDITS[curKey()];
|
||
return (e&&e.fields&&e.fields[lp])||'';
|
||
}
|
||
function retNote(lp){
|
||
const e=EDITS[curKey()];
|
||
return (e&&e.retFields&&e.retFields[lp])||'';
|
||
}
|
||
let saveT=null;
|
||
function queueSave(key){
|
||
clearTimeout(saveT);
|
||
const h=$('#editHint');
|
||
if(h){h.textContent='编辑中…';h.style.color='var(--fg3)'}
|
||
saveT=setTimeout(()=>saveEntry(key),600);
|
||
}
|
||
async function saveEntry(key){
|
||
const e=EDITS[key]||{};
|
||
const empty=!e.note&&!Object.keys(e.fields||{}).length&&!Object.keys(e.retFields||{}).length;
|
||
const h=$('#editHint');
|
||
try{
|
||
const r=await fetch('/fun-docs-edits',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key,entry:empty?null:e})});
|
||
if(h){h.textContent=r.ok?'已保存 ✓':'保存失败';h.style.color=r.ok?'var(--ok)':'var(--err)'}
|
||
}catch{if(h){h.textContent='保存失败';h.style.color='var(--err)'}}
|
||
setTimeout(()=>{const x=$('#editHint');if(x)x.textContent=''},2500);
|
||
}
|
||
function controlHTML(t,id){
|
||
// 可空字段与必填字段同样直接输入;留空即不随请求发送
|
||
return innerControl(t,id);
|
||
}
|
||
function innerControl(t,id){
|
||
switch(t.kind){
|
||
case 'string':return `<input type="text" id="${id}" data-t="string">`;
|
||
case 'int':return `<input type="text" id="${id}" data-t="int" inputmode="numeric" placeholder="0">`;
|
||
case 'bool':return xselectHTML(id,[['','(空)'],['true','true'],['false','false']]);
|
||
case 'enum':{
|
||
const opts=[['','(空)']];
|
||
(t.names||[]).forEach((n,i)=>opts.push([String(i),(t.displayNames&&t.displayNames[i])?t.displayNames[i]+' ('+n+')':n]));
|
||
return xselectHTML(id,opts);
|
||
}
|
||
case 'slice':return `<textarea id="${id}" data-t="slice" placeholder='["a","b"] 或 [{...}]'></textarea>`;
|
||
default:return `<input type="text" id="${id}" data-t="string">`;
|
||
}
|
||
}
|
||
function xselectHTML(id,opts){
|
||
let h=`<div class="xselect"><button type="button" class="xsel-btn"><span class="xsel-label ph">请选择…</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg></button><input type="hidden" id="${id}"><div class="xsel-pop">`;
|
||
opts.forEach(([v,label])=>{h+=`<div class="xopt${v===''?' empty':''}" data-v="${esc(v)}">${esc(label)}</div>`});
|
||
return h+'</div></div>';
|
||
}
|
||
const firstLower=s=>s?s[0].toLowerCase()+s.slice(1):s;
|
||
|
||
/* ---------- 取值 ---------- */
|
||
function collectStruct(t,path,obj,errors){
|
||
t.fields.forEach(f=>{
|
||
const p=[...path,f.name];
|
||
const id='f-'+p.join('.');
|
||
const key=firstLower(f.name);
|
||
let el=document.getElementById(id);
|
||
if(!el)return;
|
||
const v=el.value!=null?el.value.trim():'';
|
||
const kind=f.type.kind;
|
||
if(kind==='struct'){obj[key]=collectStruct(f.type,p,{},errors);return}
|
||
if(v===''){
|
||
const req=!f.type.optional&&kind!=='slice'&&kind!=='bool'&&kind!=='enum';
|
||
if(req)errors.push(`缺少必填字段: ${p.join('.')}`);
|
||
return;
|
||
}
|
||
switch(kind){
|
||
case 'int':{
|
||
const n=Number(v);
|
||
if(!Number.isFinite(n)||!Number.isInteger(n)){errors.push(`${p.join('.')} 不是整数`);return}
|
||
obj[key]=n;break;
|
||
}
|
||
case 'bool':obj[key]=v==='true';break;
|
||
case 'enum':obj[key]=Number(v);break;
|
||
case 'slice':{
|
||
try{obj[key]=JSON.parse(v)}catch{errors.push(`${p.join('.')} 不是合法 JSON 数组`)}
|
||
break;
|
||
}
|
||
default:obj[key]=v;
|
||
}
|
||
});
|
||
return obj;
|
||
}
|
||
let reqDirty=false;
|
||
function buildRequest(){
|
||
const errors=[];
|
||
const body={serviceName:CUR.svc.name,methodName:CUR.m.name};
|
||
if(CUR.m.dto)body.data=collectStruct(CUR.m.dto,[],{},errors);
|
||
const state={};
|
||
document.querySelectorAll('#stateRows .srow').forEach(r=>{
|
||
const k=r.querySelector('[data-sk]').value.trim(),v=r.querySelector('[data-sv]').value;
|
||
if(k)state[k]=v;
|
||
});
|
||
if(Object.keys(state).length)body.state=state;
|
||
localStorage.setItem('fun-docs-state',JSON.stringify(state));
|
||
return {body,errors};
|
||
}
|
||
function addStateRow(k,v){
|
||
const d=document.createElement('div');d.className='srow';
|
||
d.innerHTML=`<input type="text" placeholder="key(如 token)" data-sk value="${esc(k)}"><input type="text" placeholder="value" data-sv value="${esc(v)}"><button title="删除">×</button>`;
|
||
d.querySelector('button').onclick=()=>d.remove();
|
||
$('#stateRows').appendChild(d);
|
||
d.querySelectorAll('input').forEach(i=>i.addEventListener('input',previewRequest));
|
||
}
|
||
function previewRequest(){
|
||
if(!CUR||reqDirty)return;
|
||
const {body,errors}=buildRequest();
|
||
const pre=$('#reqBody');if(!pre)return;
|
||
if(errors.length){pre.value=JSON.stringify({serviceName:CUR.svc.name,methodName:CUR.m.name},null,2);fitReq();return}
|
||
pre.value=pretty(JSON.stringify(body));
|
||
fitReq();
|
||
}
|
||
function fitReq(){
|
||
const t=$('#reqBody');if(!t)return;
|
||
t.style.height='auto';
|
||
t.style.height=Math.min(t.scrollHeight+2,Math.round(innerHeight*.5))+'px';
|
||
}
|
||
function requestBody(){
|
||
const raw=$('#reqBody').value.trim();
|
||
try{return{ok:JSON.parse(raw)}}catch(e){return{ok:null,err:e.message}}
|
||
}
|
||
function copyResp(){
|
||
const t=$('#respBody').textContent;
|
||
if(!t.trim())return;
|
||
navigator.clipboard.writeText(t);
|
||
const b=$('#copyResp');b.textContent='已复制';
|
||
setTimeout(()=>b.textContent='复制',1500);
|
||
}
|
||
|
||
/* ---------- 发送 ---------- */
|
||
async function send(){
|
||
if(ABORT){ABORT.abort();return}
|
||
const parsed=requestBody();
|
||
if(!parsed.ok){
|
||
$('#reqHead').innerHTML='<span class="chip c-err">请求 JSON 无效</span>';
|
||
$('#respBody').innerHTML=`<span style="color:var(--err)">${esc(parsed.err)}</span>`;
|
||
return;
|
||
}
|
||
const body=parsed.ok;
|
||
try{$('#reqBody').value=pretty(JSON.stringify(body));fitReq()}catch{}
|
||
$('#reqHead').innerHTML='<span class="chip c-info">POST /cell</span>';
|
||
const btn=$('#send');btn.disabled=true;
|
||
const t0=performance.now();
|
||
$('#respHead').innerHTML='<span class="chip c-info">发送中…</span>';
|
||
$('#respBody').innerHTML='<span class="empty">等待响应…</span>';
|
||
ABORT=new AbortController();
|
||
try{
|
||
const res=await fetch('/cell',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body),signal:ABORT.signal});
|
||
const ms=Math.round(performance.now()-t0);
|
||
if(CUR.m.isStream){
|
||
btn.textContent='停止';btn.classList.add('stop');btn.disabled=false;
|
||
await readStream(res,ms);
|
||
}else{
|
||
const text=await res.text();
|
||
$('#respHead').innerHTML=statusChip(res.status,ms,res.headers.get('content-type'));
|
||
setRespBody(text);
|
||
}
|
||
}catch(e){
|
||
if(e.name==='AbortError')$('#respHead').innerHTML='<span class="chip c-err">已取消</span>';
|
||
else $('#respHead').innerHTML='<span class="chip c-err">请求失败</span>';
|
||
$('#respBody').textContent=String(e.message||e);
|
||
}finally{
|
||
ABORT=null;btn.textContent='发送请求';btn.classList.remove('stop');btn.disabled=false;
|
||
}
|
||
}
|
||
function setRespBody(text){
|
||
const pre=$('#respBody');
|
||
if(!text){pre.textContent='(空响应体)';return}
|
||
const p=pretty(text);
|
||
if(p!==text){pre.innerHTML=hlJSON(p)}
|
||
else{
|
||
try{JSON.parse(text);pre.innerHTML=hlJSON(p)}
|
||
catch{pre.textContent=text}
|
||
}
|
||
}
|
||
async function readStream(res,ms){
|
||
$('#respHead').innerHTML=statusChip(res.status,ms,'application/x-ndjson');
|
||
const pre=$('#respBody');pre.textContent='';
|
||
if(!res.ok||!(res.headers.get('content-type')||'').includes('ndjson')){
|
||
setRespBody(await res.text());return;
|
||
}
|
||
const reader=res.body.getReader(),dec=new TextDecoder();
|
||
let buf='',lines=0;
|
||
while(true){
|
||
if(!ABORT)break;
|
||
const {done,value}=await reader.read();
|
||
if(done)break;
|
||
buf+=dec.decode(value,{stream:true});
|
||
let i;
|
||
while((i=buf.indexOf('\n'))>=0){
|
||
const line=buf.slice(0,i).trim();buf=buf.slice(i+1);
|
||
if(!line)continue;
|
||
lines++;
|
||
const row=document.createElement('div');
|
||
row.innerHTML=hlLine(line);
|
||
pre.appendChild(row);
|
||
while(pre.childNodes.length>800)pre.removeChild(pre.firstChild);
|
||
pre.scrollTop=pre.scrollHeight;
|
||
$('#respHead').lastChild&&$('#respHead').lastChild.remove();
|
||
$('#respHead').insertAdjacentHTML('beforeend',`<span class="chip c-ok">流式 · ${lines} 行</span>`);
|
||
}
|
||
}
|
||
$('#respHead').insertAdjacentHTML('beforeend','<span class="chip c-info">流结束</span>');
|
||
}
|
||
function statusChip(status,ms,ct){
|
||
const ok=status>=200&&status<300;
|
||
return `<span class="chip ${ok?'c-ok':'c-err'}">HTTP ${status}</span><span class="chip c-info">${ms} ms</span>`+(ct?`<span class="chip c-info">${esc(ct.split(';')[0])}</span>`:'');
|
||
}
|
||
function pretty(text){
|
||
try{return JSON.stringify(JSON.parse(text),null,2)}catch{return text}
|
||
}
|
||
/* JSON 语法高亮:键/字符串/数字/布尔/null 分色 */
|
||
function hlJSON(text){
|
||
return text.replace(/"(\\u[0-9a-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|-?\d+(\.\d+)?([eE][+-]?\d+)?|\b(true|false|null)\b/g,
|
||
(m,str,colon)=>{
|
||
if(str!=null)return `<span class="tk-${colon?'key':'str'}">${esc(m)}</span>`;
|
||
if(colon)return `<span class="tk-key">${esc(m.slice(0,m.length-colon.length))}</span>${esc(colon)}`;
|
||
return `<span class="tk-${/^(true|false|null)$/.test(m)?'lit':'num'}">${esc(m)}</span>`;
|
||
});
|
||
}
|
||
function hlLine(line){
|
||
const p=pretty(line);
|
||
try{JSON.parse(line);return hlJSON(p)}catch{return esc(line)}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|