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