UNPKG

remark-lint-list-item-style

Version:
103 lines 5.3 kB
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}`); } } }