1|
2|
3|
4|
5|
6|
7|
8|
9|
10|
11|
12|
13|
14|
15|
16|
17|
18|
19|
20|
21|
22|
23|
24|
25|
26|
27|
28|
29|
30|
31|
32|
33|
34|
35|
36|
37|
38|
39|
40|
41|
42|
43|
44|
45|
46|
47|
48|
49|
50|
51|
52|
53|
54|
55|
56|
57|
58|
59|
60|
61|
62|
63|
64|
65|
66|
67|
68|
69|
70|
71|
72|
73|
74|
75|
76|
77|
78|
79|
80|
81|
82|
83|
84|
85|
86|
87|
88|
89|
90|
91|
92|
93|
94|
95|
96|
97|
98|
99|
100|
101|
102|
103|
104|
105|
106|
107|
108|
109|
110|
111|
112|
113|
114|
115|
116|
117|
118|
119|
120|
121|
122|
123|
124|
125|
126|
127|
128|
129|
130|
131|
132|
133|
134|
135|
136|
137|
138|
139|
140|
141|
142|
143|
144|
145|
146|
147|
148|
149|
150|
151|
152|
153|
154|
155|
156|
157|
158|
159|
160|
161|
162|
163|
164|
165|
166|
167|
168|
169|
170|
171|
172|
173|
174|
175|
176|
177|
178|
179|
180|
181|
182|
183|
184|
185|
186|
187|
188|
189|
190|
191|
192|
193|
194|
195|
196|
197|
198|
199|
200|
201|
202|
203|
204|
205|
206|
207|
208|
209|
210|
211|
212|
213|
214|
215|
216|
217|
218|
219|
220|
221|
222|
223|
224|
225|
226|
227|
228|
229|
230|
231|
232|
233|
234|
235|
236|
237|
238|
239|
240|
241|
import { rm, readFile } from "fs/promises";
import { join } from "path";
import hljs from "highlight.js";
import { putInSrc, Addition, myWriteFile } from "./util";
import { distFolder } from "./paths";
import { lsifParser, Lsif, findRecursiveEdge, Element } from "./lsif";
import { buildTree, TreeNode } from "./tree";
import { templatesBuilder } from "./templates/index";
import { start } from "./debug/bench";
import MarkdownIt from "markdown-it";
import { contains, Document, item, ItemEdge, ReferenceResult, Range, Id, HoverResult } from "lsif-protocol";
import { MarkupContent, MarkedString } from "vscode-languageserver-protocol";
const markdown = MarkdownIt({
highlight: (str, lang) => {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value;
} catch (_) { }
}
return '';
}
});
const treeToHtml = (tree: TreeNode[], uri: string) => {
const path = uri.split('/');
const f = (t: TreeNode, p: string[], k: string): string => {
if (t.kind === 'file') {
const c = p[0] === t.name ? ` class="current-file"` : ``;
return `<li${c}><a href="${k}/${t.name}.html">${t.name}</a></li>`;
}
return `<li>${t.name}<ul>${g(t.children, p[0] === t.name ? p.slice(1) : [], `${k}/${t.name}`)}</ul></li>`;
};
const g = (x: TreeNode[], p: string[], k: string) => x.map((y) => f(y, p, k)).join('');
return `<ul>${g(tree, path, '.')}</ul>`;
};
type ItemData = string;
const isContains = (x: Element): x is contains => {
return x.label == 'contains';
};
const getItemData = (item: Range, lsif: Lsif): ItemData => {
const { uri } = lsif.item.get(
lsif.inV.get(item.id).filter(isContains)[0].outV
) as Document;
const path = `${lsif.uriPath(uri)}.html`;
const url = `${path}#${item.start.line + 1}`;
return url;
};
type Hovers = {
[s: string]: {
content?: Id;
definition?: Id;
references?: Id;
};
};
type ObjString = {
[s: string]: Id;
};
type References = {
[s: string]: {
definitions: ItemData[];
references: ItemData[];
};
};
type MainOptions = {
input: string;
output: string;
dist?: string;
uriMap?: string;
};
const markedStringToHtml = (x: MarkedString) => {
if (typeof x === 'string') {
return x;
}
return "\n\n```" + x.language + "\n" + x.value.trim() + "\n```\n\n";
};
const hoverToHtml = (hover: MarkupContent | MarkedString | MarkedString[]) => {
if (typeof hover === 'string' || 'language' in hover) {
return markdown.render(markedStringToHtml(hover));
}
if (hover instanceof Array) {
return markdown.render(hover.map(markedStringToHtml).join('\n\n---\n\n'));
}
if (hover.kind === 'markdown') {
return markdown.render(hover.value);
}
if (hover.kind === 'plaintext') {
return `<p>${hover.value}</p>`;
}
throw new Error("bad hover content");
};
export const main = async ({ input, output, dist, uriMap }: MainOptions) => {
let bench = start('Reading files and cleaning');
const lsif = await lsifParser(input, uriMap);
const { item, uriPath, documents, srcMap, outV } = lsif;
const templates = await templatesBuilder();
const fileTree = buildTree(documents.map((x) => uriPath(x.uri)));
await rm(output, { recursive: true, force: true });
await myWriteFile(
join(output, 'index.html'),
templates.welcome({
tree: treeToHtml(fileTree, 'never$#.gav'),
}),
);
await myWriteFile(join(output, '_dist', 'main.css'), (await readFile(join(distFolder, 'main.css'))).toString());
await myWriteFile(join(output, '_dist', 'main.js'), (await readFile(join(distFolder, 'main.js'))).toString());
await myWriteFile(join(output, '.nojekyll'), '');
bench.end();
bench = start('Parsing lsif dump');
const refs: Set<Id> = new Set();
const lsifParsed = await Promise.all(documents
.map(async (doc) => {
const additions: Addition[] = [];
const hovers: Hovers = {};
const data: ObjString = {};
const setDataLazy = (key: Id, lazy: () => string) => {
if (key in data) return;
data[key] = lazy();
};
outV.get(doc.id).forEach((edge) => {
const path = uriPath(doc.uri);
if (edge.label != 'contains') {
return;
}
for (const id of edge.inVs) {
const v = item.get(id) as Range;
if (v.start.character === v.end.character && v.start.line === v.end.line) {
continue;
}
let hoverContent: Id | undefined = undefined;
let definitionPlace: Id | undefined = undefined;
let ref = undefined;
const defVertex = findRecursiveEdge(lsif, v.id, 'textDocument/definition');
if (defVertex) {
const defItemEdge = outV.get(defVertex.id)[0] as item;
const defItem = item.get(defItemEdge.inVs[0]) as Range;
if (defItem.id != v.id) {
setDataLazy(defItem.id, () => getItemData(defItem, lsif));
definitionPlace = defItem.id;
}
}
const hoverVertex = findRecursiveEdge(lsif, v.id, 'textDocument/hover') as HoverResult | undefined;
if (hoverVertex) {
setDataLazy(hoverVertex.id, () => hoverToHtml(hoverVertex.result.contents));
hoverContent = hoverVertex.id;
}
const refVertex = findRecursiveEdge(lsif, v.id, 'textDocument/references');
if (refVertex) {
refs.add(refVertex.id);
ref = refVertex.id;
}
if (hoverContent || definitionPlace || ref) {
hovers[`x${v.id}`] = {
content: hoverContent,
definition: definitionPlace,
references: ref,
};
additions.push({
position: v.start,
text: `<span id="lsif${v.id}">`
});
additions.push({
position: v.end,
text: '</span>',
});
}
}
});
const relPath = uriPath(doc.uri);
const destPath = join(output, relPath);
await myWriteFile(destPath + ".hover.json", JSON.stringify({ hovers, data }));
return { destPath, doc, additions };
}));
bench.end();
bench = start('Create find references files');
const refFolder = join(output, '_data', 'refs');
for (const id of refs) {
const edge = outV.get(id) as ItemEdge<ReferenceResult, Range>[];
const defEdge = edge.filter((x) => x.property === 'definitions');
const refEdge = edge.filter((x) => x.property === 'references');
await myWriteFile(`${refFolder}/${id}.json`, JSON.stringify({
definitions: defEdge.flatMap((e) => {
return e.inVs.map((x: Id) => {
return getItemData(item.get(x) as Range, lsif);
});
}),
references: refEdge.flatMap((e) => {
return e.inVs.map((x: Id) => {
return getItemData(item.get(x) as Range, lsif);
});
}),
}));
}
bench.end();
bench = start('Syntax highlighting');
const highlighted = lsifParsed.map((x) => {
const srcRaw = srcMap.raw(x.doc.uri);
const highlighted = hljs.highlight(srcRaw, { language: x.doc.languageId }).value;
return { ...x, highlighted };
});
bench.end();
bench = start('Adding additions');
const added = highlighted.map((x) => {
const src = putInSrc(x.highlighted, x.additions);
return { ...x, src };
});
bench.end();
bench = start('Templating with EJS');
const generated = added.map(({ doc, destPath, src }) => {
const path = uriPath(doc.uri);
const pathSplitted = path.split('/');
const filename = pathSplitted.slice(-1)[0];
const depth = pathSplitted.length - 1;
const html = templates.source({
src, filename,
tree: treeToHtml(fileTree, path),
distPath: dist ? dist : `_dist/`,
basePath: `./${'../'.repeat(depth)}`,
});
return { html, destPath, src };
});
bench.end();
bench = start('Writing generated files');
await Promise.all(generated.map(async ({ destPath, html, src }) => {
await myWriteFile(destPath + ".html", html);
await myWriteFile(destPath + ".src.html", src);
}));
bench.end();
};