UNPKG

remark-lint-checkbox-character-style

Version:

remark-lint rule to check list item checkbox characters

292 lines (274 loc) 8.74 kB
/** * remark-lint rule to warn when list item checkboxes violate a given * style. * * ## What is this? * * This package checks the character used in checkboxes. * * ## When should I use this? * * You can use this package to check that the style of GFM tasklists is * consistent. * Task lists are a GFM feature enabled with * [`remark-gfm`][github-remark-gfm]. * * ## API * * ### `unified().use(remarkLintCheckboxCharacterStyle[, options])` * * Warn when list item checkboxes violate a given style. * * ###### Parameters * * * `options` ([`Options`][api-options], default: `'consistent'`) * — either preferred values or whether to detect the first styles * and warn for further differences * * ###### Returns * * Transform ([`Transformer` from `unified`][github-unified-transformer]). * * ### `Options` * * Configuration (TypeScript type). * * ###### Type * * ```ts * type Options = Styles | 'consistent' * ``` * * ### `Styles` * * Styles (TypeScript type). * * ###### Fields * * * `checked` (`'X'`, `'x'`, or `'consistent'`, default: `'consistent'`) * — preferred style to use for checked checkboxes * * `unchecked` (`'␉'` (a tab), `'␠'` (a space), or `'consistent'`, default: * `'consistent'`) * — preferred style to use for unchecked checkboxes * * ## Recommendation * * It’s recommended to set `options.checked` to `'x'` (a lowercase X) as it * prevents an extra keyboard press and `options.unchecked` to `'␠'` (a space) * to make all checkboxes align. * * ## Fix * * [`remark-stringify`][github-remark-stringify] formats checked checkboxes * using `'x'` (lowercase X) and unchecked checkboxes using `'␠'` (a space). * * [api-options]: #options * [api-remark-lint-checkbox-character-style]: #unifieduseremarklintcheckboxcharacterstyle-options * [api-styles]: #styles * [github-remark-gfm]: https://github.com/remarkjs/remark-gfm * [github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify * [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer * * @module checkbox-character-style * @author Titus Wormer * @copyright Titus Wormer * @license MIT * * @example * {"config": {"checked": "x"}, "gfm": true, "name": "ok-x.md"} * * - [x] Mercury. * - [x] Venus. * * @example * {"config": {"checked": "X"}, "gfm": true, "name": "ok-x-upper.md"} * * - [X] Mercury. * - [X] Venus. * * @example * {"config": {"unchecked": " "}, "gfm": true, "name": "ok-space.md"} * * - [ ] Mercury. * - [ ] Venus. * - [ ]␠␠ * - [ ] * * @example * {"config": {"unchecked": "\t"}, "gfm": true, "name": "ok-tab.md"} * * - [␉] Mercury. * - [␉] Venus. * * @example * {"label": "input", "gfm": true, "name": "not-ok-default.md"} * * - [x] Mercury. * - [X] Venus. * - [ ] Earth. * - [␉] Mars. * @example * {"label": "output", "gfm": true, "name": "not-ok-default.md"} * * 2:5: Unexpected checked checkbox value `X`, expected `x` * 4:5: Unexpected unchecked checkbox value `\t`, expected ` ` * * @example * {"config": "🌍", "label": "output", "name": "not-ok-option.md", "positionless": true} * * 1:1: Unexpected value `🌍` for `options`, expected an object or `'consistent'` * * @example * {"config": {"unchecked": "🌍"}, "label": "output", "name": "not-ok-option-unchecked.md", "positionless": true} * * 1:1: Unexpected value `🌍` for `options.unchecked`, expected `'\t'`, `' '`, or `'consistent'` * * @example * {"config": {"checked": "🌍"}, "label": "output", "name": "not-ok-option-checked.md", "positionless": true} * * 1:1: Unexpected value `🌍` for `options.checked`, expected `'X'`, `'x'`, or `'consistent'` */ /** * @import {Root} from 'mdast' */ /** * @typedef {Styles | 'consistent'} Options * Configuration. * * @typedef Styles * Styles. * @property {'X' | 'x' | 'consistent' | null | undefined} [checked='consistent'] * Preferred style to use for checked checkboxes (default: `'consistent'`). * @property {'\t' | ' ' | 'consistent' | null | undefined} [unchecked='consistent'] * Preferred style to use for unchecked checkboxes (default: `'consistent'`). */ import {phrasing} from 'mdast-util-phrasing' import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' import {SKIP, visitParents} from 'unist-util-visit-parents' import {VFileMessage} from 'vfile-message' const remarkLintCheckboxCharacterStyle = lintRule( { origin: 'remark-lint:checkbox-character-style', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-checkbox-character-style#readme' }, /** * @param {Root} tree * Tree. * @param {Options | null | undefined} [options] * Configuration (optional). * @returns {undefined} * Nothing. */ function (tree, file, options) { const value = String(file) /** @type {'X' | 'x' | undefined} */ let checkedExpected /** @type {VFileMessage | undefined} */ let checkedConsistentCause /** @type {'\t' | ' ' | undefined} */ let uncheckedExpected /** @type {VFileMessage | undefined} */ let uncheckedConsistentCause if (options === null || options === undefined || options === 'consistent') { // Empty. } else if (typeof options === 'object') { if (options.checked === 'X' || options.checked === 'x') { checkedExpected = options.checked } else if (options.checked && options.checked !== 'consistent') { file.fail( 'Unexpected value `' + options.checked + "` for `options.checked`, expected `'X'`, `'x'`, or `'consistent'`" ) } if (options.unchecked === '\t' || options.unchecked === ' ') { uncheckedExpected = options.unchecked } else if (options.unchecked && options.unchecked !== 'consistent') { file.fail( 'Unexpected value `' + options.unchecked + "` for `options.unchecked`, expected `'\\t'`, `' '`, or `'consistent'`" ) } } else { file.fail( 'Unexpected value `' + options + "` for `options`, expected an object or `'consistent'`" ) } visitParents(tree, function (node, parents) { // Do not walk into phrasing. if (phrasing(node)) { return SKIP } if (node.type !== 'listItem') return const head = node.children[0] const headStart = pointStart(head) // Exit early for items without checkbox. // A list item cannot be checked and empty, according to GFM. if ( !head || !headStart || typeof node.checked !== 'boolean' || typeof headStart.offset !== 'number' ) { return } // Move back to before `] `. headStart.offset -= 2 headStart.column -= 2 // Assume we start with a checkbox, because well, `checked` is set. const match = /\[([\t Xx])]/.exec( value.slice(headStart.offset - 2, headStart.offset + 1) ) /* c8 ignore next 2 -- failsafe so we don’t crash if there actually isn’t * a checkbox. */ if (!match) return const actual = match[1] const actualDisplay = actual === '\t' ? '\\t' : actual const expected = node.checked ? checkedExpected : uncheckedExpected const expectedDisplay = expected === '\t' ? '\\t' : expected if (!expected) { const cause = new VFileMessage( (node.checked ? 'C' : 'Unc') + "hecked checkbox style `'" + actualDisplay + "'` first defined for `'consistent'` here", { ancestors: [...parents, node], place: headStart, ruleId: 'checkbox-character-style', source: 'remark-lint' } ) if (node.checked) { checkedExpected = /** @type {'X' | 'x'} */ (actual) checkedConsistentCause = cause } else { uncheckedExpected = /** @type {'\t' | ' '} */ (actual) uncheckedConsistentCause = cause } } else if (actual !== expected) { file.message( 'Unexpected ' + (node.checked ? '' : 'un') + 'checked checkbox value `' + actualDisplay + '`, expected `' + expectedDisplay + '`', { ancestors: [...parents, node], cause: node.checked ? checkedConsistentCause : uncheckedConsistentCause, place: headStart } ) } }) } ) export default remarkLintCheckboxCharacterStyle