remark-lint-list-item-style
Version:
remark-lint rule to warn when list items violate a given style
103 lines • 5.3 kB
JavaScript
import { toString } from 'mdast-util-to-string';
import { lintRule as createLintRule } from 'unified-lint-rule';
import { generated as isGenerated } from 'unist-util-generated';
import { visit } from 'unist-util-visit';
const origin = 'remark-lint:list-item-style';
export const optionsCheckFirstWord = [false, 'capitalize', 'lowercase'];
export const optionsCheckListSpread = ['first', 'final', 'first-and-final', 'each'];
const remarkLintListItemStyle = createLintRule({
origin,
url: 'https://github.com/Xunnamius/unified-utils/tree/main/packages/remark-lint-list-item-style#readme'
}, function (tree, file, options = {}) {
const {
checkPunctuation,
checkFirstWord,
checkListSpread,
ignoredFirstWords
} = coerceToOptions(file, options);
visit(tree, node => {
if (!isGenerated(node) && node.type === 'list') {
const list = node;
list.children.forEach(listItem => {
if (!isGenerated(listItem)) {
if (checkPunctuation) {
if (!listItem.children.length) {
file.message('empty list item without punctuation is not allowed', listItem);
} else {
const targets = checkListSpread === 'each' ? listItem.children : checkListSpread === 'first' ? [listItem.children[0]] : checkListSpread === 'final' ? [listItem.children.at(-1)] : listItem.children.length > 1 ? [listItem.children[0], listItem.children.at(-1)] : [listItem.children[0]];
targets.forEach(child => {
if (child.type === 'paragraph' && child.children.length && !['image', 'imageReference'].includes(child.children.at(-1)?.type || '') || !['code', 'paragraph', 'html', 'list'].includes(child.type)) {
const chars = child.type === 'paragraph' && child.children.length === 1 && child.children.at(-1)?.type === 'inlineCode' ? [''] : [...toString(child)].filter(Boolean).slice(-2);
const punctuation = chars.at(-1) === '\uFE0F' ? chars.join('') : chars.at(-1) || '';
if (!checkPunctuation.some(regExp => !!punctuation.match(regExp))) {
file.message(`"${punctuation}" is not allowed to punctuate list item`, child);
}
}
});
}
}
if (checkFirstWord && typeof listItem.checked != 'boolean' && listItem.children.length) {
listItem.children.forEach(child => {
if (child.type === 'paragraph' && child.children.length && !['inlineCode', 'link', 'linkReference', 'image', 'imageReference'].includes(child.children[0]?.type) || !['code', 'paragraph', 'html', 'list'].includes(child.type)) {
const stringifiedChild = toString(child);
const isIgnored = ignoredFirstWords.some(regExp => !!stringifiedChild.match(regExp));
if (!isIgnored) {
const actual = stringifiedChild[0];
const expected = actual[checkFirstWord === 'capitalize' ? 'toUpperCase' : 'toLowerCase']();
if (expected != actual) {
file.message(`Inconsistent list item capitalization: "${actual}" should be "${expected}"`, child);
}
}
}
});
}
}
});
}
});
});
export default remarkLintListItemStyle;
function coerceToOptions(file, options) {
options = options || {};
if (!options || typeof options != 'object' || Array.isArray(options)) {
file.fail('Error: Bad configuration');
}
const checkPunctuation = 'checkPunctuation' in options ? options.checkPunctuation : ['(\\.|\\?|;|,|!|\\p{Emoji}\uFE0F|\\p{Emoji_Presentation})'];
const checkFirstWord = 'checkFirstWord' in options ? options.checkFirstWord : 'capitalize';
const ignoredFirstWords = 'ignoredFirstWords' in options ? options.ignoredFirstWords : [];
const checkListSpread = 'checkListSpread' in options ? options.checkListSpread : 'each';
const checkPunctuationRegExp = checkPunctuationToRegExp();
const ignoredFirstWordsRegExp = ignoredFirstWordsToRegExp();
if (!optionsCheckFirstWord.includes(checkFirstWord)) {
file.fail(`Error: Bad configuration checkFirstWord value "${checkFirstWord}"`);
}
if (!optionsCheckListSpread.includes(checkListSpread)) {
file.fail(`Error: Bad configuration checkListSpread value "${checkListSpread}"`);
}
return {
checkPunctuation: checkPunctuationRegExp,
checkFirstWord,
ignoredFirstWords: ignoredFirstWordsRegExp,
checkListSpread
};
function checkPunctuationToRegExp() {
if (checkPunctuation !== false && !Array.isArray(checkPunctuation)) {
file.fail(`Error: Bad configuration checkPunctuation value "${checkPunctuation}"`);
}
try {
return checkPunctuation ? checkPunctuation.map(r => new RegExp(r, 'gu')) : false;
} catch (error) {
file.fail(`Error: Bad configuration checkPunctuation RegExp: ${error}`);
}
}
function ignoredFirstWordsToRegExp() {
if (!Array.isArray(ignoredFirstWords)) {
file.fail(`Error: Bad configuration ignoredFirstWords value "${ignoredFirstWords}"`);
}
try {
return ignoredFirstWords.map(r => new RegExp(r, 'gu'));
} catch (error) {
file.fail(`Error: Bad configuration ignoredFirstWords RegExp: ${error}`);
}
}
}