prettier-plugin-jsdoc
Version:
Prettier plugin for format jsdoc and convert to standard Match with Visual studio and other IDE which support jsdoc.
285 lines (284 loc) • 12.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.jsdocParser = void 0;
const comment_parser_1 = __importDefault(require("comment-parser"));
const prettier_1 = require("prettier");
const type_1 = require("./type");
const tags_1 = require("./tags");
const roles_1 = require("./roles");
const EMPTY_LINE_SIGNATURE = "a2@1093NmY^5!~#sdEKHuhPOK*&baSIGNATURE1";
const NEW_LINE_START_THREE_SPACE_SIGNATURE = "l2@_0^3N)Y^5!~^sd*KHuh+O~*;vlSIGNATURE2";
const NEW_PARAGRAPH_START_THREE_SPACE_SIGNATURE = "l2@_0^s43fb64ds2Huh+O~*;vlSIGNATURE3";
/**
* @link https://prettier.io/docs/en/api.html#custom-parser-api}
*/
function jsdocParser(text, parsers, options) {
const babelTs = parsers["babel-ts"];
const ast = babelTs(text);
// Options
const gap = " ".repeat(options.jsdocSpaces);
const { printWidth = 80 } = options;
/**
* Control order of tags by weights. Smaller value brings tag higher.
*
* @param {String} tagTitle TODO
* @returns {Number} Tag weight
*/
function getTagOrderWeight(tagTitle) {
if (tagTitle === tags_1.DESCRIPTION && !options.jsdocDescriptionTag) {
return -1;
}
const index = options.jsdocTagsOrder.indexOf(tagTitle);
return index === -1
? options.jsdocTagsOrder.indexOf("other") || options.jsdocTagsOrder.length
: index;
}
ast.comments.forEach((comment) => {
const { loc: { start: { column }, }, } = comment;
// Parse only comment blocks
if (comment.type !== "CommentBlock")
return;
const commentString = `/*${comment.value}*/`;
// Check if this comment block is a JSDoc. Based on:
// https://github.com/jsdoc/jsdoc/blob/master/packages/jsdoc/plugins/commentsOnly.js
if (!commentString.match(/\/\*\*[\s\S]+?\*\//g))
return;
const parsed = comment_parser_1.default(commentString, {
dotted_names: false,
trim: false,
})[0];
comment.value = "";
convertCommentDescToDescTag(parsed);
let maxTagTitleLength = 0;
let maxTagTypeNameLength = 0;
let maxTagNameLength = 0;
parsed.tags
// Prepare tags data
.map(({ name, description, type, tag, source, optional, default: _default, ...restInfo }) => {
tag = tag && tag.trim().toLowerCase();
//@ts-ignore
tag = roles_1.TAGS_SYNONYMS[tag] || tag;
const isVerticallyAlignAbleTags = roles_1.TAGS_VERTICALLY_ALIGN_ABLE.includes(tag);
if (roles_1.TAGS_NAMELESS.includes(tag) && name) {
description = `${name} ${description}`;
name = "";
}
if (type) {
type = type_1.convertToModernArray(type);
type = type_1.formatType(type, options);
if (isVerticallyAlignAbleTags)
maxTagTypeNameLength = Math.max(maxTagTypeNameLength, type.length);
// Additional operations on name
if (name) {
// Optional tag name
if (optional) {
// Figure out if tag type have default value
if (_default) {
description += ` Default is \`${_default}\``;
}
name = `[${name}]`;
}
if (isVerticallyAlignAbleTags)
maxTagNameLength = Math.max(maxTagNameLength, name.length);
}
}
if (isVerticallyAlignAbleTags) {
maxTagTitleLength = Math.max(maxTagTitleLength, tag.length);
}
description = formatDescription(tag, description, options.jsdocDescriptionWithDot);
return {
...restInfo,
name,
description,
type,
tag,
source,
default: _default,
optional,
};
})
// Sort tags
.sort((a, b) => getTagOrderWeight(a.tag) - getTagOrderWeight(b.tag))
.filter(({ description, tag }) => {
if (!description && roles_1.TAGS_DESCRIPTION_NEEDED.includes(tag)) {
return false;
}
return true;
})
// Create final jsDoc string
.forEach(({ name, description, type, tag }, tagIndex, finalTagsArray) => {
let tagTitleGapAdj = 0;
let tagTypeGapAdj = 0;
let tagNameGapAdj = 0;
let descGapAdj = 0;
if (options.jsdocVerticalAlignment &&
roles_1.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;
}
let useTagTitle = tag !== tags_1.DESCRIPTION || options.jsdocDescriptionTag;
let tagString = "\n";
if (useTagTitle) {
try {
tagString += `@${tag}${" ".repeat(tagTitleGapAdj)}`;
}
catch (error) {
console.log(error);
}
}
if (type) {
tagString += gap + `{${type}}` + " ".repeat(tagTypeGapAdj);
}
if (name)
tagString += `${gap}${name}${" ".repeat(tagNameGapAdj)}`;
// Add description (complicated because of text wrap)
if (description && tag !== tags_1.EXAMPLE) {
if (useTagTitle)
tagString += gap + " ".repeat(descGapAdj);
if ([tags_1.MEMBEROF, tags_1.SEE].includes(tag)) {
// Avoid wrapping
tagString += description;
}
else {
// Wrap tag description
const beginningSpace = tag === tags_1.DESCRIPTION ? "" : " "; // google style guide space
const marginLength = tagString.length;
let maxWidth = printWidth - column - 3; // column is location of comment, 3 is ` * `
if (marginLength >= maxWidth) {
maxWidth = marginLength;
}
let resolveDescription = `${tagString}${description}`;
tagString = resolveDescription
.split(NEW_PARAGRAPH_START_THREE_SPACE_SIGNATURE)
.map((newParagraph) => {
return newParagraph
.split(EMPTY_LINE_SIGNATURE)
.map((paragraph) => {
paragraph = paragraph[0].toUpperCase() + paragraph.slice(1); // Capitalize
return paragraph
.split(NEW_LINE_START_THREE_SPACE_SIGNATURE)
.map((desContent) => {
desContent = desContent.trim();
if (!desContent) {
return desContent;
}
let result = "";
while (desContent.length > maxWidth) {
let sliceIndex = desContent.lastIndexOf(" ", maxWidth);
if (sliceIndex === -1)
sliceIndex = maxWidth;
result += desContent.substring(0, sliceIndex);
desContent = desContent.substring(sliceIndex + 1);
desContent = `\n${beginningSpace}${desContent}`;
}
result += desContent;
return result;
})
.join("\n ");
})
.join("\n\n");
})
.join("\n\n ");
// tagString = tagString.trim();
tagString = tagString ? `\n${tagString}` : tagString;
}
}
// Try to use prettier on @example tag description
if (tag === tags_1.EXAMPLE) {
try {
const formattedExample = prettier_1.format(description || "", options);
tagString += formattedExample.replace(/(^|\n)/g, "\n ");
tagString = tagString.slice(0, tagString.length - 3);
}
catch (err) {
tagString += "\n";
tagString += description
.split("\n")
.map((l) => ` ${options.jsdocKeepUnParseAbleExampleIndent ? l : l.trim()}`)
.join("\n");
}
}
// Add empty line after some tags if there is something below
tagString += descriptionEndLine({
description: tagString,
tag,
isEndTag: tagIndex === finalTagsArray.length - 1,
});
comment.value += tagString;
});
comment.value = addStarsToTheBeginningOfTheLines(comment.value);
});
return ast;
}
exports.jsdocParser = jsdocParser;
/**
* Trim, make single line with capitalized text. Insert dot if flag for it is
* set to true and last character is a word character
*
* @private
* @param {Boolean} insertDot Flag for dot at the end of text
*/
function formatDescription(tag, text, insertDot) {
text = text || "";
text = text.replace(/^[\W]/g, "");
text = text.trim();
if (!roles_1.TAGS_NEED_FORMAT_DESCRIPTION.includes(tag)) {
return text;
}
if (!text)
return text;
text = text.replace(/\n\n\s\s\s+/g, NEW_PARAGRAPH_START_THREE_SPACE_SIGNATURE); // Add a signature for new paragraph start with three space
text = text.replace(/\n\s\s\s+/g, NEW_LINE_START_THREE_SPACE_SIGNATURE); // Add a signature for new line start with three space
text = text.replace(/\n\n/g, EMPTY_LINE_SIGNATURE); // Add a signature for empty line and use that later
text = text.replace(/\s\s+/g, " "); // Avoid multiple spaces
text = text.replace(/\n/g, " "); // Make single line
if (insertDot)
text = text.replace(/(\w)(?=$)/g, "$1."); // Insert dot if needed
text = text[0].toUpperCase() + text.slice(1); // Capitalize
return text || "";
}
function convertCommentDescToDescTag(parsed) {
if (!parsed.description) {
return;
}
const Tag = parsed.tags.find(({ tag }) => tag.toLowerCase() === tags_1.DESCRIPTION);
let { description = "" } = Tag || {};
description += parsed.description;
if (Tag) {
Tag.description = description;
}
else {
parsed.tags.push({ tag: tags_1.DESCRIPTION, description });
}
}
function descriptionEndLine({ description, tag, isEndTag, }) {
if (description.trim().length < 0 || isEndTag) {
return "";
}
if ([tags_1.DESCRIPTION, tags_1.EXAMPLE, tags_1.TODO].includes(tag)) {
return "\n";
}
return "";
}
function addStarsToTheBeginningOfTheLines(comment) {
if (numberOfAStringInString(comment.trim(), "\n") === 0) {
return `* ${comment.trim()} `;
}
return `*${comment.replace(/((?!\n$)\n)/g, "\n * ")}\n `;
}
function numberOfAStringInString(string, search) {
return (string.match(new RegExp(search, "g")) || []).length;
}