prettier-plugin-jsdoc
Version:
A Prettier plugin to format JSDoc comments.
1,290 lines (1,281 loc) • 52.2 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('comment-parser'), require('prettier'), require('binary-searching'), require('mdast-util-from-markdown'), require('prettier/plugins/babel'), require('prettier/plugins/flow'), require('prettier/plugins/typescript')) :
typeof define === 'function' && define.amd ? define(['exports', 'comment-parser', 'prettier', 'binary-searching', 'mdast-util-from-markdown', 'prettier/plugins/babel', 'prettier/plugins/flow', 'prettier/plugins/typescript'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.sayHello = {}, global.commentParser, global.prettier, null, global.mdastUtilFromMarkdown, global.parserBabel, global.parserFlow, global.parserTypescript));
})(this, (function (exports, commentParser, prettier, BSearch, mdastUtilFromMarkdown, parserBabel, parserFlow, parserTypescript) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var parserBabel__default = /*#__PURE__*/_interopDefaultLegacy(parserBabel);
var parserFlow__default = /*#__PURE__*/_interopDefaultLegacy(parserFlow);
var parserTypescript__default = /*#__PURE__*/_interopDefaultLegacy(parserTypescript);
const ABSTRACT = "abstract";
const ASYNC = "async";
const AUGMENTS = "augments";
const AUTHOR = "author";
const BORROWS = "borrows";
const CALLBACK = "callback";
const CATEGORY = "category";
const CLASS = "class";
const CONSTANT = "constant";
const DEFAULT = "default";
const DEFAULT_VALUE = "defaultValue";
const DEPRECATED = "deprecated";
const DESCRIPTION = "description";
const EXAMPLE = "example";
const EXTENDS = "extends";
const EXTERNAL = "external";
const FILE = "file";
const FIRES = "fires";
const FLOW = "flow";
const FUNCTION = "function";
const IGNORE = "ignore";
const LICENSE = "license";
const MEMBER = "member";
const MEMBEROF = "memberof";
const MODULE = "module";
const NAMESPACE = "namespace";
const OVERLOAD = "overload";
const OVERRIDE = "override";
const PARAM = "param";
const PRIVATE = "private";
const PRIVATE_REMARKS = "privateRemarks";
const PROPERTY = "property";
const PROVIDES_MODULE = "providesModule";
const REMARKS = "remarks";
const RETURNS = "returns";
const SEE = "see";
const SINCE = "since";
const TEMPLATE = "template";
const THROWS = "throws";
const TODO = "todo";
const TYPE = "type";
const TYPE_PARAM = "typeParam";
const TYPEDEF = "typedef";
const SATISFIES = "satisfies";
const VERSION = "version";
const YIELDS = "yields";
const SPACE_TAG_DATA = {
tag: "this_is_for_space",
name: "",
optional: false,
type: "",
description: "",
source: [],
problems: [],
};
const TAGS_SYNONYMS = {
arg: PARAM,
argument: PARAM,
const: CONSTANT,
constructor: CLASS,
desc: DESCRIPTION,
emits: FIRES,
examples: EXAMPLE,
exception: THROWS,
fileoverview: FILE,
func: FUNCTION,
host: EXTERNAL,
method: FUNCTION,
overview: FILE,
params: PARAM,
prop: PROPERTY,
return: RETURNS,
var: MEMBER,
virtual: ABSTRACT,
yield: YIELDS,
hidden: IGNORE,
};
const TAGS_DEFAULT = [DEFAULT, DEFAULT_VALUE];
const TAGS_NAMELESS = [
BORROWS,
CATEGORY,
DEPRECATED,
DESCRIPTION,
EXAMPLE,
EXTENDS,
LICENSE,
MODULE,
NAMESPACE,
OVERLOAD,
OVERRIDE,
PRIVATE_REMARKS,
REMARKS,
RETURNS,
SINCE,
THROWS,
TODO,
YIELDS,
FILE,
...TAGS_DEFAULT,
];
const TAGS_TYPELESS = [
BORROWS,
BORROWS,
DEPRECATED,
DESCRIPTION,
EXAMPLE,
IGNORE,
LICENSE,
MODULE,
NAMESPACE,
OVERLOAD,
OVERRIDE,
PRIVATE_REMARKS,
REMARKS,
SINCE,
TODO,
FILE,
];
const TAGS_PEV_FORMATE_DESCRIPTION = [
BORROWS,
...TAGS_DEFAULT,
MEMBEROF,
MODULE,
SEE,
];
const TAGS_DESCRIPTION_NEEDED = [
BORROWS,
CATEGORY,
DESCRIPTION,
EXAMPLE,
PRIVATE_REMARKS,
REMARKS,
SINCE,
TODO,
];
const TAGS_VERTICALLY_ALIGN_ABLE = [
EXTENDS,
PARAM,
PROPERTY,
RETURNS,
THROWS,
TYPE,
SATISFIES,
TYPEDEF,
YIELDS,
];
const TAGS_GROUP_HEAD = [CALLBACK, TYPEDEF];
const TAGS_GROUP_CONDITION = [
...TAGS_GROUP_HEAD,
TYPE,
PROPERTY,
PARAM,
RETURNS,
YIELDS,
THROWS,
];
const TAGS_ORDER = {
[REMARKS]: 1,
[PRIVATE_REMARKS]: 2,
[PROVIDES_MODULE]: 3,
[MODULE]: 4,
[LICENSE]: 5,
[FLOW]: 6,
[ASYNC]: 7,
[PRIVATE]: 8,
[IGNORE]: 9,
[MEMBEROF]: 10,
[VERSION]: 11,
[FILE]: 12,
[AUTHOR]: 13,
[DEPRECATED]: 14,
[SINCE]: 15,
[CATEGORY]: 16,
[DESCRIPTION]: 17,
[EXAMPLE]: 18,
[ABSTRACT]: 19,
[AUGMENTS]: 20,
[CONSTANT]: 21,
[DEFAULT]: 22,
[DEFAULT_VALUE]: 23,
[EXTERNAL]: 24,
[OVERLOAD]: 25,
[FIRES]: 26,
[TEMPLATE]: 27,
[TYPE_PARAM]: 28,
[FUNCTION]: 29,
[NAMESPACE]: 30,
[BORROWS]: 31,
[CLASS]: 32,
[EXTENDS]: 33,
[MEMBER]: 34,
[TYPEDEF]: 35,
[TYPE]: 36,
[SATISFIES]: 37,
[PROPERTY]: 38,
[CALLBACK]: 39,
[PARAM]: 40,
[YIELDS]: 41,
[RETURNS]: 42,
[THROWS]: 43,
other: 44,
[SEE]: 45,
[TODO]: 46,
};
function convertToModernType(oldType) {
return withoutStrings(oldType, (type) => {
type = type.trim();
type = type.replace(/\.</g, "<");
type = type.replace(/\*/g, " any ");
type = type
.replace(/^\?\s*(\w+)$/, "$1 | null")
.replace(/^(\w+)\s*\?$/, "$1 | null");
let changed = true;
while (changed) {
changed = false;
type = type.replace(/(^|[^$\w\xA0-\uFFFF])Array\s*<((?:[^<>=]|=>|=(?!>)|<(?:[^<>=]|=>|=(?!>))+>)+)>/g, (_, prefix, inner) => {
changed = true;
return `${prefix}(${inner})[]`;
});
}
return type;
});
}
function withoutStrings(type, mapFn) {
const strings = [];
let modifiedType = type.replace(/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/g, (m) => {
strings.push(m);
return `String$${strings.length - 1}$`;
});
if (modifiedType.includes("`")) {
return type;
}
modifiedType = mapFn(modifiedType);
return modifiedType.replace(/String\$(\d+)\$/g, (_, index) => strings[index]);
}
async function formatType(type, options) {
try {
const TYPE_START = "type name = ";
let pretty = type;
let rest = false;
if (pretty.startsWith("...")) {
rest = true;
pretty = `(${pretty.slice(3)})[]`;
}
pretty = await prettier.format(`${TYPE_START}${pretty}`, {
...options,
parser: "typescript",
plugins: [],
filepath: "file.ts",
});
pretty = pretty.slice(TYPE_START.length);
pretty = pretty
.replace(/^\s*/g, "")
.replace(/[;\n]*$/g, "")
.replace(/^\|/g, "")
.trim();
if (rest) {
pretty = "..." + pretty.replace(/\[\s*\]$/, "");
}
return pretty;
}
catch (error) {
return type;
}
}
function addStarsToTheBeginningOfTheLines(originalComment, comment, options) {
if ((options.jsdocCommentLineStrategy === "singleLine" &&
numberOfAStringInString(comment.trim(), "\n") === 0) ||
(options.jsdocCommentLineStrategy === "keep" &&
numberOfAStringInString(originalComment, "\n") === 0)) {
return `* ${comment.trim()} `;
}
return `*${comment.replace(/(\n(?!$))/g, "\n * ")}\n `;
}
function numberOfAStringInString(string, search) {
return (string.match(new RegExp(search, "g")) || []).length;
}
function capitalizer(str) {
if (!str) {
return str;
}
if (str.match(/^https?:\/\//i)) {
return str;
}
if (str.startsWith("- ")) {
return str.slice(0, 2) + capitalizer(str.slice(2));
}
return str[0].toUpperCase() + str.slice(1);
}
function detectEndOfLine(text) {
const counter = {
"\r": 0,
"\r\n": 0,
"\n": 0,
};
const lineEndPattern = /\r\n?|\n/g;
let m;
while ((m = lineEndPattern.exec(text))) {
counter[m[0]]++;
}
const cr = counter["\r"];
const crlf = counter["\r\n"];
const lf = counter["\n"];
const max = Math.max(cr, crlf, lf);
if (lf === max) {
return "lf";
}
else if (crlf === max) {
return "crlf";
}
else {
return "cr";
}
}
async function formatCode(result, beginningSpace, options) {
const { printWidth, jsdocKeepUnParseAbleExampleIndent } = options;
if (result
.split("\n")
.slice(1)
.every((v) => !v.trim() || v.startsWith(beginningSpace))) {
result = result.replace(new RegExp(`\n${beginningSpace.replace(/[\t]/g, "[\\t]")}`, "g"), "\n");
}
try {
let formattedExample = "";
const examplePrintWith = printWidth - 4;
if (result.trim().startsWith("{")) {
formattedExample = await prettier.format(result || "", {
...options,
parser: "json",
printWidth: examplePrintWith,
});
}
else {
formattedExample = await prettier.format(result || "", {
...options,
printWidth: examplePrintWith,
});
}
result = formattedExample.replace(/(^|\n)/g, `\n${beginningSpace}`);
}
catch (err) {
result = `\n${result
.split("\n")
.map((l) => `${beginningSpace}${jsdocKeepUnParseAbleExampleIndent ? l : l.trim()}`)
.join("\n")}\n`;
result = result.replace(/^\n[\s]+\n/g, "\n");
}
return result;
}
const findPluginByParser = (parserName, options) => {
const tsPlugin = options.plugins.find((plugin) => {
return (typeof plugin === "object" &&
plugin !== null &&
!(plugin instanceof URL) &&
plugin.name &&
plugin.parsers &&
plugin.parsers.hasOwnProperty(parserName));
});
return !tsPlugin ||
tsPlugin.name === "prettier-plugin-jsdoc" ||
tsPlugin.parsers?.hasOwnProperty("jsdoc-parser")
? undefined
: tsPlugin.parsers?.[parserName];
};
const isDefaultTag = (tag) => TAGS_DEFAULT.includes(tag);
const TABLE = "2@^5!~#sdE!_TABLE";
const parserSynonyms = (lang) => {
switch (lang) {
case "js":
case "javascript":
case "jsx":
return ["babel", "babel-flow", "vue"];
case "ts":
case "typescript":
case "tsx":
return ["typescript", "babel-ts", "angular"];
case "json":
case "css":
return ["css"];
case "less":
return ["less"];
case "scss":
return ["scss"];
case "html":
return ["html"];
case "yaml":
return ["yaml"];
default:
return ["babel"];
}
};
function descriptionEndLine({ tag, isEndTag, }) {
if ([DESCRIPTION, EXAMPLE, TODO].includes(tag) && !isEndTag) {
return "\n";
}
return "";
}
async function formatDescription(tag, text, options, formatOptions) {
if (!text)
return text;
const { printWidth } = options;
const { tagStringLength = 0, beginningSpace } = formatOptions;
text = text.replace(/^(\d+)[-][\s|]+/g, "$1. ");
text = text.replace(/\n+(\s*\d+)[-][\s]+/g, "\n$1. ");
const fencedCodeBlocks = text.matchAll(/```\S*?\n[\s\S]+?```/gm);
const indentedCodeBlocks = text.matchAll(/^\r?\n^(?:(?:(?:[ ]{4}|\t).*(?:\r?\n|$))+)/gm);
const allCodeBlocks = [...fencedCodeBlocks, ...indentedCodeBlocks];
const tables = [];
text = text.replace(/((\n|^)\|[\s\S]*?)((\n[^|])|$)/g, (code, _1, _2, _3, _, offs) => {
for (const block of allCodeBlocks) {
if (block.index !== undefined &&
block.index <= offs + 1 &&
offs + code.length + 1 <= block.index + block[0].length) {
return code;
}
}
code = _3 ? code.slice(0, -1) : code;
tables.push(code);
return `\n\n${TABLE}\n\n${_3 ? _3.slice(1) : ""}`;
});
if (options.jsdocCapitalizeDescription &&
!TAGS_PEV_FORMATE_DESCRIPTION.includes(tag)) {
text = capitalizer(text);
}
text = `${tagStringLength ? `${"!".repeat(tagStringLength - 1)}?` : ""}${text.startsWith("```") ? "\n" : ""}${text}`;
let tableIndex = 0;
text = text.replace(new RegExp("\\n" + `[\u0020]{${beginningSpace.length}}`, "g"), "\n");
const rootAst = mdastUtilFromMarkdown.fromMarkdown(text);
async function stringifyASTWithoutChildren(mdAst, intention, parent) {
if (mdAst.type === "inlineCode") {
return `\`${mdAst.value}\``;
}
if (mdAst.type === "code") {
let result = mdAst.value || "";
let _intention = intention;
if (result) {
if (mdAst.lang) {
const supportParsers = parserSynonyms(mdAst.lang.toLowerCase());
const parser = supportParsers?.includes(options.parser)
? options.parser
: supportParsers?.[0] || mdAst.lang;
result = await formatCode(result, intention, {
...options,
parser,
jsdocKeepUnParseAbleExampleIndent: true,
});
}
else if (options.jsdocPreferCodeFences || false) {
result = await formatCode(result, _intention, {
...options,
jsdocKeepUnParseAbleExampleIndent: true,
});
}
else {
_intention = intention + " ".repeat(4);
result = await formatCode(result, _intention, {
...options,
jsdocKeepUnParseAbleExampleIndent: true,
});
}
}
const addFence = options.jsdocPreferCodeFences || !!mdAst.lang;
result = addFence ? result : result.trimEnd();
return result
? addFence
? `\n\n${_intention}\`\`\`${mdAst.lang || ""}${result}\`\`\``
: `\n${result}`
: "";
}
if (mdAst.value === TABLE) {
if (parent) {
parent.costumeType = TABLE;
}
if (tables.length > 0) {
let result = tables?.[tableIndex] || "";
tableIndex++;
if (result) {
result = (await prettier.format(result, {
...options,
parser: "markdown",
})).trim();
}
return `${result
? `\n\n${intention}${result.split("\n").join(`\n${intention}`)}`
: mdAst.value}`;
}
}
if (mdAst.type === "break") {
return `\\\n`;
}
return (mdAst.value ||
mdAst.title ||
mdAst.alt ||
"");
}
async function stringyfy(mdAst, intention, parent) {
if (!Array.isArray(mdAst.children)) {
return stringifyASTWithoutChildren(mdAst, intention, parent);
}
return (await Promise.all(mdAst.children.map(async (ast, index) => {
switch (ast.type) {
case "listItem": {
let _listCount = `\n${intention}- `;
if (typeof mdAst.start === "number") {
const count = index + (mdAst.start ?? 1);
_listCount = `\n${intention}${count}. `;
}
const _intention = intention + " ".repeat(_listCount.length - 1);
const result = (await stringyfy(ast, _intention, mdAst)).trim();
return `${_listCount}${result}`;
}
case "list": {
let end = "";
if (tag !== DESCRIPTION &&
mdAst.type === "root" &&
index === mdAst.children.length - 1) {
end = "\n";
}
return `\n${await stringyfy(ast, intention, mdAst)}${end}`;
}
case "paragraph": {
const paragraph = await stringyfy(ast, intention, parent);
if (ast.costumeType === TABLE) {
return paragraph;
}
return `\n\n${paragraph
.split("\\\n")
.map((_paragraph) => {
const links = [];
_paragraph = _paragraph.replace(/{@(link|linkcode|linkplain)[\s](([^{}])*)}/g, (_, tag, link) => {
links.push(link);
return `{@${tag}${"_".repeat(link.length)}}`;
});
_paragraph = _paragraph.replace(/\s+/g, " ");
if (options.jsdocCapitalizeDescription &&
!TAGS_PEV_FORMATE_DESCRIPTION.includes(tag))
_paragraph = capitalizer(_paragraph);
if (options.jsdocDescriptionWithDot)
_paragraph = _paragraph.replace(/([\w\p{L}])$/u, "$1.");
let result = breakDescriptionToLines(_paragraph, printWidth, intention);
result = result.replace(/{@(link|linkcode|linkplain)([_]+)}/g, (original, tag, underline) => {
const link = links[0];
if (link.length === underline.length) {
links.shift();
return `{@${tag} ${link}}`;
}
return original;
});
return result;
})
.join("\\\n")}`;
}
case "strong": {
return `**${await stringyfy(ast, intention, mdAst)}**`;
}
case "emphasis": {
return `_${await stringyfy(ast, intention, mdAst)}_`;
}
case "heading": {
return `\n\n${intention}${"#".repeat(ast.depth)} ${await stringyfy(ast, intention, mdAst)}`;
}
case "link":
case "image": {
return `[${await stringyfy(ast, intention, mdAst)}](${ast.url})`;
}
case "linkReference": {
return `[${await stringyfy(ast, intention, mdAst)}][${ast.label}]`;
}
case "definition": {
return `\n\n[${ast.label}]: ${ast.url}`;
}
case "blockquote": {
const paragraph = await stringyfy(ast, "", mdAst);
return `\n\n> ${paragraph
.trim()
.replace(/(\n+)/g, `$1${intention}> `)}`;
}
}
return stringyfy(ast, intention, mdAst);
}))).join("");
}
let result = await stringyfy(rootAst, beginningSpace, null);
result = result.replace(/^[\s\n]+/g, "");
result = result.replace(/^([!]+\?)/g, "");
return result;
}
function breakDescriptionToLines(desContent, maxWidth, beginningSpace) {
let str = desContent.trim();
if (!str) {
return str;
}
let result = "";
while (str.length > maxWidth) {
let sliceIndex = str.lastIndexOf(" ", str.startsWith("\n") ? maxWidth + 1 : maxWidth);
if (sliceIndex <= beginningSpace.length)
sliceIndex = str.indexOf(" ", beginningSpace.length + 1);
if (sliceIndex === -1)
sliceIndex = str.length;
result += str.substring(0, sliceIndex);
str = str.substring(sliceIndex + 1);
if (str) {
str = `${beginningSpace}${str}`;
str = `\n${str}`;
}
}
result += str;
return `${beginningSpace}${result}`;
}
const stringify = async ({ name, description, type, tag }, tagIndex, finalTagsArray, options, maxTagTitleLength, maxTagTypeNameLength, maxTagNameLength) => {
let tagString = "\n";
if (tag === SPACE_TAG_DATA.tag) {
return tagString;
}
const { printWidth, jsdocSpaces, jsdocVerticalAlignment, jsdocDescriptionTag, tsdoc, useTabs, tabWidth, jsdocSeparateTagGroups, } = options;
const gap = " ".repeat(jsdocSpaces);
let tagTitleGapAdj = 0;
let tagTypeGapAdj = 0;
let tagNameGapAdj = 0;
let descGapAdj = 0;
if (jsdocVerticalAlignment && TAGS_VERTICALLY_ALIGN_ABLE.includes(tag)) {
if (tag)
tagTitleGapAdj += maxTagTitleLength - tag.length;
else if (maxTagTitleLength)
descGapAdj += maxTagTitleLength + gap.length;
if (type)
tagTypeGapAdj += maxTagTypeNameLength - type.length;
else if (maxTagTypeNameLength)
descGapAdj += maxTagTypeNameLength + gap.length;
if (name)
tagNameGapAdj += maxTagNameLength - name.length;
else if (maxTagNameLength)
descGapAdj = maxTagNameLength + gap.length;
}
const useTagTitle = tag !== DESCRIPTION || jsdocDescriptionTag;
if (useTagTitle) {
tagString += `@${tag}${" ".repeat(tagTitleGapAdj || 0)}`;
}
if (type) {
const getUpdatedType = () => {
if (!isDefaultTag(tag)) {
return `{${type}}`;
}
if (type === "[]")
return "[ ]";
if (type === "{}")
return "{ }";
const isAnObject = (value) => /^{.*[A-z0-9_]+ ?:.*}$/.test(value);
const fixObjectCommas = (objWithBrokenCommas) => objWithBrokenCommas.replace(/; ([A-z0-9_])/g, ", $1");
if (isAnObject(type)) {
return fixObjectCommas(type);
}
return type;
};
const updatedType = getUpdatedType();
tagString += gap + updatedType + " ".repeat(tagTypeGapAdj);
}
if (name)
tagString += `${gap}${name}${" ".repeat(tagNameGapAdj)}`;
if (tag === EXAMPLE && !tsdoc) {
const exampleCaption = description.match(/<caption>([\s\S]*?)<\/caption>/i);
if (exampleCaption) {
description = description.replace(exampleCaption[0], "");
tagString = `${tagString} ${exampleCaption[0]}`;
}
const beginningSpace = useTabs ? "\t" : " ".repeat(tabWidth);
const formattedExample = await formatCode(description, beginningSpace, options);
tagString += formattedExample
.replace(new RegExp(`^\\n${beginningSpace
.replace(/[\t]/g, "[\\t]")
.replace(/[^S\r\n]/g, "[^S\\r\\n]")}\\n`), "")
.trimEnd();
}
else if (description) {
let descriptionString = "";
if (useTagTitle)
tagString += gap + " ".repeat(descGapAdj);
if (TAGS_PEV_FORMATE_DESCRIPTION.includes(tag) ||
!TAGS_ORDER[tag]) {
descriptionString = description;
}
else {
const [, firstWord] = /^\s*(\S+)/.exec(description) || ["", ""];
const beginningSpace = tag === DESCRIPTION ||
([EXAMPLE, REMARKS, PRIVATE_REMARKS].includes(tag) && tsdoc)
? ""
: " ";
if ((tag !== DESCRIPTION &&
tagString.length + firstWord.length > printWidth) ||
[REMARKS, PRIVATE_REMARKS].includes(tag)) {
descriptionString =
`\n${beginningSpace}` +
(await formatDescription(tag, description, options, {
beginningSpace,
}));
}
else {
descriptionString = await formatDescription(tag, description, options, {
tagStringLength: tagString.length - 1,
beginningSpace,
});
}
}
if (jsdocSeparateTagGroups) {
descriptionString = descriptionString.trimEnd();
}
tagString += descriptionString.startsWith("\n")
? descriptionString.replace(/^\n[\s]+\n/g, "\n")
: descriptionString.trimStart();
}
tagString += descriptionEndLine({
tag,
isEndTag: tagIndex === finalTagsArray.length - 1,
});
return tagString;
};
const { name: nameTokenizer, tag: tagTokenizer, type: typeTokenizer, description: descriptionTokenizer, } = commentParser.tokenizers;
const getParser = (originalParse, parserName) => async function jsdocParser(text, parsersOrOptions, maybeOptions) {
let options = (maybeOptions ?? parsersOrOptions);
const prettierParse = findPluginByParser(parserName, options)?.parse || originalParse;
const ast = prettierParse(text, options);
options = {
...options,
printWidth: options.jsdocPrintWidth ?? options.printWidth,
};
const eol = options.endOfLine === "auto" ? detectEndOfLine(text) : options.endOfLine;
options = { ...options, endOfLine: "lf" };
await Promise.all(ast.comments.map(async (comment) => {
if (!isBlockComment(comment))
return;
const paramsOrder = getParamsOrders(text, comment);
const originalValue = comment.value;
comment.value = comment.value.replace(/^([*]+)/g, "*");
const commentString = `/*${comment.value.replace(/\r\n?/g, "\n")}*/`;
if (!/^\/\*\*[\s\S]+?\*\/$/.test(commentString))
return;
const parsed = commentParser.parse(commentString, {
spacing: "preserve",
tokenizers: [
tagTokenizer(),
(spec) => {
if (isDefaultTag(spec.tag)) {
return spec;
}
return typeTokenizer("preserve")(spec);
},
nameTokenizer(),
descriptionTokenizer("preserve"),
],
})[0];
comment.value = "";
if (!parsed) {
return;
}
normalizeTags(parsed);
convertCommentDescToDescTag(parsed);
const commentContentPrintWidth = getIndentationWidth(comment, text, options);
let maxTagTitleLength = 0;
let maxTagTypeLength = 0;
let maxTagNameLength = 0;
let tags = parsed.tags
.map(({ type, optional, ...rest }) => {
if (type) {
type = type.replace(/[=]$/, () => {
optional = true;
return "";
});
type = convertToModernType(type);
}
return {
...rest,
type,
optional,
};
});
tags = sortTags(tags, paramsOrder, options);
if (options.jsdocSeparateReturnsFromParam) {
tags = tags.flatMap((tag, index) => {
if (tag.tag === RETURNS && tags[index - 1]?.tag === PARAM) {
return [SPACE_TAG_DATA, tag];
}
return [tag];
});
}
if (options.jsdocAddDefaultToDescription) {
tags = tags.map(addDefaultValueToDescription);
}
tags = await Promise.all(tags
.map(assignOptionalAndDefaultToName)
.map(async ({ type, ...rest }) => {
if (type) {
type = await formatType(type, {
...options,
printWidth: commentContentPrintWidth,
});
}
return {
...rest,
type,
};
})).then((formattedTags) => formattedTags.map(({ type, name, description, tag, ...rest }) => {
const isVerticallyAlignAbleTags = TAGS_VERTICALLY_ALIGN_ABLE.includes(tag);
if (isVerticallyAlignAbleTags) {
maxTagTitleLength = Math.max(maxTagTitleLength, tag.length);
maxTagTypeLength = Math.max(maxTagTypeLength, type.length);
maxTagNameLength = Math.max(maxTagNameLength, name.length);
}
return {
type,
name,
description,
tag,
...rest,
};
}));
if (options.jsdocSeparateTagGroups) {
tags = tags.flatMap((tag, index) => {
const prevTag = tags[index - 1];
if (prevTag &&
prevTag.tag !== DESCRIPTION &&
prevTag.tag !== EXAMPLE &&
prevTag.tag !== SPACE_TAG_DATA.tag &&
tag.tag !== SPACE_TAG_DATA.tag &&
prevTag.tag !== tag.tag) {
return [SPACE_TAG_DATA, tag];
}
return [tag];
});
}
const filteredTags = tags.filter(({ description, tag }) => {
if (!description && TAGS_DESCRIPTION_NEEDED.includes(tag)) {
return false;
}
return true;
});
for (const [tagIndex, tagData] of filteredTags.entries()) {
const formattedTag = await stringify(tagData, tagIndex, filteredTags, { ...options, printWidth: commentContentPrintWidth }, maxTagTitleLength, maxTagTypeLength, maxTagNameLength);
comment.value += formattedTag;
}
comment.value = comment.value.trimEnd();
if (comment.value) {
comment.value = addStarsToTheBeginningOfTheLines(originalValue, comment.value, options);
}
if (eol === "cr") {
comment.value = comment.value.replace(/\n/g, "\r");
}
else if (eol === "crlf") {
comment.value = comment.value.replace(/\n/g, "\r\n");
}
}));
ast.comments = ast.comments.filter((comment) => !(isBlockComment(comment) && !comment.value));
return ast;
};
function sortTags(tags, paramsOrder, options) {
let canGroupNextTags = false;
let shouldSortAgain = false;
tags = tags
.reduce((tagGroups, cur) => {
if (tagGroups.length === 0 ||
(TAGS_GROUP_HEAD.includes(cur.tag) && canGroupNextTags)) {
canGroupNextTags = false;
tagGroups.push([]);
}
if (TAGS_GROUP_CONDITION.includes(cur.tag)) {
canGroupNextTags = true;
}
tagGroups[tagGroups.length - 1].push(cur);
return tagGroups;
}, [])
.flatMap((tagGroup, index, array) => {
tagGroup.sort((a, b) => {
if (paramsOrder &&
paramsOrder.length > 1 &&
a.tag === PARAM &&
b.tag === PARAM) {
const aIndex = paramsOrder.indexOf(a.name);
const bIndex = paramsOrder.indexOf(b.name);
if (aIndex > -1 && bIndex > -1) {
return aIndex - bIndex;
}
return 0;
}
return (getTagOrderWeight(a.tag, options) - getTagOrderWeight(b.tag, options));
});
if (array.length - 1 !== index) {
tagGroup.push(SPACE_TAG_DATA);
}
if (index > 0 &&
tagGroup[0]?.tag &&
!TAGS_GROUP_HEAD.includes(tagGroup[0].tag)) {
shouldSortAgain = true;
}
return tagGroup;
});
return shouldSortAgain ? sortTags(tags, paramsOrder, options) : tags;
}
function getTagOrderWeight(tag, options) {
if (tag === DESCRIPTION && !options.jsdocDescriptionTag) {
return -1;
}
let index;
if (options.jsdocTagsOrder?.[tag] !== undefined) {
index = options.jsdocTagsOrder[tag];
}
else {
index = TAGS_ORDER[tag];
}
return index === undefined ? TAGS_ORDER.other : index;
}
function isBlockComment(comment) {
return comment.type === "CommentBlock" || comment.type === "Block";
}
function getIndentationWidth(comment, text, options) {
const line = text.split(/\r\n?|\n/g)[comment.loc.start.line - 1];
let spaces = 0;
let tabs = 0;
for (let i = comment.loc.start.column - 1; i >= 0; i--) {
const c = line[i];
if (c === " ") {
spaces++;
}
else if (c === "\t") {
tabs++;
}
else {
break;
}
}
return options.printWidth - (spaces + tabs * options.tabWidth) - " * ".length;
}
const TAGS_ORDER_ENTRIES = Object.entries(TAGS_ORDER);
function normalizeTags(parsed) {
parsed.tags = parsed.tags.map(({ tag, type, name, description, default: _default, ...rest }) => {
tag = tag || "";
type = type || "";
name = name || "";
description = description || "";
_default = _default?.trim();
const tagSticksToType = tag.indexOf("{");
if (tagSticksToType !== -1 && tag[tag.length - 1] === "}") {
type = tag.slice(tagSticksToType + 1, -1) + " " + type;
tag = tag.slice(0, tagSticksToType);
}
tag = tag.trim();
const lower = tag.toLowerCase();
const tagIndex = TAGS_ORDER_ENTRIES.findIndex(([key]) => key.toLowerCase() === lower);
if (tagIndex >= 0) {
tag = TAGS_ORDER_ENTRIES[tagIndex][0];
}
else if (lower in TAGS_SYNONYMS) {
tag = TAGS_SYNONYMS[lower];
}
type = type.trim();
name = name.trim();
if (name && TAGS_NAMELESS.includes(tag)) {
description = `${name} ${description}`;
name = "";
}
if (type && TAGS_TYPELESS.includes(tag)) {
description = `{${type}} ${description}`;
type = "";
}
return {
tag,
type,
name,
description,
default: _default,
...rest,
};
});
}
function convertCommentDescToDescTag(parsed) {
let description = parsed.description || "";
parsed.description = "";
parsed.tags = parsed.tags.filter(({ description: _description, tag }) => {
if (tag.toLowerCase() === DESCRIPTION) {
if (_description.trim()) {
description += "\n\n" + _description;
}
return false;
}
else {
return true;
}
});
if (description) {
parsed.tags.unshift({
tag: DESCRIPTION,
description,
name: undefined,
type: undefined,
source: [],
optional: false,
problems: [],
});
}
}
function getParamsOrders(text, comment) {
try {
const lines = text.split("\n");
let commentEnd = 0;
for (let i = 0; i < comment.loc.end.line - 1; i++) {
commentEnd += lines[i].length + 1;
}
commentEnd += comment.loc.end.column;
const textAfterComment = text.slice(commentEnd);
const functionMatch = textAfterComment.match(/^\s*function\s+\w*\s*\(([^)]*)\)/);
if (functionMatch) {
const paramsString = functionMatch[1];
const params = paramsString
.split(",")
.map((param) => {
const trimmed = param.trim();
const colonIndex = trimmed.indexOf(":");
const paramName = colonIndex > -1 ? trimmed.slice(0, colonIndex) : trimmed;
return paramName.split(/\s+/)[0].replace(/[{}[\]]/g, "");
})
.filter((param) => param && param !== "...");
return params;
}
const arrowMatch = textAfterComment.match(/^\s*(?:const|let|var)\s+\w+\s*=\s*\(([^)]*)\)\s*=>/);
if (arrowMatch) {
const paramsString = arrowMatch[1];
const params = paramsString
.split(",")
.map((param) => {
const trimmed = param.trim();
const colonIndex = trimmed.indexOf(":");
const paramName = colonIndex > -1 ? trimmed.slice(0, colonIndex) : trimmed;
return paramName.split(/\s+/)[0].replace(/[{}[\]]/g, "");
})
.filter((param) => param && param !== "...");
return params;
}
const methodMatch = textAfterComment.match(/^\s*(\w+)\s*\(([^)]*)\)/);
if (methodMatch) {
const paramsString = methodMatch[2];
const params = paramsString
.split(",")
.map((param) => {
const trimmed = param.trim();
const colonIndex = trimmed.indexOf(":");
const paramName = colonIndex > -1 ? trimmed.slice(0, colonIndex) : trimmed;
return paramName.split(/\s+/)[0].replace(/[{}[\]]/g, "");
})
.filter((param) => param && param !== "...");
return params;
}
return undefined;
}
catch (error) {
return undefined;
}
}
function addDefaultValueToDescription(tag) {
if (tag.optional && tag.default) {
let { description } = tag;
description = description.replace(/(?:\s*Default\s+is\s+`.*?`\.?)+/g, "");
if (description && !/[.\n]$/.test(description)) {
description += ".";
}
description += ` Default is \`${tag.default}\``;
return {
...tag,
description: description.trim(),
};
}
else {
return tag;
}
}
function assignOptionalAndDefaultToName({ name, optional, default: default_, tag, type, source, description, ...rest }) {
if (isDefaultTag(tag)) {
const usefulSourceLine = source.find((x) => x.source.includes(`@${tag}`))?.source || "";
const tagMatch = usefulSourceLine.match(/@default(Value)? (\[.*]|{.*}|\(.*\)|'.*'|".*"|`.*`| \w+)( ((?!\*\/).+))?/);
const tagValue = tagMatch?.[2] || "";
const tagDescription = tagMatch?.[4] || "";
if (tagMatch) {
type = tagValue;
name = "";
description = tagDescription;
}
}
else if (optional) {
if (name) {
if (default_) {
name = `[${name}=${default_}]`;
}
else {
name = `[${name}]`;
}
}
else {
type = `${type} | undefined`;
}
}
return {
...rest,
tag,
name,
description,
optional,
type,
source,
default: default_,
};
}
const options = {
jsdocSpaces: {
name: "jsdocSpaces",
type: "int",
category: "jsdoc",
default: 1,
description: "How many spaces will be used to separate tag elements.",
},
jsdocDescriptionWithDot: {
name: "jsdocDescriptionWithDot",
type: "boolean",
category: "jsdoc",
default: false,
description: "Should dot be inserted at the end of description",
},
jsdocDescriptionTag: {
name: "jsdocDescriptionTag",
type: "boolean",
category: "jsdoc",
default: false,
description: "Should description tag be used",
},
jsdocVerticalAlignment: {
name: "jsdocVerticalAlignment",
type: "boolean",
category: "jsdoc",
default: false,
description: "Should tags, types, names and description be aligned",
},
jsdocKeepUnParseAbleExampleIndent: {
name: "jsdocKeepUnParseAbleExampleIndent",
type: "boolean",
category: "jsdoc",
default: false,
description: "Should unParseAble example (pseudo code or no js code) keep its indentation",
},
jsdocSingleLineComment: {
name: "jsdocSingleLineComment",
type: "boolean",
category: "jsdoc",
deprecated: "use jsdocCommentLineStrategy instead will be remove on v2",
default: true,
description: "Should compact single line comment",
},
jsdocCommentLineStrategy: {
name: "jsdocCommentLineStrategy",
type: "choice",
choices: [
{
value: "singleLine",
description: `Should compact single line comment, if possible`,
},
{
value: "multiline",
description: `Should compact multi line comment`,
},
{
value: "keep",
description: `Should keep original line comment`,
},
],
category: "jsdoc",
default: "singleLine",
description: "How comments line should be",
},
jsdocSeparateReturnsFromParam: {
name: "jsdocSeparateReturnsFromParam",
type: "boolean",
category: "jsdoc",
default: false,
description: "Add an space between last @param and @returns",
},
jsdocSeparateTagGroups: {
name: "jsdocSeparateTagGroups",
type: "boolean",
category: "jsdoc",
default: false,
description: "Add an space between tag groups",
},
jsdocCapitalizeDescription: {
name: "jsdocCapitalizeDescription",
type: "boolean",
category: "jsdoc",
default: true,
description: "Should capitalize first letter of description",
},
tsdoc: {
name: "tsdoc",
type: "boolean",
category: "jsdoc",
default: false,
description: "Should format as tsdoc",
},
jsdocPrintWidth: {
name: "jsdocPrintWidth",
type: "int",
category: "jsdoc",
default: undefined,
description: "If You don't set value to jsdocPrintWidth, the printWidth will be use as jsdocPrintWidth.",
},
jsdocAddDefaultToDescription: {
name: "jsdocAddDefaultToDescription",
type: "boolean",
category: "jsdoc",
default: true,
description: "Add Default value of a param to end description",
},
jsdocPreferCodeFences: {
name: "jsdocPreferCodeFences",
type: "boolean",
category: "jsdoc",
default: false,
description: `Prefer to render code blocks using "fences" (triple backticks). If not set, blocks without a language tag will be rendered with a four space indentation.`,
},
jsdocLineWrappingStyle: {
name: "jsdocLineWrappingStyle",
type: "choice",
choices: [
{
value: "greedy",
description: `Lines wrap as soon as they reach the print width`,
},
],
category: "jsdoc",
default: "greedy",
description: `Strategy for wrapping lines for the given print width. More options may be added in the future.`,
},
jsdocTagsOrder: {
name: "jsdocTagsOrder",
type: "string",
category: "jsdoc",
default: undefined,
description: "How many spaces will be used to separate tag elements.",
},
};
const defaultOptions = {
jsdocSpaces: options.jsdocSpaces.default,
jsdocPrintWidth: options.jsdocPrintWidth.default,
jsdocDescriptionWithDot: options.jsdocDescriptionWithDot.default,
jsdocDescriptionTag: options.jsdocDescriptionTag.default,
jsdocVerticalAlignment: options.jsdocVerticalAlignment.default,
jsdocKeepUnParseAbleExampleIndent: options.jsdocKeepUnParseAbleExampleIndent.default,
jsdocSingleLineComment: options.jsdocSingleLineComment.default,
jsdocCommentLineStrategy: options.jsdocCommentLineStrategy.default,
jsdocSeparateReturnsFromParam: options.jsdocSeparateReturnsFromParam.default,
jsdocSeparateTagGroups: options.jsdocSeparateTagGroups.default,
jsdocCapitalizeDescription: options.jsdocCapitalizeDescription.default,
jsdocAddDefaultToDescription: options.jsdocAddDefaultToDescription.default,
jsdocPreferCodeFences: options.jsdocPreferCodeFences.default,
tsdoc: options.tsdoc.default,
jsdocLineWrappingStyle: options.jsdocLineWrappingStyle.default,
jsdocTagsOrder: options.jsdocTagsOrder.default,
};
const parsers = {
get babel() {
const parser = parserBabel__default["default"].parsers.babel;
return mergeParsers(parser, "babel");
},
get "babel-flow"() {
const parser = parserBabel__default["default"].parsers["babel-flow"];
return mergeParsers(parser, "babel-flow");
},
get "babel-ts"() {
const parser = parserBabel__default["default"].parsers["babel-ts"];
return mergeParsers(parse