1 const express
= require('express')
2 const acorn
= require('acorn')
3 const fs
= require('fs')
4 const visitor
= require('./visitor.js')
9 const dir
= '../wiki-client/lib'
11 fs
.readdir(dir
, async (err
, files
) => {
12 mods
.push(... await Promise
.all(files
.map(load
)))
15 async
function load(file
) {
16 return new Promise(resolve
=> {
17 fs
.readFile(`${dir}/${file}`, "utf8", (err
,text
) => {
18 const tree
= acorn
.parse(text
, {ecmaVersion
: "latest"})
19 resolve({file
,text
,tree
})
27 const style
= (title
,here
='') => `
29 body {font-family:sans-serif;}
30 a {text-decoration:none;}
31 td:first-child {text-align:right;}
32 .hi {background-color:pink;}
33 section {letter-spacing:.2rem; font-size:1.2rem;}
35 <section>${title} <span style="background-color:#ddd;"> ${escape(here)} </span></section>`
38 app
.get('/index', async (req
,res
) => {
39 const reductions
= counter()
40 visitor
.walk(mods
,branch
=>
41 reductions
.count(branch
.type
))
43 <p>${reductions.size()} non-terminals
44 <br>${reductions.total()} reductions
45 <p><table>${reductions.tally()
46 .map(([k,v]) => `<tr
><td
>${v}
<td
>${link(k)}
`)
48 res
.send(style('index')+result
);
51 if(key
.match(/^Ident/)) return `<a href="/terminal?type=${key}&field=name">${key}</a>`
52 if(key
.match(/^(As|B|L|U).*Ex/)) return `<a href="/terminal?type=${key}&field=operator">${key}</a>`
53 if(key
.match(/^Lit/)) return `<a href="/terminal?type=${key}&field=value">${key}</a>`
58 app
.get('/terminal', (req
,res
) => {
59 const {type
,field
} = req
.query
60 const terms
= counter()
61 visitor
.walk(mods
,branch
=> {
63 terms
.count(branch
[field
])})
64 const result
= style('terminal',type
)+`
65 <p>${terms.size()} uniques
66 <br>${terms.total()} total
67 <p><table>${terms.tally()
68 .map(([k,v]) => `<tr
><td
>${v}
<td
><a href
="/usage?type=${type}&field=${field}&key=${encodeURIComponent(k)}&width=2&depth=3">${escape(k)}
</a
>`)
73 app
.get('/usage', (req
,res
) => {
74 const {type
,field
,key
,width
,depth
} = req
.query
76 const files
= counter()
77 visitor
.walk(mods
,(branch
,stack
) => {
78 if(branch
.type
==type
&& branch
[field
]==key
)list
.push(`
79 <tr><td><a href="/nesting/?file=${files.count(stack.at(-1))}&type=${type}&key=${key}&start=${branch.start}&end=${branch.end}">
81 <td>${sxpr(stack[width ?? 2], depth ?? 3)}`)})
82 list
.sort((a
,b
) => vis(a
)>vis(b
) ? 1 : -1)
83 const q
= (id
,delta
) => Object
.entries(req
.query
)
84 .map(([k
,v
]) => k
== id
? `${k}=${+v+delta}` : `${k}=${v}`)
86 const p
= id
=> `<a href=/usage?${q(id,+1)} style="background-color:#ddd;"> + </a>`
87 const m
= id
=> `<a href=/usage?${q(id,-1)} style="background-color:#ddd;"> − </a>`
88 const d
= id
=> `<span title=${req.query[id]}>${id} ${p(id)} ${m(id)}</span>`
89 res
.send(style('usage',key
)+`
90 <p><details><summary>${files.total()} uses in ${files.size()} files</summary>
91 <table>${files.tally().map(([k,v]) => `<tr><td>${v}<td>${k}`).join("\n")}</table></details>
92 <p
><section
>${d('width')} ${d('depth')}
</section
>
93 <p
><table
>${list.join("\n")}
</table
>`)
96 app.get('/nesting', (req,res) => {
97 const {file,type,key,start,end} = req.query
99 visitor.walk(mods,(branch,stack) => {
100 if(stack.at(-1)==file && branch.type==type && branch.start==start && branch.end==end) {
101 const path = stack.slice(0,-1).map((n,i) => `
103 <td
><a title
=${file} href
=/similar?${query(req.query)}&nest=${i}>${n.type}</a>:
104 <td
>${sxpr(n,3,null,stack[i-1])}
`).reverse()
107 <p
><table
>${path.join("")}
</table
><br
>
108 <p
><pre
>${escape(JSON.stringify(hit,omit,2))}
</pre
>`)
111 res.send(style('nesting',key)+`${result.join("<hr>")}
`)
114 app.get('/similar', (req,res) => {
115 const {file,type,key,start,end,nest} = req.query
117 visitor.walk(mods,(branch,stack) => {
118 if(stack.at(-1)==file && branch.type==type && branch.start==start && branch.end==end)
119 nested = stack[nest]})
120 const norm = node => vis(`\n\n\n${sxpr(node,3,null)}
`)
121 const source = (file,node) => mods.find(mod => mod.file == file).text.substring(+node.start,+node.end)
122 const want = norm(nested)
124 visitor.walk(mods,(branch,stack) => {
125 if(norm(branch) == want)
126 result.push(`<pre
>${escape(source(stack.at(-1),branch))}
</pre
><hr
>`)})
127 res.send(style('similar',key)+
137 const counts = new Map()
141 counts.set(item, counts.get(item)+1)
151 .reduce((sum,each) => sum + each[1], 0)
155 .sort((a,b) => a[1]==b[1] ? (a[0]>b[0] ? 1 : -1) : b[1]-a[1])
160 function escape(text) {
163 .replace(/&/g, '&')
164 .replace(/</g, '<')
165 .replace(/>/g, '>')
166 .replace(/\*(.+?)\*/g, '<i>$1</i>')
172 function sxpr(obj,deep,key,child) {
173 const hilite = obj===child ? 'class="hi"' : ''
174 const link = word => obj.type == 'Identifier' ? `<a href
=/usage?type=Identifier&field=name&key=${word}>${word}</a>` : word
177 const fields = Object.entries(obj)
178 .filter(([k,v]) => !['start','end','raw','computed','optional','kind'].includes(k))
181 (typeof v == 'string') ? link(escape(v)) :
182 Array.isArray(v) ? `[${v.map(o => sxpr(o,deep-1,k,child)).join(" ")}
]` :
183 sxpr(v, deep-1, k, child))
185 return key ? `<span ${hilite} title
=${key}
>(${(fields)}
)</span
>` : `(${(fields)}
)`
186 } else return elipsis(obj)
187 } else return `<span title
=${obj}
>.</span
>`
191 return `<span title
=${type}
>${type.replaceAll(/[a-z]/g,'')}
</span
>`
195 return k=='type'?v:k=='start'||k=='end'?undefined:v
198 function elipsis(obj) {
199 const bytes = (obj.end||0)-(obj.start||0)
200 const dots = '..' + '.'.repeat(Math.floor(Math.log2(bytes||1)))
201 return `(<span title
="${bytes} bytes">${dots}
</span
>)`
205 return row.split(/\n/)[3].trim()
206 .replaceAll(/<.*?>/g,'')
207 .replaceAll(/\.\.+/g,'..')
210 function query(obj,adj={}) {
211 return Object.entries(obj)
212 .map(([k,v]) => k in adj ? `${k}
=${adj[k](v)}
` : `${k}
=${v}
`)