-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdoc-gen.ts
141 lines (131 loc) · 3.28 KB
/
doc-gen.ts
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
/**
* @file doc-gen.ts
* @copyright 2020-2024 Brandon Kalinowski (@brandonkal)
* @description
* Parse File-level JSDoc from a folder of TS/JS files for document generation
* @license MIT
*/
import * as fs from "jsr:@std/[email protected]";
const jsDocStartRe = /^\/\*\*/;
const jsDocMidRe = /^\s+\*\s/;
const jsDocEndRe = /\*\//;
const kindsRe = /\* @(file|author|copyright|description|license|version)(.*)/;
interface ParsedTopDoc {
file?: string;
author?: string;
copyright?: string;
description?: string;
license?: string;
version?: string;
}
function log(o: any) {
console.log(JSON.stringify(o, undefined, 2));
}
/**
* Parse File-level JSDoc from a folder of TS/JS files for document generation
* @throws
*/
export default async function genDoc(files: string[] = []) {
const collected = files;
if (!files.length) {
for await (
const f of fs.walk(Deno.cwd(), {
maxDepth: 1,
exts: ["ts", "js", "tsx"],
})
) {
if (f.isFile) collected.push(f.path);
}
}
const filenames = collected.filter(
(f) => !f.match(/(\.d\.ts$)|(test\.(ts|tsx|js|jsx))/),
);
const fileContents = await Promise.all(
filenames.map((fn) => {
return Deno.readTextFile(fn);
}),
);
const metadata = fileContents.map(parse);
filenames.forEach((name, i) => {
if (!isValid(metadata[i])) {
throw new Error(
`Invalid top JSDoc header for file: ${name}\n` +
`Required properties are file,copyright,description,license. author is optional.`,
);
}
});
return metadata.sort((a, b) => {
const f = [a.file, b.file].sort();
if (a.file === b.file) return 0;
if (f[0] === a.file) return -1;
return 1;
});
}
function isValid(obj: any): obj is ParsedTopDoc {
if (!obj) return false;
if (obj.file && obj.description && obj.copyright) return true;
return false;
}
/** parses text contents line-by-line for the top JSDoc comment */
function parse(contents: string): ParsedTopDoc {
const lines = contents.split("\n");
let isOpen = false;
const obj: ParsedTopDoc = {};
const buffer: [keyof ParsedTopDoc, string][] = [];
let lastKind: keyof ParsedTopDoc;
for (const line of lines) {
if (isOpen || jsDocStartRe.exec(line)) {
isOpen = true;
let m;
if ((m = kindsRe.exec(line))) {
buffer.push([m[1] as any, m[2]]);
lastKind = m[1] as keyof ParsedTopDoc;
} else if ((m = jsDocMidRe.exec(line))) {
const v = line.replace(jsDocMidRe, "").trim();
buffer.push([lastKind!, v]);
}
if (line.match(jsDocEndRe)) {
break;
}
}
}
if (!buffer.length) {
return {};
}
buffer.forEach(([kind, txt]) => {
if (!obj[kind]) {
obj[kind] = txt.trim();
} else {
obj[kind] = obj[kind] + "\n" + txt.trim();
}
});
return obj;
}
/** toMarkdown generates a markdown list with file and description */
function toMarkdown(list: ParsedTopDoc[]): string {
let out: string[] = [];
out = list.map((doc) => {
return `### ${doc.file}\n\n${doc.description}\n`;
});
return out.join("\n");
}
if (import.meta.main) {
try {
let files = Deno.args;
let shouldMarkdown = false;
if (files.includes("-m")) {
files = files.filter((file) => file !== "-m");
shouldMarkdown = true;
}
const docs = await genDoc(files);
if (shouldMarkdown) {
const md = toMarkdown(docs);
console.log(md);
} else {
log(docs);
}
} catch (e) {
console.error(e.message);
Deno.exit(1);
}
}