├── README.md └── docgen.js /README.md: -------------------------------------------------------------------------------- 1 | # XDocGen 2 | 3 | ## Description 4 | 5 | [XDocGen](http://x-vba.com/xdocgen) is a utility tool used to generate documentation from VBA source code. 6 | It uses a basic tag syntax which you can place within your source code, and will 7 | generate JSON documentation based off these tags and the procedures in the 8 | Module. 9 | 10 | ## Usage 11 | 12 | XDocGen is written in pure ES6 JavaScript, which means it can be run in the 13 | browser and does not require any external downloads to run. To use it, simply 14 | go to the XDocGen web page and follow the prompts. 15 | 16 | ## XDocGen Tags 17 | 18 | Tags are placed within comments, and take the following format: 19 | 20 | '@TagName: this is my tag 21 | 22 | Tags can either be placed within a Function or Sub, in which case they will be 23 | Produre-Level Tags, or outside of Functions or Subs, in which case they will be 24 | Module-Level Tags. For more information on the Tag syntax, see the official 25 | documentation. 26 | 27 | ## Where does my code go? 28 | 29 | Since XDocGen is written in pure ES6 JavaScript and has no external 30 | dependencies, your code is never shipped to a server to generate docs. XDocGen 31 | is run purely locally and can be run offline by saving the web page. Additionally, 32 | the source code for XDocGen can be found in the web page in unminified form, 33 | so you can be sure that your VBA code remains with you. 34 | 35 | ## XDocGen isn't running? 36 | 37 | XDocGen is written in ES6 JavaScript. Some older browsers don't support ES6 38 | JavaScript (notably older versions of Internet Explorer). If XDocGen does not 39 | run in your browser, try using a different browser, such as Chrome, Firefox, or 40 | Safari. 41 | 42 | Another common issue is incorrect syntax, or inconsistent @Param Tags compared 43 | to the actual parameter names of the Function or Sub. For more information on Tag 44 | syntax, see the official documentation. 45 | 46 | ## License 47 | 48 | The MIT License (MIT) 49 | 50 | Copyright © 2020 Anthony Mancini 51 | 52 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 53 | 54 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 55 | 56 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 57 | -------------------------------------------------------------------------------- /docgen.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Version 1.2.0 4 | 5 | /** 6 | * This function takes VBA source code and converts the relevant features and 7 | * the XDocGen tags into a JSON format to allow automatic documentation 8 | * generation. 9 | * 10 | * @author Anthony Mancini 11 | * @version 1.0.0 12 | * @license MIT 13 | * @todo Add module level XDocGen tag generation 14 | * @todo Add support for Module level variables, probably need anothe function for this, but 15 | * should be fairly easy to implement 16 | * @param {string} vbaSourceCode is the source code from a VBA module 17 | * @returns {string} a JSON string with relevant features of the procedures and 18 | * XDocGen tags 19 | */ 20 | function vbaDocGen(vbaSourceCode) { 21 | 22 | // Getting an array of Function and Sub source code 23 | let procedureCodeArray = getVbaFunctionAndSubProcedures(vbaSourceCode); 24 | 25 | // Generating partial XDocGen Documentation JSON for each Function and Sub, 26 | // and associating them with Argument Documentation Objects which will be 27 | // used to fill in parameter detials 28 | procedureCodeArray = procedureCodeArray.map(procedureCode => { 29 | return [ 30 | generateVbaProcedureDocumentationObject(procedureCode), 31 | generateVbaArgumentDocumentationObject(procedureCode), 32 | ] 33 | }); 34 | 35 | // Adjusting the XDocGen Documentation Objects to include details about 36 | // the parameters 37 | procedureCodeArray = procedureCodeArray.map(prodObj => adjustDocumentationParameters(prodObj[0], prodObj[1])); 38 | 39 | // Combining the Module Level XDocGen tag information into a single, 40 | // completed XDocGen Documentation Object 41 | let XDocGenDocumentationObject = { 42 | Module: generateVbaModuleDocumentationObject(vbaSourceCode), 43 | Procedures: procedureCodeArray, 44 | }; 45 | 46 | // Returning a JSON of the XDocGen Documentation Object 47 | return JSON.stringify(XDocGenDocumentationObject, null, 2) 48 | } 49 | 50 | 51 | /** 52 | * This function takes a VBA source code string and returns an Array of strings 53 | * of the Procedures in the source code. Currently this function supports 54 | * Functions, Subs, and Properties 55 | * 56 | * @author Anthony Mancini 57 | * @version 1.1.0 58 | * @license MIT 59 | * @todo Add support for other procedures like Operators 60 | * @param {string} vbaSourceCode is the source code from a VBA module 61 | * @returns {Array} an array of strings containing individual procedures (such 62 | * as Functions and Subs) 63 | */ 64 | function getVbaFunctionAndSubProcedures(vbaSourceCode) { 65 | // Creating a Regex to match all Functions and Subroutines 66 | let procedureRegex = /((Public|Private|Friend)\s){0,1}(Static\s){0,1}(Function|Sub|Property\sGet|Property\sLet|Property\sSet)\s{0,1}[a-zA-Z0-9_]*?\s{0,1}\([\S\s]*?End\s(Function|Sub|Property)/gmi 67 | 68 | return vbaSourceCode.match(procedureRegex) 69 | } 70 | 71 | 72 | /** 73 | * This function takes a single procedure source code and generates a partially 74 | * complete documentation Object with relevant details from the XDocGen tags. 75 | * 76 | * @author Anthony Mancini 77 | * @version 1.1.1 78 | * @license MIT 79 | * @param {string} vbaProcedureCode is the source code of a single procedure 80 | * @returns {Object} a partially completed documentation Object. It's partially 81 | * complete in that it does not yet contain additional information about the 82 | * parameters 83 | */ 84 | function generateVbaProcedureDocumentationObject(vbaProcedureCode) { 85 | let documentationObject = {}; 86 | 87 | // Getting the function details 88 | let procedureDetailsArray = vbaProcedureCode.split("(")[0].trim(); 89 | 90 | // Getting the name of the Procedure 91 | let procedureName = procedureDetailsArray.split(" ")[procedureDetailsArray.split(" ").length - 1]; 92 | documentationObject["Name"] = procedureName; 93 | 94 | procedureDetailsArray = procedureDetailsArray.split(" ").map(produceDetail => produceDetail.toLowerCase()); 95 | 96 | // Getting the Scope of the Procedure 97 | let scopePart; 98 | 99 | if (procedureDetailsArray.includes("public")) { 100 | scopePart = "Public"; 101 | } else if (procedureDetailsArray.includes("private")) { 102 | scopePart = "Private"; 103 | } else if (procedureDetailsArray.includes("friend")) { 104 | scopePart = "Friend"; 105 | } else { 106 | scopePart = "Public"; 107 | } 108 | 109 | documentationObject["Scope"] = scopePart; 110 | 111 | 112 | // Determining if the Procedure is a Static procedure 113 | let staticPart; 114 | 115 | if (procedureDetailsArray.includes("static")) { 116 | staticPart = true; 117 | } else { 118 | staticPart = false; 119 | } 120 | 121 | documentationObject["Static"] = staticPart; 122 | 123 | 124 | // Determining if the Procedure is a Function or a Subroutine 125 | let procedurePart; 126 | 127 | if (procedureDetailsArray.includes("function")) { 128 | procedurePart = "Function"; 129 | } else if (procedureDetailsArray.includes("sub")) { 130 | procedurePart = "Sub"; 131 | } else if (procedureDetailsArray.includes("property")) { 132 | procedurePart = "Property"; 133 | } 134 | 135 | documentationObject["Procedure"] = procedurePart; 136 | 137 | // For Properties, determining if they are Getters, Letters, or Setters 138 | if (procedurePart === "Property") { 139 | let propertyType = procedureDetailsArray[1].substr(0, 1).toUpperCase() + procedureDetailsArray[1].substr(1, procedureDetailsArray[1].length); 140 | documentationObject["Property"] = propertyType; 141 | 142 | // Modifying the name in cases of Properties so that the Get and Let don't 143 | // overwrite each other in the documentation object 144 | documentationObject["Name"] = `${documentationObject["Name"]}(${propertyType})` 145 | } 146 | 147 | // Getting the return type of the function 148 | let typePart = vbaProcedureCode.split(")")[1]; 149 | 150 | // Could use a bit of refactoring, as a little complicated now, but handles 151 | // cases where the return type is on a seperate line as the As, such as As _ 152 | if (typePart.split("\n")[0].includes(" _")) { 153 | typePart = typePart.split("\n")[1].trim(); 154 | } else if (typePart.split("\n")[0].toLowerCase().includes("as ")) { 155 | typePart = typePart.split(/As\s/gmi)[1].trim(); 156 | } else { 157 | typePart = "Variant"; 158 | } 159 | 160 | if (typePart.includes("\n")) { 161 | typePart = typePart.split("\n")[0].trim(); 162 | } 163 | 164 | if (typePart.toLowerCase().includes("as ")) { 165 | typePart = typePart.split(/As\s/gmi)[1].trim(); 166 | } 167 | 168 | documentationObject["Type"] = typePart; 169 | 170 | 171 | // Adding the procedure source code 172 | documentationObject["Source"] = vbaProcedureCode; 173 | 174 | 175 | return Object.assign(documentationObject, generateXDocGenTagsObject(vbaProcedureCode)) 176 | } 177 | 178 | 179 | /** 180 | * This function takes a single procedure source code and generates an 181 | * Argument documentation Object which contains details about the arguments of 182 | * the procedure 183 | * 184 | * @author Anthony Mancini 185 | * @version 1.0.0 186 | * @license MIT 187 | * @param {string} vbaProcedureCode is the source code of a single procedure 188 | * @returns {Object} an Argument Documentation Object which will be used to 189 | * fill in the remaining details of the Documentation Object 190 | */ 191 | function generateVbaArgumentDocumentationObject(vbaProcedureCode) { 192 | let documentationTagRegex = /\([\S\s]*?\)/gmi 193 | let documentationObject = {}; 194 | 195 | let argumentArray = vbaProcedureCode.match(documentationTagRegex)[0]; 196 | argumentArray = argumentArray.replace("(", "").replace(")", "").replace(/_/gmi, "").split(","); 197 | argumentArray = argumentArray.map(argumentLine => argumentLine.trim()); 198 | 199 | argumentArray.forEach(argumentLine => { 200 | let modifierArray = argumentLine.split(/\sAs\s/gmi)[0].split(" "); 201 | modifierArray = modifierArray.map(modifierName => modifierName.toLowerCase()); 202 | 203 | let optionalPart; 204 | if (modifierArray.includes("optional")) 205 | optionalPart = true; 206 | else 207 | optionalPart = false; 208 | 209 | let passingPart; 210 | if (modifierArray.includes("byval")) 211 | passingPart = "ByVal"; 212 | else 213 | passingPart = "ByRef"; 214 | 215 | let paramArrayPart; 216 | if (modifierArray.includes("paramarray")) 217 | paramArrayPart = true; 218 | else 219 | paramArrayPart = false; 220 | 221 | let namePart = argumentLine.split(/\sAs\s/gmi)[0].split(" "); 222 | namePart = namePart[namePart.length - 1].replace().trim(); 223 | 224 | let arrayPart; 225 | if (namePart.includes("(")) 226 | arrayPart = true; 227 | else 228 | arrayPart = false; 229 | 230 | namePart = namePart.split("(").join("").split(")").join(""); 231 | 232 | let typePart; 233 | if (argumentLine.toLowerCase().includes(" as ")) 234 | typePart = argumentLine.split(/\sAs\s/gmi)[1].trim(); 235 | else 236 | typePart = "Variant"; 237 | 238 | let defaultValuePart; 239 | if (argumentLine.toLowerCase().includes("=")) 240 | defaultValuePart = argumentLine.split("=")[1].trim(); 241 | else 242 | defaultValuePart = null; 243 | 244 | 245 | documentationObject[namePart] = { 246 | Name: namePart, 247 | Optional: optionalPart, 248 | Passing: passingPart, 249 | ParamArray: paramArrayPart, 250 | Type: typePart, 251 | Array: arrayPart, 252 | Default: defaultValuePart, 253 | }; 254 | 255 | }); 256 | 257 | return documentationObject 258 | } 259 | 260 | 261 | /** 262 | * This function takes a partially completed Documentation Object and an 263 | * Argument Documentation Object and fills in the Param details of the 264 | * Documentation Object 265 | * 266 | * @author Anthony Mancini 267 | * @version 1.0.0 268 | * @license MIT 269 | * @todo potentially rework this function a bit as it is currently quite nested 270 | * since it is handling a lot of different cases. It's the most complicated 271 | * function in this program probably 272 | * @param {Object} documentationObject is a partially completed Documentation Object 273 | * @param {Object} parameterObject is an Argument Documentation Object 274 | * @returns {Object} a filled in Documentation Object containing Param details 275 | */ 276 | function adjustDocumentationParameters(documentationObject, parameterObject) { 277 | 278 | // Used when there are more than 1 param and thus is an array 279 | if (Array.isArray(documentationObject["Param"])) { 280 | documentationObject["Param"] = documentationObject["Param"].map(paramLine => { 281 | let argumentName = paramLine.split(" ")[0]; 282 | argumentName = argumentName.split("(").join("").split(")").join(""); 283 | let argumentDescription = paramLine.substr(paramLine.indexOf(" ") + 1, paramLine.length); 284 | 285 | try { 286 | return Object.assign(parameterObject[argumentName], {Description: argumentDescription}) 287 | } catch (e) { 288 | throw `Argument Not Found.\n\nDoes the list contain your argument?\nIf not, check that that your @Param tags match the argument names.\n\nArgument Name: ${argumentName}\nArgument List: [${Object.keys(parameterObject)}]` 289 | } 290 | }); 291 | 292 | // Used when there are no @Param tags 293 | } else if (documentationObject["Param"] === undefined) { 294 | 295 | // Executes when the Procedure has no actual arguments 296 | if ("" in parameterObject) { 297 | documentationObject["Param"] = null; 298 | 299 | // Executes when there are parameters but there are no @Param tags 300 | // documenting them 301 | } else { 302 | 303 | // Executes for multiple parameters 304 | if (Object.keys(parameterObject).length > 1) { 305 | let parameterArray = []; 306 | 307 | for (let paramKey in parameterObject) { 308 | parameterArray.push(parameterObject[paramKey]); 309 | } 310 | 311 | documentationObject["Param"] = parameterArray; 312 | 313 | // Executes for single parameters 314 | } else { 315 | documentationObject["Param"] = parameterObject[Object.keys(parameterObject)[0]]; 316 | } 317 | } 318 | 319 | // Used when there is only a single parameter and thus not an array 320 | } else { 321 | let argumentName = documentationObject["Param"].split(" ")[0]; 322 | argumentName = argumentName.split("(").join("").split(")").join(""); 323 | let argumentDescription = documentationObject["Param"].substr(documentationObject["Param"].indexOf(" ") + 1, documentationObject["Param"].length); 324 | 325 | try { 326 | documentationObject["Param"] = Object.assign(parameterObject[argumentName], {Description: argumentDescription}) 327 | } catch (e) { 328 | throw `Argument Not Found.\n\nDoes the list contain your argument?\nIf not, check that that your @Param tags match the argument names.\n\nArgument Name: ${argumentName}\nArgument List: [${Object.keys(parameterObject)}]` 329 | } 330 | 331 | } 332 | 333 | return documentationObject 334 | } 335 | 336 | 337 | /** 338 | * This function generates the Module Level tag documentation object 339 | * 340 | * @author Anthony Mancini 341 | * @version 1.0.0 342 | * @license MIT 343 | * @todo clean up the code names, and perhaps move some of this stuff to a class since I 344 | * am reusing a bunch of code here, and especially the regexs and the tag doc generation. 345 | * @param {string} vbaSourceCode is the VBA source code 346 | * @returns {Object} a Module Documentation Object 347 | */ 348 | function generateVbaModuleDocumentationObject(vbaSourceCode) { 349 | let procedureRegex = /((Public|Private|Friend)\s){0,1}(Static\s){0,1}(Function|Sub|Property\sGet|Property\sLet|Property\sSet)\s{0,1}[a-zA-Z0-9_]*?\s{0,1}\([\S\s]*?End\s(Function|Sub|Property)/gmi 350 | 351 | let procedureMatches = vbaSourceCode.match(procedureRegex); 352 | 353 | if (procedureMatches !== null) { 354 | procedureMatches.forEach(procedureCode => { 355 | vbaSourceCode = vbaSourceCode.split(procedureCode).join(""); 356 | }); 357 | } 358 | 359 | 360 | return generateXDocGenTagsObject(vbaSourceCode); 361 | } 362 | 363 | 364 | /** 365 | * This function generates the Module Level tag documentation object 366 | * 367 | * @author Anthony Mancini 368 | * @version 1.0.0 369 | * @license MIT 370 | * @todo add this to a class at some point 371 | * @param {string} vbaCodeFragment is the remaining VBA source code after removing 372 | * procedures from it 373 | * @returns {Object} a Module Documentation Object 374 | */ 375 | function generateXDocGenTagsObject(vbaCodeFragment) { 376 | 377 | let documentationTagRegex = /\'\@[a-zA-Z0-9_]*?[:][\S\s]*?\n/gmi 378 | 379 | // Generating the documentation 380 | let tagMatches = vbaCodeFragment.match(documentationTagRegex); 381 | let documentationObject = {}; 382 | 383 | if (tagMatches !== null) { 384 | tagMatches.forEach(docLine => { 385 | let docTag = docLine.substr(0, docLine.indexOf(":")).replace("@", "").replace("'", "").replace(":", "").trim(); 386 | let docValue = docLine.substr(docLine.indexOf(":") + 1, docLine.length - 1).trimLeft().replace("\n", "").replace("\r", ""); 387 | 388 | if (docTag in documentationObject) { 389 | documentationObject[docTag].push(docValue); 390 | } else { 391 | documentationObject[docTag] = [docValue]; 392 | } 393 | }); 394 | } 395 | 396 | // Adjusting the documentation so that all arrays with length of 1 are 397 | // combined into a single value 398 | for (let docTag in documentationObject) { 399 | if (documentationObject[docTag].length === 1) { 400 | documentationObject[docTag] = documentationObject[docTag][0]; 401 | } 402 | } 403 | 404 | return documentationObject; 405 | } 406 | 407 | 408 | module.exports = { 409 | vbaDocGen: vbaDocGen, 410 | }; 411 | --------------------------------------------------------------------------------