UNPKG

prettier-plugin-jsdoc

Version:
399 lines 14.7 kB
import { parse, tokenizers } from "comment-parser"; import { addStarsToTheBeginningOfTheLines, convertToModernType, formatType, detectEndOfLine, findPluginByParser, isDefaultTag, } from "./utils.js"; import { DESCRIPTION, PARAM, RETURNS, EXAMPLE } from "./tags.js"; import { TAGS_DESCRIPTION_NEEDED, TAGS_GROUP_HEAD, TAGS_GROUP_CONDITION, TAGS_NAMELESS, TAGS_ORDER, TAGS_SYNONYMS, TAGS_TYPELESS, TAGS_VERTICALLY_ALIGN_ABLE, } from "./roles.js"; import { stringify } from "./stringify.js"; import { SPACE_TAG_DATA } from "./tags.js"; const { name: nameTokenizer, tag: tagTokenizer, type: typeTokenizer, description: descriptionTokenizer, } = tokenizers; export 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 = 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_, }; } //# sourceMappingURL=parser.js.map