1 | /* |
2 | Copyright - 2021 - wwwouaiebe - Contact: https://www.ouaie.be/ |
3 | |
4 | This program is free software; |
5 | you can redistribute it and/or modify it under the terms of the |
6 | GNU General Public License as published by the Free Software Foundation; |
7 | either version 3 of the License, or any later version. |
8 | |
9 | This program is distributed in the hope that it will be useful, |
10 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | GNU General Public License for more details. |
13 | |
14 | You should have received a copy of the GNU General Public License |
15 | along with this program; if not, write to the Free Software |
16 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
17 | */ |
18 | /* |
19 | Changes: |
20 | - v1.0.0: |
21 | - created |
22 | - v1.1.0: |
23 | - Issue ♯1 : Improve colorization of sources files... |
24 | Doc reviewed 20211111 |
25 | */ |
26 | /* ------------------------------------------------------------------------------------------------------------------------- */ |
27 | |
28 | import process from 'process'; |
29 | import fs from 'fs'; |
30 | import babelParser from '@babel/parser'; |
31 | import traverse from '@babel/traverse'; |
32 | |
33 | import ClassDocBuilder from './ClassDocBuilder.js'; |
34 | import VariableDocBuilder from './VariableDocBuilder.js'; |
35 | import theConfig from './Config.js'; |
36 | import SourceHtmlBuilder from './SourceHtmlBuilder.js'; |
37 | import ClassHtmlBuilder from './ClassHtmlBuilder.js'; |
38 | import VariablesHtmlBuilder from './VariablesHtmlBuilder.js'; |
39 | import theLinkBuilder from './LinkBuilder.js'; |
40 | import DocsValidator from './DocsValidator.js'; |
41 | import IndexHtmlBuilder from './IndexHtmlBuilder.js'; |
42 | |
43 | /** |
44 | A simple container to store the line, column and html tag value to insert in the source file |
45 | for comments, string literals, template literals and regexp literals |
46 | */ |
47 | |
48 | class TagData { |
49 | |
50 | /** |
51 | The line number in the source file where the tag must be inserted |
52 | @type {Number} |
53 | */ |
54 | |
55 | #line; |
56 | |
57 | /** |
58 | The column number in the source file where the tag must be inserted |
59 | @type {?Number} |
60 | */ |
61 | |
62 | #column; |
63 | |
64 | /** |
65 | The tag to insert. |
66 | @type {String} |
67 | */ |
68 | |
69 | #tag; |
70 | |
71 | /** |
72 | The constructor |
73 | @param {Number} line The line number in the source file where the tag must be inserted |
74 | @param {?Number} column The column number in the source file where the tag must be inserted |
75 | @param {String} tag The tag to insert. |
76 | */ |
77 | |
78 | constructor ( line, column, tag ) { |
79 | Object.freeze ( this ); |
80 | this.#line = line; |
81 | this.#column = column; |
82 | this.#tag = tag; |
83 | } |
84 | |
85 | /** |
86 | The line number in the source file where the tag must be inserted |
87 | @type {Number} |
88 | */ |
89 | |
90 | get line ( ) { return this.#line; } |
91 | |
92 | /** |
93 | The column number in the source file where the tag must be inserted |
94 | If the tag must be inserted at the end of the line the value is null |
95 | @type {?Number} |
96 | */ |
97 | |
98 | get column ( ) { return this.#column; } |
99 | |
100 | /** |
101 | The tag to insert. |
102 | To avoid a replacement of the < ,> and " chars when creating the source html file |
103 | the < char is replaced with <, the > char with > and the " char with " and then |
104 | replaced with the correct value inthe source html file. |
105 | @type {String} |
106 | */ |
107 | |
108 | get tag ( ) { return this.#tag; } |
109 | } |
110 | |
111 | /* ------------------------------------------------------------------------------------------------------------------------- */ |
112 | /** |
113 | Build the complete documentation: generate AST from the source files, then extracting doc objects from AST |
114 | and finally buid HTML pages from the doc objects. |
115 | */ |
116 | /* ------------------------------------------------------------------------------------------------------------------------- */ |
117 | |
118 | class DocBuilder { |
119 | |
120 | /** |
121 | A SourceHtmlBuilder object used by the class |
122 | @type {SourceHtmlBuilder} |
123 | */ |
124 | |
125 | #sourceHtmlBuilder; |
126 | |
127 | /** |
128 | A VariableDocBuilder object used by the class |
129 | @type {VariableDocBuilder} |
130 | */ |
131 | |
132 | #variableDocBuilder; |
133 | |
134 | /** |
135 | A ClassDocBuilder object used by the class |
136 | @type {ClassDocBuilder} |
137 | */ |
138 | |
139 | #classDocBuilder; |
140 | |
141 | /** |
142 | The generated ClassDoc objects |
143 | @type {Array.<ClassDoc>} |
144 | */ |
145 | |
146 | #classesDocs = []; |
147 | |
148 | /** |
149 | The generated VariableDoc objects |
150 | @type {Array.<VariableDoc>} |
151 | */ |
152 | |
153 | #variablesDocs = []; |
154 | |
155 | /** |
156 | The options for the babel/parser |
157 | @type {Object} |
158 | */ |
159 | |
160 | #parserOptions = { |
161 | allowAwaitOutsideFunction : true, |
162 | allowImportExportEverywhere : true, |
163 | allowReturnOutsideFunction : true, |
164 | allowSuperOutsideMethod : true, |
165 | plugins : [ |
166 | [ 'decorators', { |
167 | decoratorsBeforeExport : true |
168 | } ], |
169 | 'doExpressions', |
170 | 'exportDefaultFrom', |
171 | 'functionBind', |
172 | 'importMeta', |
173 | [ 'pipelineOperator', { |
174 | proposal : 'minimal' |
175 | } ], |
176 | 'throwExpressions' |
177 | ], |
178 | ranges : true, |
179 | sourceType : 'module' |
180 | }; |
181 | |
182 | /** |
183 | A Map with all the TagData of all the source files ordered by SourceFileName |
184 | @type {Map.<Array.<TagData>>} |
185 | */ |
186 | |
187 | #tagsDataMap; |
188 | |
189 | /** |
190 | The constructor |
191 | */ |
192 | |
193 | constructor ( ) { |
194 | Object.freeze ( this ); |
195 | this.#classDocBuilder = new ClassDocBuilder ( ); |
196 | this.#variableDocBuilder = new VariableDocBuilder ( ); |
197 | } |
198 | |
199 | /** |
200 | Build all the docs for a file |
201 | @param {Object} ast The root |
202 | [ast node](https://github.com/babel/babel/blob/main/packages/babel-parser/ast/spec.md) given by the babel/parser |
203 | @param {String} sourceFileName The source file name, including relative path since theConfig.srcDir |
204 | */ |
205 | |
206 | #buildDocs ( ast, sourceFileName ) { |
207 | ast.program.body.forEach ( |
208 | astNode => { |
209 | switch ( astNode.type ) { |
210 | case 'ClassDeclaration' : |
211 | { |
212 | const classDoc = this.#classDocBuilder.build ( astNode, sourceFileName ); |
213 | if ( ! classDoc?.commentsDoc?.ignore ) { |
214 | this.#classesDocs.push ( classDoc ); |
215 | } |
216 | } |
217 | break; |
218 | case 'VariableDeclaration' : |
219 | { |
220 | const variableDoc = this.#variableDocBuilder.build ( astNode, sourceFileName ); |
221 | if ( ! variableDoc?.commentsDoc?.ignore ) { |
222 | this.#variablesDocs.push ( variableDoc ); |
223 | } |
224 | } |
225 | break; |
226 | default : |
227 | break; |
228 | } |
229 | } |
230 | ); |
231 | } |
232 | |
233 | /** |
234 | Traverse the ast created by the Babel parser and extract the TagData objects for the |
235 | template literals, string literals, regexp literals and comments |
236 | @param {Object} ast The root |
237 | [ast node](https://github.com/babel/babel/blob/main/packages/babel-parser/ast/spec.md) given by the babel/parser |
238 | @param {String} sourceFileName The source file name, including relative path since theConfig.srcDir |
239 | */ |
240 | |
241 | #traverseAst ( ast, sourceFileName ) { |
242 | |
243 | const tagsData = []; |
244 | |
245 | /* |
246 | A helper function for extracting the comments TagData |
247 | Yes, I know... I don't like functions, but traverse will not know this |
248 | */ |
249 | |
250 | function addCommentTags ( comment ) { |
251 | let currentLine = comment.loc.start.line; |
252 | tagsData.push ( new TagData ( currentLine, comment.loc.start.column, '' ) ); |
253 | while ( currentLine !== comment.loc.end.line ) { |
254 | tagsData.push ( new TagData ( currentLine, null, '' ) ); |
255 | currentLine ++; |
256 | tagsData.push ( new TagData ( currentLine, 0, '' ) ); |
257 | } |
258 | tagsData.push ( new TagData ( comment.loc.end.line, comment.loc.end.column, '' ) ); |
259 | } |
260 | |
261 | traverse.default ( |
262 | ast, |
263 | { |
264 | enter ( path ) { |
265 | switch ( path.node.type ) { |
266 | case 'TemplateLiteral' : |
267 | case 'RegExpLiteral' : |
268 | case 'StringLiteral' : |
269 | tagsData.push ( |
270 | new TagData ( |
271 | path.node.loc.start.line, |
272 | path.node.loc.start.column, |
273 | ' + '"' + path.node.type + '">' |
274 | ) |
275 | ); |
276 | tagsData.push ( new TagData ( path.node.loc.end.line, path.node.loc.end.column, '' ) ); |
277 | break; |
278 | default : |
279 | break; |
280 | } |
281 | |
282 | if ( path.node.leadingComments ) { |
283 | path.node.leadingComments.forEach ( addCommentTags ); |
284 | } |
285 | if ( path.node.trailingComments ) { |
286 | path.node.trailingComments.forEach ( addCommentTags ); |
287 | } |
288 | } |
289 | } |
290 | ); |
291 | this.#tagsDataMap.set ( sourceFileName, tagsData ); |
292 | } |
293 | |
294 | /** |
295 | Build all the docs for the app and then build all the html files |
296 | @param {Array.<String>} sourceFilesList The source files names, including relative path since theConfig.srcDir |
297 | */ |
298 | |
299 | buildFiles ( sourceFilesList ) { |
300 | let ast = null; |
301 | this.#tagsDataMap = new Map ( ); |
302 | sourceFilesList.forEach ( |
303 | sourceFileName => { |
304 | try { |
305 | |
306 | // Reading the source |
307 | const fileContent = fs.readFileSync ( theConfig.srcDir + sourceFileName, 'utf8' ); |
308 | ast = babelParser.parse ( fileContent, this.#parserOptions ); |
309 | } |
310 | catch ( err ) { |
311 | console.error ( |
312 | `\n\t\x1b[31mError\x1b[0m parsing file \x1b[31m${sourceFileName}\x1b[0m` + |
313 | ` at line ${err.loc.line} column ${err.loc.column} : \n\t\t${err.message}\n` |
314 | ); |
315 | |
316 | process.exit ( 1 ); |
317 | } |
318 | |
319 | if ( ! theConfig.noSourcesColor ) { |
320 | this.#traverseAst ( ast, sourceFileName ); |
321 | } |
322 | |
323 | // buiding docs for the source |
324 | this.#buildDocs ( ast, sourceFileName ); |
325 | |
326 | // buiding the links for the source |
327 | const htmlFileName = sourceFileName.replace ( '.js', 'js.html' ); |
328 | theLinkBuilder.setSourceLink ( sourceFileName, htmlFileName ); |
329 | } |
330 | ); |
331 | |
332 | // Saving links for classes and variables |
333 | this.#classesDocs.forEach ( classDoc => theLinkBuilder.setClassLink ( classDoc ) ); |
334 | this.#variablesDocs.forEach ( variableDoc => theLinkBuilder.setVariableLink ( variableDoc ) ); |
335 | |
336 | // Validation |
337 | if ( theConfig.validate ) { |
338 | const docsValidator = new DocsValidator ( ); |
339 | docsValidator.validate ( this.#classesDocs, this.#variablesDocs ); |
340 | } |
341 | |
342 | // Building classes html files |
343 | const classHtmlBuilder = new ClassHtmlBuilder ( ); |
344 | this.#classesDocs.forEach ( classDoc => classHtmlBuilder.build ( classDoc ) ); |
345 | |
346 | console. error ( `\n\tCreated ${classHtmlBuilder.classesCounter} class files` ); |
347 | |
348 | // Building sources html files |
349 | const sourceHtmlBuilder = new SourceHtmlBuilder ( ); |
350 | sourceFilesList.forEach ( |
351 | sourceFileName => { |
352 | const fileContent = fs.readFileSync ( theConfig.srcDir + sourceFileName, 'utf8' ); |
353 | sourceHtmlBuilder.build ( fileContent, sourceFileName, this.#tagsDataMap.get ( sourceFileName ) ); |
354 | } |
355 | ); |
356 | |
357 | console.error ( `\n\tCreated ${sourceHtmlBuilder.sourcesCounter} source files` ); |
358 | |
359 | // Building the variables html file |
360 | new VariablesHtmlBuilder ( ).build ( this.#variablesDocs ); |
361 | |
362 | // Building the index.html file |
363 | new IndexHtmlBuilder ( ).build ( ); |
364 | } |
365 | } |
366 | |
367 | export default DocBuilder; |
368 | |
369 | /* --- End of file --------------------------------------------------------------------------------------------------------- */ |
370 |