Skip to content

Commit

Permalink
feat(prettier-plugin-jsdoc): detect examples' captions
Browse files Browse the repository at this point in the history
  • Loading branch information
homer0 committed Oct 25, 2020
1 parent 41952ab commit b7a29b1
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 43 deletions.
67 changes: 48 additions & 19 deletions src/fns/prepareExampleTag.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
const { format } = require('prettier');
const R = require('ramda');
const { isTag } = require('./utils');
const { isTag, prefixLines, splitLinesAndClean } = require('./utils');

/**
* @typedef {import('../types').PrettierOptions} PrettierOptions
* @typedef {import('../types').CommentTag} CommentTag
* @typedef {import('../types').CommentTagExample} CommentTagExample
*/

/**
* Attempts to format the code inside an example tag using Prettier.
* Attempts to format a example code using Prettier.
*
* @callback FormatExampleTagFn
* @param {PrettierOptions} options The options sent to the plugin, needed for the formatter.
* @param {CommentTag} tag The tag to format.
* @returns {CommentTag}
*/

/**
* @type {FormatExampleTagFn}
* @param {string} example The example code.
* @returns {string}
*/
const formatExampleTag = R.curry((options, tag) => {
const example = `${tag.name} ${tag.description}`;
const formatExample = (options, example) => {
let code;
let indent;
try {
Expand All @@ -32,18 +27,52 @@ const formatExampleTag = R.curry((options, tag) => {
}

if (indent) {
code = R.compose(
R.join('\n'),
R.map(R.concat(' '.repeat(options.tabWidth))),
R.split('\n'),
R.trim(),
)(code);
code = prefixLines(' '.repeat(options.tabWidth), code);
}

return code;
};

/**
* Takes an example code block that may or may not contain multiple `<caption>` blocks and split
* them into a list of examples, each one with its `code` and `caption` properties.
*
* @param {PrettierOptions} options The options sent to the plugin, needed for the formatter.
* @param {string} example The example code.
* @returns {CommentTagExample[]}
*/
const splitExamples = (options, example) => R.compose(
R.map(R.compose(
([caption, code]) => ({
caption,
code: formatExample(options, code),
}),
splitLinesAndClean(/<\s*\/\s*caption\s*>/i),
)),
splitLinesAndClean(/<\s*caption\s*>/i),
)(example);

/**
* Attempts to format the code inside an example tag using Prettier.
*
* @callback FormatExampleTagFn
* @param {PrettierOptions} options The options sent to the plugin, needed for the formatter.
* @param {CommentTag} tag The tag to format.
* @returns {CommentTag}
*/

/**
* @type {FormatExampleTagFn}
*/
const formatExampleTag = R.curry((options, tag) => {
const examples = tag.description.match(/<\s*caption\s*>/i) ?
splitExamples(options, tag.description) :
[{ code: formatExample(options, tag.description) }];

return {
...tag,
name: '',
description: code,
description: '',
examples,
};
});

Expand Down
39 changes: 39 additions & 0 deletions src/fns/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,43 @@ const hasValidProperty = R.curry((property, obj) => R.propSatisfies(
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));

module.exports.ensureArray = ensureArray;
module.exports.findTagIndex = findTagIndex;
module.exports.isTag = isTag;
Expand All @@ -321,3 +358,5 @@ module.exports.capitalize = capitalize;
module.exports.getIndexOrFallback = getIndexOrFallback;
module.exports.limitAdjacentRepetitions = limitAdjacentRepetitions;
module.exports.hasValidProperty = hasValidProperty;
module.exports.prefixLines = prefixLines;
module.exports.splitLinesAndClean = splitLinesAndClean;
30 changes: 21 additions & 9 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,29 @@
// Parser
// =========================================

/**
* @typedef {Object} CommentTagExample
* @property {string} code The actual code of the example.
* @property {string} [caption] The caption text to show above the example.
*/

/**
* @typedef {Object} CommentTag
* @property {string} tag The name of the tag.
* @property {string} type The type of what the tag represents, without the curly
* brackets.
* @property {string} name The name of what the tag represents.
* @property {string} description The description of what the tag represents.
* @property {boolean} optional Whether or not what the tag represents is optional.
* @property {string} [default] The default value of what the tag represents.
* @property {boolean} [descriptionParagrah] If `true`, it means that the description was originally
* below the tag line.
* @property {string} tag The name of the tag.
* @property {string} type The type of what the tag represents,
* without the curly brackets.
* @property {string} name The name of what the tag represents.
* @property {string} description The description of what the tag
* represents.
* @property {boolean} optional Whether or not what the tag represents is
* optional.
* @property {string} [default] The default value of what the tag
* represents.
* @property {boolean} [descriptionParagrah] If `true`, it means that the description
* was originally below the tag line.
* @property {CommentTagExample[]} [examples] A list of examples associated to the tag.
* Normally, this would only be present for
* for `example` tags.
*/

/**
Expand Down
101 changes: 86 additions & 15 deletions test/unit/fns/prepareExampleTag.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,14 @@ describe('prepareExampleTag', () => {
format.mockImplementationOnce(() => prettierResponse);
const input = {
tag: 'example',
name: 'const',
description: 'x = \'something\';',
description: 'const x = \'something\';',
};
const output = {
tag: 'example',
name: '',
description: prettierResponse,
description: '',
examples: [{
code: prettierResponse,
}],
};
const options = {
semi: true,
Expand All @@ -50,7 +51,7 @@ describe('prepareExampleTag', () => {
// Then
expect(result).toEqual(output);
expect(format).toHaveBeenCalledTimes(1);
expect(format).toHaveBeenCalledWith(`${input.name} ${input.description}`, options);
expect(format).toHaveBeenCalledWith(`${input.description}`, options);
});

it('should indent formatted text', () => {
Expand All @@ -59,13 +60,14 @@ describe('prepareExampleTag', () => {
format.mockImplementationOnce(() => prettierResponse);
const input = {
tag: 'example',
name: 'const',
description: 'x = \'something\';',
description: 'const x = \'something\';',
};
const output = {
tag: 'example',
name: '',
description: ` ${prettierResponse}`,
description: '',
examples: [{
code: ` ${prettierResponse}`,
}],
};
const options = {
jsdocIndentFormattedExamples: true,
Expand All @@ -77,7 +79,7 @@ describe('prepareExampleTag', () => {
// Then
expect(result).toEqual(output);
expect(format).toHaveBeenCalledTimes(1);
expect(format).toHaveBeenCalledWith(`${input.name} ${input.description}`, options);
expect(format).toHaveBeenCalledWith(`${input.description}`, options);
});

it('should indent unformatted text', () => {
Expand All @@ -87,13 +89,14 @@ describe('prepareExampleTag', () => {
});
const input = {
tag: 'example',
name: 'const',
description: 'x = \'something\';',
description: 'const x = \'something\';',
};
const output = {
tag: 'example',
name: '',
description: ` ${input.name} ${input.description}`,
description: '',
examples: [{
code: ` ${input.description}`,
}],
};
const options = {
jsdocIndentUnformattedExamples: true,
Expand All @@ -105,6 +108,74 @@ describe('prepareExampleTag', () => {
// Then
expect(result).toEqual(output);
expect(format).toHaveBeenCalledTimes(1);
expect(format).toHaveBeenCalledWith(`${input.name} ${input.description}`, options);
expect(format).toHaveBeenCalledWith(`${input.description}`, options);
});

it('should detect an example caption', () => {
// Given
const prettierResponse = 'prettier-response';
format.mockImplementationOnce(() => prettierResponse);
const input = {
tag: 'example',
description: '<caption>Some caption</caption>\nconst x = \'something\';',
};
const output = {
tag: 'example',
description: '',
examples: [{
caption: 'Some caption',
code: ` ${prettierResponse}`,
}],
};
const options = {
jsdocIndentFormattedExamples: true,
tabWidth: 2,
};
let result = null;
// When
result = prepareExampleTag(input, options);
// Then
expect(result).toEqual(output);
expect(format).toHaveBeenCalledTimes(1);
expect(format).toHaveBeenCalledWith('const x = \'something\';', options);
});

it('should detect multiple captions', () => {
// Given
const prettierResponse = 'prettier-response';
format.mockImplementationOnce(() => prettierResponse);
format.mockImplementationOnce(() => prettierResponse);
const input = {
tag: 'example',
description: [
'<caption>\nThe first\ncaption</caption>',
'const x = \'fist example\';',
'<caption>The second\ncaption\n</caption>',
'const y = \'second example\';',
].join('\n'),
};
const output = {
tag: 'example',
description: '',
examples: [
{
caption: 'The first\ncaption',
code: prettierResponse,
},
{
caption: 'The second\ncaption',
code: prettierResponse,
},
],
};
const options = {};
let result = null;
// When
result = prepareExampleTag(input, options);
// Then
expect(result).toEqual(output);
expect(format).toHaveBeenCalledTimes(2);
expect(format).toHaveBeenNthCalledWith(1, 'const x = \'fist example\';', options);
expect(format).toHaveBeenNthCalledWith(2, 'const y = \'second example\';', options);
});
});

0 comments on commit b7a29b1

Please sign in to comment.