UNPKG

@homer0/prettier-plugin-jsdoc

Version:
422 lines (382 loc) 12.6 kB
const R = require('ramda'); const { get, provider } = require('./app'); /** * @typedef {import('../types').CommentTag} CommentTag */ /** * Ensures a given object is an array. * * @param {T | T[]} obj The object to validate. * @returns {T[]} * @template T */ const ensureArray = (obj) => R.unless(R.is(Array), R.of(Array), obj); /** * Creates a reducer that finds the last index of a tag on a list and saves it on the * accumulator using a custom property. * * @callback FindTagIndexFn * @param {string | string[]} targetTag The name of the tag or tags the function should * find. * @param {string} propName The name of the property that will be used for * the accumulator. * @returns {Object.<string, number>} */ /** * @type {FindTagIndexFn} */ const findTagIndex = R.curry((targetTag, propName, step) => { const targetTags = get(ensureArray)(targetTag); return (acc, tag, index) => { const nextAcc = targetTags.includes(tag.tag) ? R.assocPath([propName], index, acc) : acc; return step(nextAcc, tag, index); }; }); /** * Checks if a tag is of an specified type (or types). * * @callback IsTagFn * @param {string | string[]} targetTag The name of the tag or tags the function should * validate against. * @param {CommentTag} tag The tag to validate. * @returns {boolean} */ /** * @type {IsTagFn} */ const isTag = R.curry((targetTag, tag) => { const targetTags = get(ensureArray)(targetTag); return R.propSatisfies(R.includes(R.__, targetTags), 'tag', tag); }); /** * Adds an item to a list, only if it wasn't already present. * * @callback AppendIfNotPresentFn * @param {*} item The item to add. * @param {Array} list The list where the item should be added. * @returns {Array} */ /** * @type {AppendIfNotPresentFn} */ const appendIfNotPresent = R.curry((item, list) => R.unless(R.includes(item), R.append(item), list), ); /** * Takes a list of strings, filters out those that are empty and then joins them together. * * @callback JoinIfNotEmptyFn * @param {string} glue The string that will be added between the items on the final * result. * @param {string[]} str The list of strings to join. * @returns {string} */ /** * @type {JoinIfNotEmptyFn} */ const joinIfNotEmpty = R.curry((glue, str) => R.pipe(R.reject(R.isEmpty), R.join(glue))(str), ); /** * Replaces the last item on an array. * * @callback ReplaceLastItemFn * @param {*} item The "new last item". * @param {Array} list The list where the item will be replaced. * @returns {Array} */ /** * @type {ReplaceLastItemFn} */ const replaceLastItem = R.curry((item, list) => R.compose(R.append(item), R.dropLast(1))(list), ); /** * Validates that the `length` of an object is a positive number. * * @param {*} item The item to validate. * @returns {boolean} */ const hasItems = (item) => R.compose(R.gt(R.__, 0), R.length)(item); /** * Validates if a string matches a regular expression. This utility function exists * because `R.match` returns an array even if there are no matches, so the call to * `R.match` has to be composed with a function to validate the result `.length`. * * @callback IsMatchFn * @param {RegExp} expression The regular expression the string has to match. * @param {string} str The string to validate. * @returns {boolean} */ /** * @type {IsMatchFn} */ const isMatch = R.curry((expression, str) => R.compose(get(hasItems), R.match(expression))(str), ); /** * This is a utility function to make regular expression replacements where a group can * capture part of other group. The function will replace the text and it won't stop until * there are no matches. * * @callback ReplaceAdjacentFn * @param {RegExp} expression The expression to match and replace. * @param {string} replacement The replacement text. * @param {string} text The target text. * @returns {string} */ /** * @type {ReplaceAdjacentFn} */ const replaceAdjacent = R.curry((expression, replacement, text) => { let useText = text; let match = useText.match(expression); while (match) { useText = useText.replace(expression, replacement); match = useText.match(expression); } return useText; }); /** * Depending on `useDot`, this function will ensure that the type name and the generics * for a target type are separated, or not, with a dot. * For the use of generics, JSDoc recommends the use a dot before listing them (i.e. * `Array.<string>`), but that is unnecessary if you are using JSDoc for TypeScript * annotations. * * @callback ReplaceDotOnTypeGeneric * @param {string} targetType The type that should/shouldn't have a dot before generics. * @param {boolean} useDot Whether or not the dots should be present. * @param {string} type The actual type where the dots will be added or removed. * @returns {string} */ /** * @type {ReplaceDotOnTypeGeneric} */ const replaceDotOnTypeGeneric = R.curry((targetType, useDot, type) => { const useReplaceAdjacent = get(replaceAdjacent); return R.ifElse( R.always(useDot), useReplaceAdjacent(new RegExp(`([^\\w]|^)(${targetType})\\s*<`, 'i'), '$1$2.<'), useReplaceAdjacent(new RegExp(`([^\\w]|^)(${targetType})\\s*\\.\\s*<`, 'i'), '$1$2<'), )(type); }); /** * Capitalizes a string. * * @param {string} str The string to capitalize. * @returns {string} */ const capitalize = (str) => R.compose(R.join(''), R.juxt([R.compose(R.toUpper, R.head), R.tail]))(str); /** * Gets the item of an item of a list or a fallback value. * * @callback GetIndexOrFallbackFn * @param {Array} list The list where the function will look for the item. * @param {number} fallback The fallback index in case one is not found for the item. * @param {*} item The item to look for. * @returns {number} */ /** * @type {GetIndexOrFallbackFn} */ const getIndexOrFallback = R.curry((list, fallback, item) => R.compose(R.when(R.equals(-1), R.always(fallback)), R.indexOf(item))(list), ); /** * The predicate function that will be used by {@link LimitAdjacentRepetitionsFn} in order * to validate the items. * * @callback LimitAdjacentRepetitionsPredFn * @param {*} item The list item to validate. * @returns {boolean} */ /** * Formats a list in order to remove items repeated items next to each other. * * @callback LimitAdjacentRepetitionsFn * @param {LimitAdjacentRepetitionsPredFn} pred The function to validate if an item * should be considered and start the * count. * @param {number} limit How many times an item that was * validated with predicate function can be * adjacently repeated. * @param {Array} list The list to format. * @example * * limitAdjacentRepetitions(R.equals('\n'), 1, ['hello', '\n', '\n', 'world']); * // ['hello', '\n', 'world'] * */ /** * @type {LimitAdjacentRepetitionsFn} */ const limitAdjacentRepetitions = R.curry((pred, limit, list) => R.compose( R.prop('list'), R.reduce( (acc, item) => { if (pred(item)) { const newCount = acc.count + 1; if (newCount <= limit) { acc.count = newCount; acc.list.push(item); } } else { acc.count = 0; acc.list.push(item); } return acc; }, { count: 0, list: [], }, ), )(list), ); /** * Checks if an object was a specific property and is not _empty_. * * @callback HasValidPropertyFn * @param {string} property The name of the property to validate. * @param {Object} obj The object where the property will be validated. * @returns {boolean} */ /** * @type {HasValidPropertyFn} */ const hasValidProperty = R.curry((property, obj) => R.propSatisfies(R.complement(R.either(R.isEmpty, R.isNil)), property)(obj), ); /** * Adds a prefix on all the lines from a text. * * @callback PrefixLinesFn * @param {string} prefix The prefix to add on every line. * @param {string} text The target text that will be prefixed. * @returns {string} */ /** * @type {PrefixLinesFn} */ const prefixLines = R.curry((prefix, text) => R.compose(R.join('\n'), R.map(R.concat(prefix)), R.split('\n'), R.trim())(text), ); /** * Splits the lines of a text and removes the empty ones. * * @callback SplitLinesAndCleanFn * @param {string | RegExp} splitter The string or expression to use on `String.split`. * @param {string} text The text to split. * @returns {string[]} */ /** * @type {SplitLinesAndCleanFn} */ const splitLinesAndClean = R.curry((splitter, text) => R.compose(R.reject(R.isEmpty), R.map(R.trim), R.split(splitter))(text), ); /** * Validates if a text is a valid URL. * * @param {string} text The text to validate. * @returns {boolean} */ const isURL = (text) => isMatch( /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/i, text, ); /** * Validates whether a text is Markdown table row (starts and ends with a pipe). * * @param {string} text The text to validate. * @returns {boolean} */ const isTableRow = (text) => isMatch(/^\s*\|.*?\|\s*$/, text); /** * Ensures a text starts with an uppercase and ends with a period. * * @param {string} text The text to format. * @returns {string} */ const ensureSentence = (text) => R.when( R.allPass([R.complement(get(isURL)), get(isMatch)(/[\w\.]\s*$/)]), R.compose( R.replace(/(\.)?(\s*)$/, (full, dot, padding) => `.${padding}`), R.replace( /^(\s*)(\w)/, (full, padding, letter) => `${padding}${letter.toUpperCase()}`, ), ), text, ); /** * A version of Rambda's `compose` that can handle promises. * * @param {...*} args The list of functions to compose. * @returns {*} * @see https://gist.github.com/ehpc/2a524b78729ee6b4e8111f89c66d7ff5 */ const composeWithPromise = (...args) => R.composeWith((f, val) => { if (val && val.then) { return val.then(f); } return f(val); })(args); /** * @callback ReduceWithPromiseFn * @param {TItem} item The item to process. * @returns {Promise<TOutput>} * @template TItem The type of the items on the list. * @template TOutput The type of the item that will be returned. */ /** * A utility function that will process a list of items and return a promise with the * results. * The idea of this function is to replace a `for` loop with `await` inside. * * @param {TItem[]} items The list of items to process. * @param {ReduceWithPromiseFn<TItem, TOutput>} fn The function that will process each * item. * @returns {Promise<TOutput[]>} * @template TItem The type of the items on the list. * @template TOutput The type of the item that will be returned by the reducer. */ const reduceWithPromise = (items, fn) => items.reduce( (accPromise, item) => accPromise.then(async (acc) => { const result = await fn(item); acc.push(result); return acc; }), Promise.resolve([]), ); module.exports.ensureArray = ensureArray; module.exports.findTagIndex = findTagIndex; module.exports.isTag = isTag; module.exports.appendIfNotPresent = appendIfNotPresent; module.exports.joinIfNotEmpty = joinIfNotEmpty; module.exports.replaceLastItem = replaceLastItem; module.exports.hasItems = hasItems; module.exports.isMatch = isMatch; module.exports.replaceDotOnTypeGeneric = replaceDotOnTypeGeneric; module.exports.capitalize = capitalize; module.exports.getIndexOrFallback = getIndexOrFallback; module.exports.limitAdjacentRepetitions = limitAdjacentRepetitions; module.exports.hasValidProperty = hasValidProperty; module.exports.prefixLines = prefixLines; module.exports.splitLinesAndClean = splitLinesAndClean; module.exports.isURL = isURL; module.exports.isTableRow = isTableRow; module.exports.ensureSentence = ensureSentence; module.exports.composeWithPromise = composeWithPromise; module.exports.reduceWithPromise = reduceWithPromise; module.exports.provider = provider('utils', module.exports);