@homer0/prettier-plugin-jsdoc
Version:
A Prettier plugin to format JSDoc blocks
182 lines (166 loc) • 6.02 kB
JavaScript
const R = require('ramda');
const { ensureArray, joinIfNotEmpty, appendIfNotPresent } = require('./utils');
const { get, provider } = require('./app');
/**
* @typedef {import('../types').PJPDescriptionTagOptions} PJPDescriptionTagOptions
* @typedef {import('../types').CommentTag} CommentTag
* @typedef {import('../types').CommentBlock} CommentBlock
*/
/**
* @callback FindTagHandlerFn
* @param {T} acc The reducer accumulator.
* @param {CommentTag} tag The current tag for the iteration.
* @param {number} index The index of the tag in the list.
* @returns {T}
* @template T
*/
/**
* Creates a reducer that finds the last index of a tag on a list and saves it on the
* accumulator using a custom property.
* Creates a reducer that will try to match a tag (or a list of them) and execute a
* function if it matches, or another if it doesn't.
*
* @callback FindTagFn
* @param {string | string[]} targetTag The name of the tag or tags the
* function should find.
* @param {FindTagHandlerFn<any>} matchHandlerFn The function called when the current
* tag matches with the one(s) the
* reducer is looking for.
* @param {FindTagHandlerFn<any>} unmatchHandlerFn The function called when the current
* tag doesn't match with the one(s) the
* reducer is looking for.
* @returns {Function}
*/
/**
* @type {FindTagFn}
*/
const findTag = R.curry((targetTag, matchHandlerFn, unmatchHandlerFn, step) => {
const targetTags = get(ensureArray)(targetTag);
return (acc, tag, index) => {
const nextAcc = targetTags.includes(tag.tag)
? matchHandlerFn(acc, tag, index)
: unmatchHandlerFn(acc, tag, index);
return step(nextAcc, tag, index);
};
});
/**
* @typedef {Object} ProcessTagAccumulator
* @property {string[]} parts All the description "parts" the reducer has found.
* One can be from the block body, other from a
* description tag, other from a typedef tag, etc.
* @property {CommentTag[]} tags The list of tags processed by the reducer.
* @property {number} tagIndex The index, if found, of a description tag.
*/
/**
* Generates a reducer handler for specific tags.
*
* @param {string | string[]} descriptionProperty
* The property or properties that make the description of a tag.
* @param {boolean} saveIndex
* Whether or not the tag index should be saved as the accumulator `tagIndex`
* property.
* @returns {FindTagHandlerFn<ProcessTagAccumulator>}
*/
const processTag = (descriptionProperty, saveIndex = false) => {
const descriptionProperties = get(ensureArray)(descriptionProperty);
const generateDescription = R.compose(
get(joinIfNotEmpty)(' '),
R.values,
R.pick(descriptionProperties),
);
const emptyProps = descriptionProperties.reduce(
(acc, prop) => ({ ...acc, [prop]: '' }),
{},
);
return (acc, tag, index) =>
R.evolve(
{
parts: get(appendIfNotPresent)(generateDescription(tag)),
tags: R.append(R.mergeRight(tag, emptyProps)),
tagIndex: R.when(R.always(saveIndex), R.always(index)),
},
acc,
);
};
/**
* Finds all possible descriptions of a block (body, description tag, after a definition,
* etc.), puts them together, and, depending on the options, it adds it on the body or a
* description tag.
*
* @callback FormatDescriptionFn
* @param {CommentBlock} block The block to format.
* @param {PJPDescriptionTagOptions} options The options that will tell the method how to
* handle the description.
* @returns {CommentBlock}
*/
/**
* @type {FormatDescriptionFn}
*/
const formatDescription = R.curry((block, options) => {
/**
* A handler for when a tag wasn't match; it just pushes it to the accumulator.
*
* @type {FindTagHandlerFn<ProcessTagAccumulator>}
*/
const pushTag = (acc, tag) => R.evolve({ tags: R.append(tag) }, acc);
const useFindTag = get(findTag);
const useProcessTag = get(processTag);
const { parts, tags, tagIndex } = block.tags.reduce(
R.compose(
useFindTag(
['typedef', 'callback', 'function'],
useProcessTag('description'),
R.identity,
),
useFindTag('description', useProcessTag(['name', 'description'], true), R.identity),
useFindTag(['description', 'typedef', 'callback', 'function'], R.identity, pushTag),
)(R.identity),
{
parts: [block.description],
tags: [],
tagIndex: -1,
},
);
const description = get(joinIfNotEmpty)('\n\n', parts);
let blockDescription = block.description;
let blockTags = tags;
if (options.jsdocAllowDescriptionTag) {
if (tagIndex > -1) {
blockDescription = '';
blockTags = R.adjust(
tagIndex,
R.evolve({
description: R.always(description),
}),
)(blockTags);
} else if (options.jsdocUseDescriptionTag) {
blockDescription = '';
const descriptionTag = {
tag: 'description',
name: '',
description,
type: '',
};
blockTags = R.ifElse(
R.pipe(R.prop('length'), R.gt(R.__, 1)),
R.insert(1, descriptionTag),
R.append(descriptionTag),
)(blockTags);
} else {
blockDescription = description;
}
} else {
if (tagIndex > -1) {
blockTags = R.remove(tagIndex, 1, blockTags);
}
blockDescription = description;
}
return {
description: blockDescription,
tags: blockTags,
};
});
module.exports.formatDescription = formatDescription;
module.exports.findTag = findTag;
module.exports.processTag = processTag;
module.exports.provider = provider('formatDescription', module.exports);