UNPKG

prettier-plugin-jsdoc

Version:
1,290 lines (1,281 loc) 52.2 kB
(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