| 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 TypeDescription from './TypeDescription.js'; |
| 29 | import CommentsDoc from './CommentsDoc.js'; |
| 30 | |
| 31 | /* ------------------------------------------------------------------------------------------------------------------------- */ |
| 32 | /** |
| 33 | Build a CommentsDoc object from the leading comments of a class, method, property or variable |
| 34 | */ |
| 35 | /* ------------------------------------------------------------------------------------------------------------------------- */ |
| 36 | |
| 37 | class CommentsDocBuilder { |
| 38 | |
| 39 | /** |
| 40 | The currently builded comments |
| 41 | @type {CommentsDoc} |
| 42 | */ |
| 43 | |
| 44 | #commentsDoc; |
| 45 | |
| 46 | /** |
| 47 | A RegExp to find the @desc, @classdesc, @sample,@type, @param, |
| 48 | @return, @returns, @ignore tags |
| 49 | @type {RegExp} |
| 50 | */ |
| 51 | |
| 52 | #tagRegExp; |
| 53 | |
| 54 | /** |
| 55 | A RegExp to find space or new line at the beginning of the string |
| 56 | @type {RegExp} |
| 57 | */ |
| 58 | |
| 59 | #beginSpaceNewlineRegExp; |
| 60 | |
| 61 | /** |
| 62 | A RegExp to find space or new line in the string |
| 63 | @type {RegExp} |
| 64 | */ |
| 65 | |
| 66 | #spaceNewlineRegExp; |
| 67 | |
| 68 | /** |
| 69 | A RegExp to find space or new line at the end of the string |
| 70 | @type {RegExp} |
| 71 | */ |
| 72 | |
| 73 | #endSpaceNewlineRegExp; |
| 74 | |
| 75 | /** |
| 76 | A RegExp to find multiple spaces |
| 77 | @type {RegExp} |
| 78 | */ |
| 79 | |
| 80 | #multipleSpacesRegExp; |
| 81 | |
| 82 | /** |
| 83 | A RegExp to find multiple spaces + newline +multiple spaces |
| 84 | @type {RegExp} |
| 85 | */ |
| 86 | |
| 87 | #spaceNewlineSpaceRegExp; |
| 88 | |
| 89 | /** |
| 90 | A RegExp to find a new line at the beginning of the string |
| 91 | @type {RegExp} |
| 92 | */ |
| 93 | |
| 94 | #beginNewLineRegExp; |
| 95 | |
| 96 | /** |
| 97 | * A RegExp to find a new line folowwed by a star followed by a space |
| 98 | * @type {RegExp} |
| 99 | */ |
| 100 | |
| 101 | #newLineStarSpaceRegExp; |
| 102 | |
| 103 | /** |
| 104 | A RegExp to find a type in the string ( a string starting with { and ending with } |
| 105 | @type {RegExp} |
| 106 | */ |
| 107 | |
| 108 | #typeRegExp; |
| 109 | |
| 110 | /** |
| 111 | A RegExp to find a name the string ( the first word with only chars and numbers |
| 112 | @type {RegExp} |
| 113 | */ |
| 114 | |
| 115 | #nameRegExp; |
| 116 | |
| 117 | /** |
| 118 | The constructor |
| 119 | */ |
| 120 | |
| 121 | constructor ( ) { |
| 122 | Object.freeze ( this ); |
| 123 | this.#tagRegExp = RegExp ( '@[a-z]*' ); |
| 124 | this.#beginSpaceNewlineRegExp = RegExp ( '^[ |\\n]' ); |
| 125 | this.#spaceNewlineRegExp = RegExp ( '[ |\\n]' ); |
| 126 | this.#endSpaceNewlineRegExp = RegExp ( '[ |\\n]$' ); |
| 127 | this.#multipleSpacesRegExp = RegExp ( '[ ]+', 'g' ); |
| 128 | this.#spaceNewlineSpaceRegExp = RegExp ( '[ ]*[\\n][ ]*', 'g' ); |
| 129 | this.#newLineStarSpaceRegExp = RegExp ( '[\\n][*][ ]', 'g' ); |
| 130 | this.#beginNewLineRegExp = RegExp ( '^\\n' ); |
| 131 | this.#typeRegExp = RegExp ( '{.*}' ); |
| 132 | this.#nameRegExp = RegExp ( '^[a-zA-Z0-9]*' ); |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | Set to uppercase the first letter of a text |
| 137 | @param {String} text The text to capitalize |
| 138 | @return {String} The capitalized text |
| 139 | */ |
| 140 | |
| 141 | #capitalizeFirstLetter ( text ) { |
| 142 | |
| 143 | switch ( text.toLowerCase ( ) ) { |
| 144 | case '' : |
| 145 | return text; |
| 146 | case 'null' : |
| 147 | return 'null'; |
| 148 | default : |
| 149 | return text [ 0 ].toUpperCase ( ) + text.substring ( 1 ); |
| 150 | } |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | Parse a type tag ( the value into {} for a type, param, return or returns tags. |
| 155 | Remove the { } < > ! and space chars from the type, replace the . char with ' of ', |
| 156 | replace the ? char with 'null or ', replace the | char with ' or ' and finally capitalize the first letter |
| 157 | of the types, so '{Number}' is parsed to 'Number', '{?String}' is parsed to 'null or String', |
| 158 | 'Array.<Number>' is parsed to 'Array of Number', {String|Number} is parsed to 'String or Number' |
| 159 | @param {String} type The type tag to parse |
| 160 | @return {String} The parsed type |
| 161 | */ |
| 162 | |
| 163 | #parseType ( type ) { |
| 164 | const tmpType = |
| 165 | type |
| 166 | .replaceAll ( '{', '' ) |
| 167 | .replaceAll ( '}', '' ) |
| 168 | .replaceAll ( ' ', '' ) |
| 169 | .replaceAll ( '.', ' of ' ) |
| 170 | .replaceAll ( '<', '' ) |
| 171 | .replaceAll ( '>', '' ) |
| 172 | .replaceAll ( '!', '' ) |
| 173 | .replaceAll ( '.', ' of ' ) |
| 174 | .replaceAll ( '?', 'null or ' ) |
| 175 | .replaceAll ( '|', ' or ' ); |
| 176 | if ( '' === tmpType ) { |
| 177 | return null; |
| 178 | } |
| 179 | |
| 180 | let returnValue = ''; |
| 181 | tmpType.trim ( ).split ( ' ' ) |
| 182 | .forEach ( |
| 183 | word => { |
| 184 | returnValue += |
| 185 | ( -1 === [ 'of', 'null', 'or' ].indexOf ( word ) ) |
| 186 | ? |
| 187 | this.#capitalizeFirstLetter ( word ) |
| 188 | : |
| 189 | word; |
| 190 | returnValue += ' '; |
| 191 | } |
| 192 | ); |
| 193 | |
| 194 | return returnValue.trimEnd ( ); |
| 195 | } |
| 196 | |
| 197 | /** |
| 198 | This method build a TypeDescription object from the contains of a comment tags |
| 199 | @param {String} commentTag The comment tag |
| 200 | @param {boolean} haveName A flag indicating that commentTag contains also a name to add in the TypeDescription |
| 201 | */ |
| 202 | |
| 203 | #getTypeDescription ( commentTag, haveName ) { |
| 204 | |
| 205 | const typeDescription = new TypeDescription ( ); |
| 206 | |
| 207 | // removing tag and spaces or newline. Spaces and newline must be in a separate replace! |
| 208 | let tmpCommentTag = |
| 209 | commentTag.replace ( this.#tagRegExp, '' ) |
| 210 | .replace ( this.#beginSpaceNewlineRegExp, '' ); |
| 211 | |
| 212 | // Searching type |
| 213 | const type = commentTag.match ( this.#typeRegExp ); |
| 214 | if ( type ) { |
| 215 | typeDescription.type = this.#parseType ( type [ 0 ] ); |
| 216 | |
| 217 | // removing type and spaces or newline |
| 218 | tmpCommentTag = |
| 219 | commentTag.substring ( commentTag.indexOf ( '}' ) + 1 ).replace ( this.#beginSpaceNewlineRegExp, '' ); |
| 220 | } |
| 221 | |
| 222 | // Searching name |
| 223 | if ( haveName ) { |
| 224 | if ( tmpCommentTag.match ( this.#nameRegExp ) ) { |
| 225 | typeDescription.name = tmpCommentTag.match ( this.#nameRegExp ) [ 0 ]; |
| 226 | typeDescription.name = typeDescription.name.replace ( this.#spaceNewlineRegExp, '' ); |
| 227 | if ( '' === typeDescription.name ) { |
| 228 | typeDescription.name = null; |
| 229 | } |
| 230 | |
| 231 | // removing name and spaces or newline |
| 232 | tmpCommentTag = tmpCommentTag.replace ( this.#nameRegExp, '' ).replace ( this.#beginSpaceNewlineRegExp, '' ); |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | // Searching desscription |
| 237 | // removing space and newline at the end |
| 238 | tmpCommentTag = tmpCommentTag.replace ( this.#endSpaceNewlineRegExp, '' ); |
| 239 | if ( '' !== tmpCommentTag ) { |
| 240 | typeDescription.desc = this.#capitalizeFirstLetter ( tmpCommentTag ); |
| 241 | } |
| 242 | |
| 243 | return Object.freeze ( typeDescription ); |
| 244 | } |
| 245 | |
| 246 | /** |
| 247 | Parse a comment tag. A comment tag is a text starting at the beginning of a comment, just after the /** |
| 248 | or starting with a @ char and finishing just before the next @ char in the comment or just before the */ |
| 249 | @param {String} commentTag the comment tag to parse |
| 250 | */ |
| 251 | |
| 252 | #parseCommentTag ( commentTag ) { |
| 253 | |
| 254 | // no @ char at the beginning. It's a desc... |
| 255 | if ( ! commentTag.startsWith ( '@' ) ) { |
| 256 | this.#commentsDoc.desc = this.#capitalizeFirstLetter ( commentTag ); |
| 257 | return; |
| 258 | } |
| 259 | |
| 260 | // searching the @ tag |
| 261 | const tag = commentTag.match ( this.#tagRegExp ) [ 0 ]; |
| 262 | |
| 263 | switch ( tag ) { |
| 264 | case '@desc' : |
| 265 | case '@classdesc' : |
| 266 | this.#commentsDoc.desc = this.#capitalizeFirstLetter ( |
| 267 | commentTag.replace ( this.#tagRegExp, '' ).replace ( this.#endSpaceNewlineRegExp, '' ) |
| 268 | ); |
| 269 | break; |
| 270 | case '@sample' : |
| 271 | this.#commentsDoc.sample = |
| 272 | commentTag.replace ( this.#tagRegExp, '' ).replace ( this.#endSpaceNewlineRegExp, '' ); |
| 273 | break; |
| 274 | case '@type' : |
| 275 | { |
| 276 | const type = commentTag.match ( this.#typeRegExp ); |
| 277 | if ( type ) { |
| 278 | this.#commentsDoc.type = this.#parseType ( type [ 0 ] ); |
| 279 | } |
| 280 | } |
| 281 | break; |
| 282 | case '@param' : |
| 283 | this.#commentsDoc.params = ( this.#commentsDoc.params ?? [] ); |
| 284 | this.#commentsDoc.params.push ( this.#getTypeDescription ( commentTag, true ) ); |
| 285 | break; |
| 286 | case '@return' : |
| 287 | case '@returns' : |
| 288 | this.#commentsDoc.returns = this.#getTypeDescription ( commentTag, false ); |
| 289 | break; |
| 290 | case '@ignore' : |
| 291 | this.#commentsDoc.ignore = true; |
| 292 | break; |
| 293 | default : |
| 294 | break; |
| 295 | } |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | Parse a leading comment and extracts the @desc, @classdesc, @sample,@type, @param, |
| 300 | @return, @returns, @ignore tags |
| 301 | @param {String} leadingComment The comment to parse |
| 302 | */ |
| 303 | |
| 304 | #parseLeadingComment ( leadingComment ) { |
| 305 | |
| 306 | // replacing Windows and Mac EOL with Unix EOL, tab with spaces and @ with a strange text surely not used |
| 307 | // then spliting the comments at the strange text, so the comment is splitted, preserving the @ |
| 308 | leadingComment |
| 309 | .replaceAll ( '\r\n', '\n' ) // eol windows |
| 310 | .replaceAll ( '\r', '\n' ) // eol mac |
| 311 | .replaceAll ( '\t', ' ' ) // tab |
| 312 | .replaceAll ( this.#multipleSpacesRegExp, ' ' ) // multiple spaces |
| 313 | .replaceAll ( this.#spaceNewlineSpaceRegExp, '\n' ) // spaces + eol + spaces |
| 314 | |
| 315 | .replaceAll ( this.#newLineStarSpaceRegExp, '\n' ) // eol + space + * |
| 316 | |
| 317 | .replaceAll ( '@', 'ยงยงยง@' ) // strange text |
| 318 | .replace ( this.#beginNewLineRegExp, '' ) // eol at the beginning |
| 319 | .split ( 'ยงยงยง' ) |
| 320 | .forEach ( |
| 321 | |
| 322 | // and parsing each result |
| 323 | commentTag => { this.#parseCommentTag ( commentTag ); } |
| 324 | ); |
| 325 | } |
| 326 | |
| 327 | /** |
| 328 | * Build a CommentsDoc object from the leading comments found in the code before the class/method/properties/variable |
| 329 | * @param {Array.<String>} leadingComments The leadingComments to use |
| 330 | * @return {CommentsDoc} An object with the comments |
| 331 | */ |
| 332 | |
| 333 | build ( leadingComments ) { |
| 334 | |
| 335 | if ( ! leadingComments ) { |
| 336 | return null; |
| 337 | } |
| 338 | |
| 339 | // Filtering on comments starting with * |
| 340 | const docLeadingComments = leadingComments.filter ( leadingComment => '*' === leadingComment.value [ 0 ] ); |
| 341 | |
| 342 | if ( 0 === docLeadingComments.length ) { |
| 343 | return null; |
| 344 | } |
| 345 | |
| 346 | this.#commentsDoc = new CommentsDoc ( ); |
| 347 | docLeadingComments.forEach ( |
| 348 | docLeadingComment => this.#parseLeadingComment ( docLeadingComment.value.substring ( 1 ) ) |
| 349 | ); |
| 350 | return Object.freeze ( this.#commentsDoc ); |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | export default CommentsDocBuilder; |
| 355 | |
| 356 | /* --- End of file --------------------------------------------------------------------------------------------------------- */ |
| 357 |