universal-ast-mapper 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BLUEPRINT.md +230 -0
- package/README.md +465 -0
- package/dist/analysis.js +134 -0
- package/dist/callgraph.js +238 -0
- package/dist/cli.js +617 -0
- package/dist/config.js +53 -0
- package/dist/extractors/common.js +54 -0
- package/dist/extractors/go.js +212 -0
- package/dist/extractors/python.js +142 -0
- package/dist/extractors/typescript.js +320 -0
- package/dist/graph-analysis.js +243 -0
- package/dist/graph.js +118 -0
- package/dist/html.js +325 -0
- package/dist/index.js +762 -0
- package/dist/parser.js +84 -0
- package/dist/registry.js +40 -0
- package/dist/resolver.js +131 -0
- package/dist/search.js +68 -0
- package/dist/skeleton.js +106 -0
- package/dist/types.js +5 -0
- package/package.json +44 -0
package/dist/html.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
const KIND_COLORS = {
|
|
2
|
+
class: "#7c3aed",
|
|
3
|
+
interface: "#0ea5e9",
|
|
4
|
+
struct: "#0d9488",
|
|
5
|
+
function: "#2563eb",
|
|
6
|
+
method: "#4f46e5",
|
|
7
|
+
type: "#db2777",
|
|
8
|
+
enum: "#ea580c",
|
|
9
|
+
const: "#65a30d",
|
|
10
|
+
var: "#ca8a04",
|
|
11
|
+
field: "#64748b",
|
|
12
|
+
};
|
|
13
|
+
function esc(s) {
|
|
14
|
+
return s
|
|
15
|
+
.replace(/&/g, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">")
|
|
18
|
+
.replace(/"/g, """);
|
|
19
|
+
}
|
|
20
|
+
function badge(kind) {
|
|
21
|
+
const color = KIND_COLORS[kind] ?? "#64748b";
|
|
22
|
+
return `<span class="badge" style="background:${color}1a;color:${color};border:1px solid ${color}55;">${kind}</span>`;
|
|
23
|
+
}
|
|
24
|
+
function renderSymbol(sym) {
|
|
25
|
+
const vis = sym.visibility === "private"
|
|
26
|
+
? `<span class="vis priv" title="private">private</span>`
|
|
27
|
+
: "";
|
|
28
|
+
const exported = sym.exported ? `<span class="vis exp" title="exported">export</span>` : "";
|
|
29
|
+
const sig = sym.signature
|
|
30
|
+
? `<code class="sig">${esc(sym.signature)}</code>`
|
|
31
|
+
: "";
|
|
32
|
+
const lines = `<span class="lines">L${sym.range.startLine}–${sym.range.endLine}</span>`;
|
|
33
|
+
const doc = sym.doc ? `<div class="doc">${esc(sym.doc)}</div>` : "";
|
|
34
|
+
const head = `${badge(sym.kind)}<span class="name">${esc(sym.name)}</span>${exported}${vis}${sig}${lines}`;
|
|
35
|
+
if (sym.children.length > 0) {
|
|
36
|
+
const kids = sym.children.map(renderSymbol).join("");
|
|
37
|
+
return `<details open class="node"><summary>${head}</summary>${doc}<div class="children">${kids}</div></details>`;
|
|
38
|
+
}
|
|
39
|
+
return `<div class="node leaf">${head}${doc}</div>`;
|
|
40
|
+
}
|
|
41
|
+
function collectSymbolNames(symbols) {
|
|
42
|
+
const names = [];
|
|
43
|
+
for (const sym of symbols) {
|
|
44
|
+
names.push(sym.name);
|
|
45
|
+
if (sym.children.length > 0)
|
|
46
|
+
names.push(...collectSymbolNames(sym.children));
|
|
47
|
+
}
|
|
48
|
+
return names;
|
|
49
|
+
}
|
|
50
|
+
function renderFileSection(skel, index) {
|
|
51
|
+
const body = skel.symbols.length > 0
|
|
52
|
+
? skel.symbols.map(renderSymbol).join("")
|
|
53
|
+
: `<p class="empty">No top-level symbols found.</p>`;
|
|
54
|
+
return `<details id="file-${index}" class="file-section" open>
|
|
55
|
+
<summary class="file-summary">
|
|
56
|
+
<span class="fs-path">${esc(skel.file)}</span>
|
|
57
|
+
<span class="fs-meta">${esc(skel.language)} · ${skel.symbolCount} symbols · <time>${esc(skel.generatedAt)}</time></span>
|
|
58
|
+
</summary>
|
|
59
|
+
<div class="fs-body"><div class="tree">${body}</div></div>
|
|
60
|
+
</details>`;
|
|
61
|
+
}
|
|
62
|
+
export function renderCombinedHtml(skeletons) {
|
|
63
|
+
const sections = skeletons.map((s, i) => renderFileSection(s, i)).join("\n");
|
|
64
|
+
const totalSymbols = skeletons.reduce((n, s) => n + s.symbolCount, 0);
|
|
65
|
+
const generatedAt = new Date().toISOString();
|
|
66
|
+
// Compact per-file data for client-side search and tree rendering.
|
|
67
|
+
const fileData = JSON.stringify(skeletons.map((s, i) => ({
|
|
68
|
+
id: i,
|
|
69
|
+
file: s.file,
|
|
70
|
+
lang: s.language,
|
|
71
|
+
n: s.symbolCount,
|
|
72
|
+
syms: collectSymbolNames(s.symbols).join(" "),
|
|
73
|
+
})));
|
|
74
|
+
return `<!doctype html>
|
|
75
|
+
<html lang="en">
|
|
76
|
+
<head>
|
|
77
|
+
<meta charset="utf-8">
|
|
78
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
79
|
+
<title>Codebase Skeleton (${skeletons.length} files)</title>
|
|
80
|
+
<style>
|
|
81
|
+
:root{color-scheme:light dark;--bg:#f8fafc;--bg2:#fff;--fg:#0f172a;--fg2:#475569;--bdr:#e2e8f0;--hover:#f1f5f9;--sb-bg:#fff;--sb-w:260px;--accent:#6366f1;}
|
|
82
|
+
@media(prefers-color-scheme:dark){:root{--bg:#0b1120;--bg2:#111827;--fg:#e2e8f0;--fg2:#94a3b8;--bdr:#1f2937;--hover:#111827;--sb-bg:#0f172a;}}
|
|
83
|
+
*{box-sizing:border-box;margin:0;padding:0;}
|
|
84
|
+
body{font:13px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:var(--bg);color:var(--fg);display:flex;flex-direction:column;height:100vh;overflow:hidden;}
|
|
85
|
+
/* topbar */
|
|
86
|
+
.topbar{display:flex;align-items:center;gap:12px;padding:8px 16px;background:var(--bg2);border-bottom:1px solid var(--bdr);flex-shrink:0;flex-wrap:wrap;}
|
|
87
|
+
.topbar-title{font-weight:700;font-size:14px;}
|
|
88
|
+
.topbar-meta{font-size:12px;color:var(--fg2);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
|
89
|
+
.topbar-actions{display:flex;gap:6px;flex-shrink:0;}
|
|
90
|
+
button{font:inherit;cursor:pointer;border:1px solid var(--bdr);background:transparent;color:inherit;border-radius:8px;padding:3px 10px;font-size:12px;}
|
|
91
|
+
button:hover{background:var(--hover);}
|
|
92
|
+
/* layout */
|
|
93
|
+
.layout{display:flex;flex:1;min-height:0;}
|
|
94
|
+
/* sidebar */
|
|
95
|
+
.sidebar{width:var(--sb-w);flex-shrink:0;background:var(--sb-bg);border-right:1px solid var(--bdr);display:flex;flex-direction:column;overflow:hidden;}
|
|
96
|
+
.search-wrap{padding:8px;border-bottom:1px solid var(--bdr);}
|
|
97
|
+
#search{width:100%;font:inherit;font-size:12px;padding:5px 8px;border:1px solid var(--bdr);border-radius:8px;background:var(--bg);color:var(--fg);outline:none;}
|
|
98
|
+
#search:focus{border-color:var(--accent);}
|
|
99
|
+
.nav-tree{flex:1;overflow-y:auto;padding:6px 4px;}
|
|
100
|
+
/* nav tree nodes */
|
|
101
|
+
.dir-node{margin:1px 0;}
|
|
102
|
+
.dir-node>summary{list-style:none;cursor:pointer;padding:3px 6px;border-radius:6px;font-size:12px;font-weight:600;color:var(--fg2);display:flex;align-items:center;gap:4px;user-select:none;}
|
|
103
|
+
.dir-node>summary::-webkit-details-marker{display:none;}
|
|
104
|
+
.dir-node>summary::before{content:"\\25B8";font-size:10px;opacity:.5;transition:transform .12s;flex-shrink:0;}
|
|
105
|
+
.dir-node[open]>summary::before{transform:rotate(90deg);}
|
|
106
|
+
.dir-node>summary:hover{background:var(--hover);}
|
|
107
|
+
.dir-children{padding-left:12px;}
|
|
108
|
+
a.file-link{display:flex;align-items:center;justify-content:space-between;padding:3px 8px;border-radius:6px;text-decoration:none;color:var(--fg);font-size:12px;cursor:pointer;gap:4px;}
|
|
109
|
+
a.file-link:hover{background:var(--hover);}
|
|
110
|
+
a.file-link.active{background:color-mix(in srgb,var(--accent) 12%,transparent);color:var(--accent);}
|
|
111
|
+
.fname{font-family:ui-monospace,monospace;font-size:11px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
112
|
+
.fmeta{font-size:10px;color:var(--fg2);flex-shrink:0;}
|
|
113
|
+
/* main panel */
|
|
114
|
+
.main-panel{flex:1;overflow-y:auto;padding:16px;}
|
|
115
|
+
/* file sections */
|
|
116
|
+
details.file-section{border:1px solid var(--bdr);border-radius:12px;margin-bottom:14px;background:var(--bg2);}
|
|
117
|
+
summary.file-summary{list-style:none;cursor:pointer;padding:10px 14px;display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;border-radius:12px;}
|
|
118
|
+
summary.file-summary::-webkit-details-marker{display:none;}
|
|
119
|
+
summary.file-summary::before{content:"\\25B8";opacity:.4;transition:transform .15s;flex-shrink:0;}
|
|
120
|
+
details.file-section[open]>summary.file-summary::before{transform:rotate(90deg);}
|
|
121
|
+
summary.file-summary:hover{background:var(--hover);}
|
|
122
|
+
.fs-path{font-family:ui-monospace,monospace;font-weight:700;font-size:13px;}
|
|
123
|
+
.fs-meta{font-size:11px;color:var(--fg2);margin-left:auto;}
|
|
124
|
+
.fs-body{padding:8px 14px 14px;}
|
|
125
|
+
/* symbol tree (reused styles) */
|
|
126
|
+
.tree{display:flex;flex-direction:column;gap:4px;}
|
|
127
|
+
details.node{border:1px solid var(--bdr);border-radius:10px;padding:2px 4px;}
|
|
128
|
+
summary{list-style:none;cursor:pointer;padding:6px 8px;border-radius:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
|
|
129
|
+
summary::-webkit-details-marker{display:none;}
|
|
130
|
+
summary::before{content:"\\25B8";opacity:.5;transition:transform .15s;}
|
|
131
|
+
details[open]>summary::before{transform:rotate(90deg);}
|
|
132
|
+
.leaf{padding:6px 8px 6px 24px;border-radius:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
|
|
133
|
+
summary:hover,.leaf:hover{background:var(--hover);}
|
|
134
|
+
.children{margin:2px 0 6px 18px;padding-left:10px;border-left:2px solid #e2e8f033;display:flex;flex-direction:column;gap:4px;}
|
|
135
|
+
.badge{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;text-transform:uppercase;letter-spacing:.03em;}
|
|
136
|
+
.name{font-family:ui-monospace,monospace;font-weight:600;}
|
|
137
|
+
.vis{font-size:10px;padding:1px 6px;border-radius:6px;}
|
|
138
|
+
.vis.priv{background:#ef44441a;color:#ef4444;}
|
|
139
|
+
.vis.exp{background:#22c55e1a;color:#16a34a;}
|
|
140
|
+
.sig{font-family:ui-monospace,monospace;font-size:12px;background:var(--hover);padding:1px 6px;border-radius:6px;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
|
141
|
+
.lines{font-size:11px;opacity:.55;margin-left:auto;font-family:ui-monospace,monospace;}
|
|
142
|
+
.doc{font-size:12px;opacity:.8;margin:2px 0 6px 28px;white-space:pre-wrap;font-style:italic;}
|
|
143
|
+
.empty{opacity:.6;}
|
|
144
|
+
.no-match{padding:40px;text-align:center;color:var(--fg2);font-size:13px;}
|
|
145
|
+
</style>
|
|
146
|
+
</head>
|
|
147
|
+
<body>
|
|
148
|
+
<header class="topbar">
|
|
149
|
+
<span class="topbar-title">Codebase Skeleton</span>
|
|
150
|
+
<span class="topbar-meta">${skeletons.length} files · ${totalSymbols} symbols · ${esc(generatedAt)}</span>
|
|
151
|
+
<div class="topbar-actions">
|
|
152
|
+
<button id="btn-expand">Expand all</button>
|
|
153
|
+
<button id="btn-collapse">Collapse all</button>
|
|
154
|
+
</div>
|
|
155
|
+
</header>
|
|
156
|
+
<div class="layout">
|
|
157
|
+
<nav class="sidebar">
|
|
158
|
+
<div class="search-wrap">
|
|
159
|
+
<input id="search" type="search" placeholder="Search files or symbols…" autocomplete="off" spellcheck="false">
|
|
160
|
+
</div>
|
|
161
|
+
<div id="nav-tree" class="nav-tree"></div>
|
|
162
|
+
</nav>
|
|
163
|
+
<main id="main" class="main-panel">
|
|
164
|
+
${sections}
|
|
165
|
+
<div id="no-match" class="no-match" style="display:none">No matching files or symbols.</div>
|
|
166
|
+
</main>
|
|
167
|
+
</div>
|
|
168
|
+
<script>
|
|
169
|
+
(function(){
|
|
170
|
+
const FILES=${fileData};
|
|
171
|
+
|
|
172
|
+
// ── folder tree ──────────────────────────────────────────────────────────────
|
|
173
|
+
function buildTreeData(files){
|
|
174
|
+
const root={dirs:{},files:[]};
|
|
175
|
+
for(const f of files){
|
|
176
|
+
const parts=f.file.split('/');
|
|
177
|
+
let node=root;
|
|
178
|
+
for(let i=0;i<parts.length-1;i++){
|
|
179
|
+
if(!node.dirs[parts[i]])node.dirs[parts[i]]={dirs:{},files:[]};
|
|
180
|
+
node=node.dirs[parts[i]];
|
|
181
|
+
}
|
|
182
|
+
node.files.push(f);
|
|
183
|
+
}
|
|
184
|
+
return root;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function e(s){return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
188
|
+
|
|
189
|
+
const linkMap=new Map(); // id -> <a>
|
|
190
|
+
function renderTreeNode(node,container){
|
|
191
|
+
const dirs=Object.entries(node.dirs).sort(([a],[b])=>a.localeCompare(b));
|
|
192
|
+
for(const[name,child]of dirs){
|
|
193
|
+
const det=document.createElement('details');
|
|
194
|
+
det.className='dir-node';
|
|
195
|
+
det.open=true;
|
|
196
|
+
const sum=document.createElement('summary');
|
|
197
|
+
sum.textContent=name;
|
|
198
|
+
det.appendChild(sum);
|
|
199
|
+
const inner=document.createElement('div');
|
|
200
|
+
inner.className='dir-children';
|
|
201
|
+
renderTreeNode(child,inner);
|
|
202
|
+
det.appendChild(inner);
|
|
203
|
+
container.appendChild(det);
|
|
204
|
+
}
|
|
205
|
+
for(const f of node.files){
|
|
206
|
+
const a=document.createElement('a');
|
|
207
|
+
a.href='#file-'+f.id;
|
|
208
|
+
a.className='file-link';
|
|
209
|
+
a.dataset.id=String(f.id);
|
|
210
|
+
const fname=f.file.split('/').pop()||f.file;
|
|
211
|
+
a.innerHTML='<span class="fname">'+e(fname)+'</span><span class="fmeta">'+f.n+'</span>';
|
|
212
|
+
container.appendChild(a);
|
|
213
|
+
linkMap.set(f.id,a);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const navTree=document.getElementById('nav-tree');
|
|
218
|
+
renderTreeNode(buildTreeData(FILES),navTree);
|
|
219
|
+
|
|
220
|
+
// ── search ───────────────────────────────────────────────────────────────────
|
|
221
|
+
const searchEl=document.getElementById('search');
|
|
222
|
+
const mainEl=document.getElementById('main');
|
|
223
|
+
const noMatch=document.getElementById('no-match');
|
|
224
|
+
const sections=Array.from(document.querySelectorAll('details.file-section'));
|
|
225
|
+
|
|
226
|
+
searchEl.addEventListener('input',applyFilter);
|
|
227
|
+
function applyFilter(){
|
|
228
|
+
const q=searchEl.value.trim().toLowerCase();
|
|
229
|
+
let visCount=0;
|
|
230
|
+
FILES.forEach(f=>{
|
|
231
|
+
const sec=document.getElementById('file-'+f.id);
|
|
232
|
+
if(!sec)return;
|
|
233
|
+
const match=!q||f.file.toLowerCase().includes(q)||f.syms.toLowerCase().includes(q);
|
|
234
|
+
sec.style.display=match?'':'none';
|
|
235
|
+
const link=linkMap.get(f.id);
|
|
236
|
+
if(link)link.style.display=match?'':'none';
|
|
237
|
+
if(match)visCount++;
|
|
238
|
+
});
|
|
239
|
+
noMatch.style.display=visCount===0&&q?'':'none';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── expand / collapse ────────────────────────────────────────────────────────
|
|
243
|
+
document.getElementById('btn-expand').addEventListener('click',()=>{
|
|
244
|
+
document.querySelectorAll('details').forEach(d=>d.open=true);
|
|
245
|
+
});
|
|
246
|
+
document.getElementById('btn-collapse').addEventListener('click',()=>{
|
|
247
|
+
document.querySelectorAll('details').forEach(d=>d.open=false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ── active sidebar link on scroll ────────────────────────────────────────────
|
|
251
|
+
const io=new IntersectionObserver(entries=>{
|
|
252
|
+
for(const en of entries){
|
|
253
|
+
if(en.isIntersecting){
|
|
254
|
+
const idx=parseInt(en.target.id.replace('file-',''),10);
|
|
255
|
+
linkMap.forEach((a,id)=>a.classList.toggle('active',id===idx));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},{root:mainEl,threshold:0.15});
|
|
259
|
+
sections.forEach(s=>io.observe(s));
|
|
260
|
+
})();
|
|
261
|
+
</script>
|
|
262
|
+
</body>
|
|
263
|
+
</html>`;
|
|
264
|
+
}
|
|
265
|
+
export function renderHtml(skel) {
|
|
266
|
+
const body = skel.symbols.length > 0
|
|
267
|
+
? skel.symbols.map(renderSymbol).join("")
|
|
268
|
+
: `<p class="empty">No top-level symbols found.</p>`;
|
|
269
|
+
return `<!doctype html>
|
|
270
|
+
<html lang="en">
|
|
271
|
+
<head>
|
|
272
|
+
<meta charset="utf-8">
|
|
273
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
274
|
+
<title>Skeleton — ${esc(skel.file)}</title>
|
|
275
|
+
<style>
|
|
276
|
+
:root { color-scheme: light dark; }
|
|
277
|
+
* { box-sizing: border-box; }
|
|
278
|
+
body { font: 14px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
|
279
|
+
margin: 0; padding: 24px; background: #f8fafc; color: #0f172a; }
|
|
280
|
+
@media (prefers-color-scheme: dark) { body { background:#0b1120; color:#e2e8f0; } header.meta{background:#111827;border-color:#1f2937;} .node{border-color:#1f2937;} summary:hover,.leaf:hover{background:#111827;} .sig{background:#111827;color:#93c5fd;} .doc{color:#94a3b8;} }
|
|
281
|
+
header.meta { background:#fff; border:1px solid #e2e8f0; border-radius:12px; padding:16px 20px; margin-bottom:20px; }
|
|
282
|
+
header.meta h1 { font-size:16px; margin:0 0 6px; font-family:ui-monospace,monospace; }
|
|
283
|
+
header.meta .sub { font-size:12px; opacity:.7; }
|
|
284
|
+
.toolbar { margin:14px 0; display:flex; gap:8px; }
|
|
285
|
+
button { font:inherit; cursor:pointer; border:1px solid #cbd5e1; background:transparent; color:inherit;
|
|
286
|
+
border-radius:8px; padding:4px 10px; }
|
|
287
|
+
.tree { display:flex; flex-direction:column; gap:4px; }
|
|
288
|
+
details.node { border:1px solid #e2e8f0; border-radius:10px; padding:2px 4px; }
|
|
289
|
+
summary { list-style:none; cursor:pointer; padding:6px 8px; border-radius:8px; display:flex; align-items:center;
|
|
290
|
+
gap:8px; flex-wrap:wrap; }
|
|
291
|
+
summary::-webkit-details-marker { display:none; }
|
|
292
|
+
summary::before { content:"\\25B8"; opacity:.5; transition:transform .15s; }
|
|
293
|
+
details[open] > summary::before { transform:rotate(90deg); }
|
|
294
|
+
.leaf { padding:6px 8px 6px 24px; border-radius:8px; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
295
|
+
summary:hover,.leaf:hover { background:#f1f5f9; }
|
|
296
|
+
.children { margin:2px 0 6px 18px; padding-left:10px; border-left:2px solid #e2e8f033; display:flex;
|
|
297
|
+
flex-direction:column; gap:4px; }
|
|
298
|
+
.badge { font-size:11px; font-weight:600; padding:1px 7px; border-radius:999px; text-transform:uppercase;
|
|
299
|
+
letter-spacing:.03em; }
|
|
300
|
+
.name { font-family:ui-monospace,monospace; font-weight:600; }
|
|
301
|
+
.vis { font-size:10px; padding:1px 6px; border-radius:6px; }
|
|
302
|
+
.vis.priv { background:#ef44441a; color:#ef4444; }
|
|
303
|
+
.vis.exp { background:#22c55e1a; color:#16a34a; }
|
|
304
|
+
.sig { font-family:ui-monospace,monospace; font-size:12px; background:#f1f5f9; padding:1px 6px; border-radius:6px;
|
|
305
|
+
max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
306
|
+
.lines { font-size:11px; opacity:.55; margin-left:auto; font-family:ui-monospace,monospace; }
|
|
307
|
+
.doc { font-size:12px; opacity:.8; margin:2px 0 6px 28px; white-space:pre-wrap; font-style:italic; }
|
|
308
|
+
.empty { opacity:.6; }
|
|
309
|
+
</style>
|
|
310
|
+
</head>
|
|
311
|
+
<body>
|
|
312
|
+
<header class="meta">
|
|
313
|
+
<h1>${esc(skel.file)}</h1>
|
|
314
|
+
<div class="sub">${esc(skel.language)} · ${skel.symbolCount} symbols · parser: ${esc(skel.parser.grammar)} · ${esc(skel.generatedAt)}</div>
|
|
315
|
+
</header>
|
|
316
|
+
<div class="toolbar">
|
|
317
|
+
<button onclick="document.querySelectorAll('details').forEach(d=>d.open=true)">Expand all</button>
|
|
318
|
+
<button onclick="document.querySelectorAll('details').forEach(d=>d.open=false)">Collapse all</button>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="tree">
|
|
321
|
+
${body}
|
|
322
|
+
</div>
|
|
323
|
+
</body>
|
|
324
|
+
</html>`;
|
|
325
|
+
}
|