]> dreyeck.freedombox.rocks Git - idiomatic.git/blob - index.js
Update README.md
[idiomatic.git] / index.js
1 const express = require('express')
2 const acorn = require('acorn')
3 const fs = require('fs')
4 const visitor = require('./visitor.js')
5
6
7 // P A R S E
8
9 const dir = '../wiki-client/lib'
10 const mods = []
11 fs.readdir(dir, async (err, files) => {
12 mods.push(... await Promise.all(files.map(load)))
13 })
14
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})
20 })
21 })
22 }
23
24
25 // P A G E S
26
27 const style = (title,here='') => `
28 <style>
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;}
34 </style>
35 <section>${title} <span style="background-color:#ddd;">&nbsp;${escape(here)}&nbsp;</span></section>`
36 const app = express()
37
38 app.get('/index', async (req,res) => {
39 const reductions = counter()
40 visitor.walk(mods,branch =>
41 reductions.count(branch.type))
42 const result = `
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)}`)
47 .join("\n")}</table>`
48 res.send(style('index')+result);
49
50 function link(key) {
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>`
54 return key
55 }
56 })
57
58 app.get('/terminal', (req,res) => {
59 const {type,field} = req.query
60 const terms = counter()
61 visitor.walk(mods,branch => {
62 if(branch.type==type)
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>`)
69 .join("\n")}</table>`
70 res.send(result)
71 })
72
73 app.get('/usage', (req,res) => {
74 const {type,field,key,width,depth} = req.query
75 const list = []
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}">
80 ${stack.at(-1)}</a>
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}`)
85 .join('&')
86 const p = id => `<a href=/usage?${q(id,+1)} style="background-color:#ddd;">&nbsp;&plus;&nbsp;</a>`
87 const m = id => `<a href=/usage?${q(id,-1)} style="background-color:#ddd;">&nbsp;&minus;&nbsp;</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>`)
94 })
95
96 app.get('/nesting', (req,res) => {
97 const {file,type,key,start,end} = req.query
98 const result = []
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) => `
102 <tr>
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()
105 const hit = stack[1]
106 result.push(`
107 <p><table>${path.join("")}</table><br>
108 <p><pre>${escape(JSON.stringify(hit,omit,2))}</pre>`)
109 }
110 })
111 res.send(style('nesting',key)+`${result.join("<hr>")}`)
112 })
113
114 app.get('/similar', (req,res) => {
115 const {file,type,key,start,end,nest} = req.query
116 let nested
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)
123 const result = []
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)+
128 `<p>${want}<hr>` +
129 result.join("\n")
130 )
131 })
132
133
134 // H E L P E R S
135
136 function counter() {
137 const counts = new Map()
138 return {
139 count(item) {
140 if(counts.has(item))
141 counts.set(item, counts.get(item)+1)
142 else
143 counts.set(item,1)
144 return item
145 },
146 size() {
147 return counts.size
148 },
149 total() {
150 return [...counts]
151 .reduce((sum,each) => sum + each[1], 0)
152 },
153 tally() {
154 return [...counts]
155 .sort((a,b) => a[1]==b[1] ? (a[0]>b[0] ? 1 : -1) : b[1]-a[1])
156 },
157 }
158 }
159
160 function escape(text) {
161 try {
162 return text
163 .replace(/&/g, '&amp;')
164 .replace(/</g, '&lt;')
165 .replace(/>/g, '&gt;')
166 .replace(/\*(.+?)\*/g, '<i>$1</i>')
167 } catch (e) {
168 return text
169 }
170 }
171
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
175 if (obj) {
176 if(deep) {
177 const fields = Object.entries(obj)
178 .filter(([k,v]) => !['start','end','raw','computed','optional','kind'].includes(k))
179 .map(([k,v]) =>
180 k=='type' ? abv(v) :
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))
184 .join(" ")
185 return key ? `<span ${hilite} title=${key}>(${(fields)})</span>` : `(${(fields)})`
186 } else return elipsis(obj)
187 } else return `<span title=${obj}>.</span>`
188 }
189
190 function abv(type) {
191 return `<span title=${type}>${type.replaceAll(/[a-z]/g,'')}</span>`
192 }
193
194 function omit(k,v) {
195 return k=='type'?v:k=='start'||k=='end'?undefined:v
196 }
197
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>)`
202 }
203
204 function vis(row) {
205 return row.split(/\n/)[3].trim()
206 .replaceAll(/<.*?>/g,'')
207 .replaceAll(/\.\.+/g,'..')
208 }
209
210 function query(obj,adj={}) {
211 return Object.entries(obj)
212 .map(([k,v]) => k in adj ? `${k}=${adj[k](v)}` : `${k}=${v}`)
213 .join('&')
214 }
215
216 app.listen(1954)