File : DocBuilder.js

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