File : ClassHtmlBuilder.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 ♯3 : String.substr ( ) is deprecated... Replace...
24
Doc reviewed 20211111
25
*/
26
/* ------------------------------------------------------------------------------------------------------------------------- */
27
28
import FileWriter from './FileWriter.js';
29
import theLinkBuilder from './LinkBuilder.js';
30
import NavHtmlBuilder from './NavHtmlBuilder.js';
31
import { marked } from 'marked';
32
33
marked.use ( {
34
    mangle : false,
35
    headerIds : false
36
} );
37
38
/* ------------------------------------------------------------------------------------------------------------------------- */
39
/**
40
Build the html page for a class
41
*/
42
/* ------------------------------------------------------------------------------------------------------------------------- */
43
44
class ClassHtmlBuilder {
45
46
    /**
47
    A classes files counter
48
    @type {Number}
49
    */
50
51
    #classesCounter;
52
53
    /**
54
    The name of the class currently treated
55
    @type {String}
56
    */
57
58
    #className;
59
60
    /**
61
    The html with the class documentation
62
    @type {String}
63
    */
64
65
    #html;
66
67
    /**
68
    The path between the html file and theConfig.destDir ( something like '../../../', depending of the folders tree )
69
    @type {String}
70
    */
71
72
    #rootPath;
73
74
    /**
75
    A array with the methods or properties that must be currently added to the html
76
    @type {Array.<MethodOrPropertyDoc>}
77
    */
78
79
    #methodsOrPropertiesDoc;
80
81
    /**
82
    The constructor
83
    */
84
85
    constructor ( ) {
86
        Object.freeze ( this );
87
        this.#classesCounter = 0;
88
    }
89
90
    /**
91
    Build an html table with the parameters from a MethodOrPropertyDoc object and
92
    include this table in the #html property. Parameters names are extracted from the code and parameters types
93
    and descriptions are extracted from the comments
94
    @param {MethodOrPropertyDoc} methodOrPropertyDoc The object with the extracted documentation for the method
95
    */
96
97
    #buildParamsTable ( methodOrPropertyDoc ) {
98
        this.#html += '<h4>Parameters</h4>';
99
        this.#html += '<table class="params"><tr><th>Name</th> <th>Type</th> <th>Description</th></tr>';
100
        methodOrPropertyDoc.params.forEach (
101
            param => {
102
                const paramDoc = methodOrPropertyDoc?.commentsDoc?.params?.find ( first => first.name === param );
103
                const paramType = paramDoc?.type ? theLinkBuilder.getTypeLinks ( paramDoc.type, this.#rootPath ) : '???';
104
                const paramDesc =
105
                    paramDoc?.desc
106
                        ?
107
                        theLinkBuilder.getDescLink ( paramDoc.desc, this.#rootPath )
108
                        :
109
                        ' ...No description provided. Coming soon?';
110
                this.#html += `<tr><td>${param}</td> <td>${paramType}</td> <td>${paramDesc}</td></tr>`;
111
            }
112
        );
113
        this.#html += '</table>';
114
    }
115
116
    /**
117
    Build a string  with the parameters from a MethodOrPropertyDoc object and
118
    enclose the string in ( ). Parameters names are extracted from the code.
119
    @param {MethodOrPropertyDoc} methodOrPropertyDoc The object with the extracted documentation for the method
120
    @return {String} A string with the parameters
121
    */
122
123
    #buildParamsHeader ( methodOrPropertyDoc ) {
124
        let params = '';
125
        if ( methodOrPropertyDoc.params ) {
126
            methodOrPropertyDoc.params.forEach (
127
                param => params += param + ', '
128
            );
129
            // eslint-disable-next-line no-magic-numbers
130
            params = params.substring ( 0, params.length - 2 );
131
        }
132
133
        return ` ( ${params} )`;
134
    }
135
136
    /**
137
    Build the html for a method or property header
138
    @param {MethodOrPropertyDoc} methodOrPropertyDoc The object with the extracted documentation for the method or property
139
    */
140
141
    #buildMethodOrPropertyHeader ( methodOrPropertyDoc ) {
142
143
        // header css class
144
        const cssClassName = methodOrPropertyDoc.private ? 'private' : 'public';
145
146
        // readonly flag. Only getter have the readonly flag, so we search a setter with the same name
147
        // to find the readonly flag
148
        const readOnlyPrefix =
149
            'get' ===
150
                methodOrPropertyDoc.kind
151
                &&
152
                ! this.#methodsOrPropertiesDoc.find (
153
                    method => 'set' === method.kind && methodOrPropertyDoc.name === method.name
154
                )
155
                ?
156
                '<span>readonly </span>'
157
                :
158
                '';
159
160
        // get set flags
161
        const getSetPrefix =
162
            'set' === methodOrPropertyDoc.kind || 'get' === methodOrPropertyDoc.kind
163
                ?
164
                '<span>' + methodOrPropertyDoc.kind + '</span> '
165
                :
166
                '';
167
168
        // async flag
169
        const asyncPrefix = methodOrPropertyDoc.async ? '<span>async</span> ' : '';
170
171
        // static flag
172
        const staticPrefix = methodOrPropertyDoc.static ? '<span>static</span> ' : '';
173
174
        // # flag
175
        const namePrefix = methodOrPropertyDoc.private ? '#' : '';
176
177
        // method name
178
        const methodName =
179
            'constructor' === methodOrPropertyDoc.name ? `<span>new</span> ${this.#className}` : methodOrPropertyDoc.name;
180
181
        // params
182
        const paramsPostfix =
183
            'method' === methodOrPropertyDoc.isA && 0 === getSetPrefix.length
184
                ?
185
                this.#buildParamsHeader ( methodOrPropertyDoc )
186
                :
187
                '';
188
189
        // type flag
190
        const typePostfix =
191
            methodOrPropertyDoc?.commentsDoc?.type
192
                ?
193
                ' <span> : ' +
194
                theLinkBuilder.getTypeLinks ( methodOrPropertyDoc.commentsDoc.type, this.#rootPath ) +
195
                '</span>'
196
                :
197
                '';
198
199
        // building html
200
        this.#html += `<div class="${cssClassName}">`;
201
        this.#html +=
202
            `<h3>${readOnlyPrefix}${asyncPrefix}${staticPrefix}${getSetPrefix}${namePrefix}` +
203
            `${methodName}${paramsPostfix}${typePostfix}</h3>`;
204
    }
205
206
    /**
207
    Build the html for a method or property
208
    @param {MethodOrPropertyDoc} methodOrPropertyDoc The object with the extracted documentation for the method or property
209
    */
210
211
    // eslint-disable-next-line complexity
212
    #buildMethodOrProperty ( methodOrPropertyDoc ) {
213
214
        // Header
215
        this.#buildMethodOrPropertyHeader ( methodOrPropertyDoc );
216
217
        // description
218
        let desc =
219
            methodOrPropertyDoc?.commentsDoc?.desc
220
                ?
221
                theLinkBuilder.getDescLink ( methodOrPropertyDoc.commentsDoc.desc, this.#rootPath )
222
                :
223
                ' ...No description provided. Coming soon?';
224
225
        if ( 'set' === methodOrPropertyDoc.kind ) {
226
            const getter = this.#methodsOrPropertiesDoc.find (
227
                method => 'get' === method.kind && methodOrPropertyDoc.name === method.name
228
            );
229
            desc = getter && getter?.commentsDoc?.desc ? '' : desc;
230
        }
231
232
        this.#html += `<div>${desc}</div>`;
233
234
        // sample
235
        if ( methodOrPropertyDoc?.commentsDoc?.sample ) {
236
            this.#html += `<div>${marked.parse ( methodOrPropertyDoc?.commentsDoc?.sample )}</div>`;
237
        }
238
239
        // source
240
        const sourceLink = theLinkBuilder.getSourceLink ( methodOrPropertyDoc );
241
        this.#html +=
242
            `<div>Source : <a href="${sourceLink}"> file ${methodOrPropertyDoc.file}` +
243
            ` at line ${methodOrPropertyDoc.line}</a></div>`;
244
245
        // params
246
        if (
247
            methodOrPropertyDoc.params
248
            &&
249
            0 !== methodOrPropertyDoc.params.length
250
            &&
251
            'set' !== methodOrPropertyDoc.kind
252
            &&
253
            'get' !== methodOrPropertyDoc.kind
254
        ) {
255
            this.#buildParamsTable ( methodOrPropertyDoc );
256
        }
257
258
        // returns
259
        if (
260
            methodOrPropertyDoc?.commentsDoc?.returns
261
            &&
262
            'method' === methodOrPropertyDoc.isA
263
            &&
264
            'set' !== methodOrPropertyDoc.kind
265
            &&
266
            'get' !== methodOrPropertyDoc.kind
267
            &&
268
            'constructor' !== methodOrPropertyDoc.kind
269
        ) {
270
            const returnType =
271
                methodOrPropertyDoc.commentsDoc?.returns?.type
272
                    ?
273
                    theLinkBuilder.getTypeLinks ( methodOrPropertyDoc.commentsDoc.returns.type, this.#rootPath )
274
                    :
275
                    '???';
276
277
            const returnDesc = methodOrPropertyDoc?.commentsDoc?.returns?.desc ?? ' ...No description provided. Coming soon?';
278
279
            this.#html += `<h4>Returns</h4><div>${returnDesc}</div>` +
280
                `<div>Type : ${returnType}</div>`;
281
        }
282
283
        this.#html += '</div>';
284
    }
285
286
    /**
287
    Build the html for methods and properties
288
    @param {String} heading The header to add in the #html property before the methods and properties
289
    */
290
291
    #buildMethodsAndProperties ( heading ) {
292
293
        // no methods or properties... returning
294
        if ( 0 === this.#methodsOrPropertiesDoc.length ) {
295
            return;
296
        }
297
298
        // heading
299
        this.#html += `${heading}`;
300
301
        // loop on methods and properties
302
        this.#methodsOrPropertiesDoc.forEach (
303
            methodOrPropertyDoc => this.#buildMethodOrProperty ( methodOrPropertyDoc )
304
        );
305
    }
306
307
    /**
308
    A classes files counter
309
    @type {Number}
310
    */
311
312
    get classesCounter ( ) { return this.#classesCounter; }
313
314
    /**
315
    Build the html for a complete class
316
    @param {ClassDoc} classDoc The object with the class documentation
317
    */
318
319
    build ( classDoc ) {
320
321
        this.#classesCounter ++;
322
323
        // saving rootPath...
324
        this.#rootPath = classDoc.rootPath;
325
326
        // ... and className
327
        this.#className = classDoc.name;
328
329
        // start html build
330
        this.#html =
331
            '<!DOCTYPE html><html><head><meta charset="UTF-8">' +
332
            `<link type="text/css" rel="stylesheet" href="${classDoc.rootPath}ESSimpleDoc.css"></head>` +
333
            '<body class=\'have-private-button\'>';
334
335
        // <nav> tag build
336
        const navHtmlBuilder = new NavHtmlBuilder ( );
337
        this.#html += navHtmlBuilder.build ( this.#rootPath );
338
339
        // Class header
340
        const superClass =
341
            classDoc?.superClass
342
                ?
343
                '<span> extends ' + theLinkBuilder.getClassLink ( classDoc.superClass, this.#rootPath ) + '</span>'
344
                :
345
                '';
346
347
        this.#html += `<h1><span>Class</span> ${classDoc.name} ${superClass}</h1>`;
348
349
        // class description
350
        const desc =
351
            classDoc?.commentsDoc?.desc
352
                ?
353
                theLinkBuilder.getDescLink ( classDoc.commentsDoc.desc, this.#rootPath )
354
                :
355
                ' ...No description provided. Coming soon?';
356
357
        this.#html += `<div>${desc}</div>`;
358
359
        // sample
360
        if ( classDoc?.commentsDoc?.sample ) {
361
            this.#html += `<div>${marked.parse ( classDoc?.commentsDoc?.sample )}</div>`;
362
        }
363
364
        // class source
365
        const sourceLink = theLinkBuilder.getSourceLink ( classDoc );
366
        this.#html += `<div>Source : <a href="${sourceLink}"> file ${classDoc.file} at line ${classDoc.line}</a></div>`;
367
368
        // Filtering methodsOrPropertiesDoc to add the constructor
369
        this.#methodsOrPropertiesDoc = classDoc.methodsAndProperties.filter (
370
            methodOrProperty => (
371
                'method' === methodOrProperty.isA &&
372
                    'constructor' === methodOrProperty.kind
373
            )
374
        );
375
        this.#buildMethodsAndProperties ( '<h2 class="public">Constructor</h2>' );
376
377
        // Filtering methodsOrPropertiesDoc to add public properties
378
        this.#methodsOrPropertiesDoc = classDoc.methodsAndProperties.filter (
379
            methodOrProperty => (
380
                'property' === methodOrProperty.isA &&
381
                    ! methodOrProperty.private
382
            )
383
        ).sort ( ( first, second ) => first.name.localeCompare ( second.name ) );
384
        this.#buildMethodsAndProperties ( '<h2 class="public">Public properties</h2>' );
385
386
        // Filtering methodsOrPropertiesDoc to add public getter and setter
387
        this.#methodsOrPropertiesDoc = classDoc.methodsAndProperties.filter (
388
            methodOrProperty => (
389
                'method' === methodOrProperty.isA &&
390
                    ! methodOrProperty.private &&
391
                    ( 'set' === methodOrProperty.kind || 'get' === methodOrProperty.kind ) &&
392
                    'constructor' !== methodOrProperty.kind
393
            )
394
        ).sort ( ( first, second ) => ( first.name + first.kind ).localeCompare ( second.name + second.kind ) );
395
        this.#buildMethodsAndProperties ( '<h2 class="public">Public getters and setters</h2>' );
396
397
        // Filtering methodsOrPropertiesDoc to add public methods
398
        this.#methodsOrPropertiesDoc = classDoc.methodsAndProperties.filter (
399
            methodOrProperty => (
400
                'method' === methodOrProperty.isA &&
401
                    ! methodOrProperty.private &&
402
                    'set' !== methodOrProperty.kind &&
403
                    'get' !== methodOrProperty.kind    &&
404
                    'constructor' !== methodOrProperty.kind
405
            )
406
        ).sort ( ( first, second ) => first.name.localeCompare ( second.name ) );
407
        this.#buildMethodsAndProperties ( '<h2 class="public">Public methods</h2>' );
408
409
        // Filtering methodsOrPropertiesDoc to add private properties
410
        this.#methodsOrPropertiesDoc = classDoc.methodsAndProperties.filter (
411
            methodOrProperty => (
412
                'property' === methodOrProperty.isA &&
413
                    methodOrProperty.private
414
            )
415
        ).sort ( ( first, second ) => first.name.localeCompare ( second.name ) );
416
        this.#buildMethodsAndProperties ( '<h2 class="private">Private properties</h2>'    );
417
418
        // Filtering methodsOrPropertiesDoc to add private getter and setter
419
        this.#methodsOrPropertiesDoc = classDoc.methodsAndProperties.filter (
420
            methodOrProperty => (
421
                'method' === methodOrProperty.isA &&
422
                    methodOrProperty.private &&
423
                    ( 'set' === methodOrProperty.kind || 'get' === methodOrProperty.kind ) &&
424
                    'constructor' !== methodOrProperty.kind
425
            )
426
        ).sort ( ( first, second ) => ( first.name + first.kind ).localeCompare ( second.name + second.kind ) );
427
        this.#buildMethodsAndProperties ( '<h2 class="private">Private getters and setters</h2>' );
428
429
        // Filtering methodsOrPropertiesDoc to add private methods
430
        this.#methodsOrPropertiesDoc = classDoc.methodsAndProperties.filter (
431
            methodOrProperty => (
432
                'method' === methodOrProperty.isA &&
433
                    methodOrProperty.private &&
434
                    'set' !== methodOrProperty.kind &&
435
                    'get' !== methodOrProperty.kind    &&
436
                    'constructor' !== methodOrProperty.kind
437
            )
438
        ).sort ( ( first, second ) => first.name.localeCompare ( second.name ) );
439
        this.#buildMethodsAndProperties ( '<h2 class="private">Private methods</h2>' );
440
441
        // footer
442
        this.#html += navHtmlBuilder.footer;
443
        this.#html +=
444
            '<script>' +
445
            'document.getElementById(\'show-private-nav\')' +
446
            '.addEventListener(\'click\',()=>document.body.classList.toggle(\'show-private\'))' +
447
            '</script>';
448
        this.#html += '</body></html>';
449
450
        // writting html to file
451
        const dirs = classDoc.file.split ( '/' );
452
        dirs.pop ( );
453
        new FileWriter ( ).write ( dirs, classDoc.name + '.html', this.#html );
454
    }
455
}
456
457
export default ClassHtmlBuilder;
458
459
/* --- End of file --------------------------------------------------------------------------------------------------------- */
460