Convert JsDoc to JSON used in Sheets' Custom Functions written in Google Apps Script
Use RegEx to quickly identify relevant patterns of JsDoc supported by Sheets Custom Function in Apps Script and convert that into JSON.
Problem statement
I was recently working on yet another workspace add-on (Custom Functions) which required me to parse JsDoc data that was provided as part of an open-source repository of custom functions (being consumed by Google Sheets), written in Apps Script and that's what got me to write my own set of RegEx to match all the relevant, applicable and supported params.
Here's an example of a custom function —
/**
* Multiplies the input value by 2.
*
* @param {number} input The value or range of cells to multiply.
* @return {number} The input multiplied by 2.
* @customfunction
*/
function DOUBLE(input) {
return Array.isArray(input) ?
input.map(row => row.map(cell => cell * 2)) :
input * 2;
}
Now, I wanted to display the description of the function that shows-up like so on Google Sheets —
...inside my add-on —
I also eventually wanted to display the input param
and the data that would show-up via return
and so, figured it could come in handy to transform the entire JsDoc blob into JSON.
Prior reading
- Custom Functions in Google Sheets > Autocomplete
- A little know-how about JsDoc
Supported formats
For the first time (ever!), I thought about creating the right set of test cases to begin with, BEFORE I launched myself head-first into creating the actual RegEx itself. These are the ones I landed on —
@param
@param somebody
@param {string} somebody
@param {string} somebody Somebody's name.
@param {string} somebody - Somebody's name.
@param {string} employee.name - The name of the employee.
@param {Object[]} employees - The employees who are responsible for the project.
@param {string} employees[].department - The employee's department.
@param {string=} somebody - Somebody's name.
@param {*} somebody - Whatever you want.
@param {string} [somebody=John Doe] - Somebody's name.
@param {(string|string[])} [somebody=John Doe] - Somebody's name, or an array of names.
@param {string} [somebody] - Somebody's name.
@return
@returns {number}
@returns {number} Sum of a and b
@returns {(number|Array)} Sum of a and b or an array that contains a, b and the sum of a and b.
@returns {Promise} Promise object represents the sum of a and b
While the goal was to come-up with a single expression, I wasn't fully able to do that but had some fun with it anyway —
Solution
I ended-up with 3 expressions. 2 to handle all kinds of input @param
and 1 for the values that would show-up as per @return
—
Case 1
When the @param
didn't specify any data type 😬
@param somebody
and the RegEx statement to handle this —
^@(param) (?:(?=[)(?:[(.*)]$)|(?![)(?:([^\s]+)$))
Case 2
Literally, all the other, supported cases of @param
within Apps Script (for the purposes of Custom Functions)
@param {string} somebody
@param {string} somebody Somebody's name.
@param {string} somebody - Somebody's name.
@param {string} employee.name - The name of the employee.
@param {Object[]} employees - The employees who are responsible for the project.
@param {string} employees[].department - The employee's department.
@param {string=} somebody - Somebody's name.
@param {*} somebody - Whatever you want.
@param {string} [somebody=John Doe] - Somebody's name.
@param {(string|string[])} [somebody=John Doe] - Somebody's name, or an array of names.
@param {string} [somebody] - Somebody's name.
and the RegEx statement to handle these —
^\@(param)(?: )\{(.*)\}(?: )(?:(?=\[)(?:\[(.*?)\])|(?!\[)(?:(.*?)))(?:(?= )(?: )(?:\- )?(.*)|(?! )$)
This one came with a VERY useful aha! moment too —
Case 3
To handle the humble @return
Possible scenarios —
@returns {number}
@returns {number} Sum of a and b
@returns {(number|Array)} Sum of a and b or an array that contains a, b and the sum of a and b.
@returns {Promise} Promise object represents the sum of a and b
and the RegEx statement to handle 'em all —
^\@(returns)(?: )\{(.*)\}(?:(?= )(?: )(?:\- )?(.*)|(?! )$)
Codebase
You can access the entire script via my GitHub repository here or make a copy of this Apps Script file here.
For reference, here's the code —
function jsDoc2JSON(input) {
input = UrlFetchApp.fetch("https://raw.githubusercontent.com/custom-functions/google-sheets/main/functions/DOUBLE.gs").getBlob().getDataAsString()
const jsDocJSON = {};
const jsDocComment = input.match(/\/\*\*.*\*\//s);
const jsDocDescription = jsDocComment ? jsDocComment[0].match(/^[^@]*/s) : false;
const description = jsDocDescription ? jsDocDescription[0].split("*").map(el => el.trim()).filter(el => el !== '' && el !== '/').join(" ") : false;
const jsDocTags = jsDocComment ? jsDocComment[0].match(/@.*(?=\@)/s) : false;
const rawTags = jsDocTags ? jsDocTags[0].split("*").map(el => el.trim()).filter(el => el !== '') : false;
const tags = [];
let components;
rawTags.forEach(el => {
if (el.startsWith("@param ")) { // https://jsdoc.app/tags-param.html
components = el.match(/^\@(param)(?: )\{(.*)\}(?: )(?:(?=\[)(?:\[(.*?)\])|(?!\[)(?:(.*?)))(?:(?= )(?: )(?:\- )?(.*)|(?! )$)/i);
if (components) {
components = components.filter(el => el !== undefined);
tags.push({
"tag": "param",
"type": components[2] ? components[2] : null,
"name": components[3] ? components[3] : null,
"description": components[4] ? components[4] : null,
});
} else {
components = el.match(/^\@(param) (?:(?=\[)(?:\[(.*)\]$)|(?!\[)(?:([^\s]+)$))/i);
if (components) {
components = components.filter(el => el !== undefined);
tags.push({
"tag": "param",
"type": components[2] ? components[2] : null,
"name": components[3] ? components[3] : null,
"description": components[4] ? components[4] : null,
});
} else {
console.log(`invalid @param tag: ${el}`);
}
}
} else if (el.startsWith("@return ") || el.startsWith("@returns ")) { // https://jsdoc.app/tags-returns.html
components = el.match(/^\@(returns?)(?: )\{(.*)\}(?:(?= )(?: )(?:\- )?(.*)|(?! )$)/i);
if (components) {
components = components.filter(el => el !== undefined);
tags.push({
"tag": "return",
"type": components[2] ? components[2] : null,
"description": components[3] ? components[3] : null,
});
} else {
console.log(`invalid @return tag: ${el}`);
}
} else {
console.log(`unknown tag: ${el}`);
}
});
jsDocJSON.description = description;
jsDocJSON.tags = tags;
console.log(JSON.stringify(jsDocJSON, null, 2));
}
// https://jsdoc.app/tags-param.html
// ^\@(param) (?:(?=\[)(?:\[(.*)\]$)|(?!\[)(?:([^\s]+)$))
// @param somebody
// ^\@(param)(?: )\{(.*)\}(?: )(?:(?=\[)(?:\[(.*?)\])|(?!\[)(?:(.*?)))(?:(?= )(?: )(?:\- )?(.*)|(?! )$)
// @param {string} somebody
// @param {string} somebody Somebody's name.
// @param {string} somebody - Somebody's name.
// @param {string} employee.name - The name of the employee.
// @param {Object[]} employees - The employees who are responsible for the project.
// @param {string} employees[].department - The employee's department.
// @param {string=} somebody - Somebody's name.
// @param {*} somebody - Whatever you want.
// @param {string} [somebody=John Doe] - Somebody's name.
// @param {(string|string[])} [somebody=John Doe] - Somebody's name, or an array of names.
// @param {string} [somebody] - Somebody's name.
// https://jsdoc.app/tags-returns.html
// ^\@(returns)(?: )\{(.*)\}(?:(?= )(?: )(?:\- )?(.*)|(?! )$)
// @returns {number}
// @returns {number} Sum of a and b
// @returns {(number|Array)} Sum of a and b or an array that contains a, b and the sum of a and b.
// @returns {Promise} Promise object represents the sum of a and b
Conclusion
In case you find any of the applicable patterns NOT being handled by the RegEx/script, please do reach out to me either via email (code@script.gs
), Twitter, LinkedIn or any of the means listed here.
You'd be surprised how many folks reach out via Telegram too 😄