diff --git a/package.json b/package.json index 8d5a9a35..dc375ad0 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "remark-cli": "^12.0.0", "remark-comment-config": "^8.0.0", "remark-directive": "^3.0.0", + "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "remark-github": "^12.0.0", "remark-math": "^6.0.0", diff --git a/packages/remark-lint-blockquote-indentation/index.js b/packages/remark-lint-blockquote-indentation/index.js index a6221dfa..3f4c5537 100644 --- a/packages/remark-lint-blockquote-indentation/index.js +++ b/packages/remark-lint-blockquote-indentation/index.js @@ -65,41 +65,52 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md", "config": 4} + * {"config": 2, "name": "ok-2.md"} + * + * > Mercury. * - * > Hello + * Venus. * - * Paragraph. + * > Earth. * - * > World * @example - * {"name": "ok.md", "config": 2} + * {"config": 4, "name": "ok-4.md"} * - * > Hello + * > Mercury. * - * Paragraph. + * Venus. * - * > World + * > Earth. * * @example - * {"name": "not-ok.md", "label": "input"} + * { "name": "ok-tab.md"} + * + * >␉Mercury. * - * > Hello + * @example + * {"label": "input", "name": "not-ok.md"} * - * Paragraph. + * > Mercury. * - * > World + * Venus. * - * Paragraph. + * > Earth. * - * > World + * Mars. * + * > Jupiter * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} + * + * 5:5: Unexpected `4` spaces between block quote marker and content, expected `3` spaces, remove `1` space + * 9:3: Unexpected `2` spaces between block quote marker and content, expected `3` spaces, add `1` space * - * 5:5: Remove 1 space between block quote and content - * 9:3: Add 1 space between block quote and content + * @example + * {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true} + * + * 1:1: Unexpected value `🌍` for `options`, expected `number` or `'consistent'` */ /** @@ -114,7 +125,7 @@ import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const remarkLintBlockquoteIndentation = lintRule( { @@ -130,33 +141,53 @@ const remarkLintBlockquoteIndentation = lintRule( * Nothing. */ function (tree, file, options) { - let option = options || 'consistent' + /** @type {number | undefined} */ + let expected + + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (typeof options === 'number') { + expected = options + } else { + file.fail( + 'Unexpected value `' + + options + + "` for `options`, expected `number` or `'consistent'`" + ) + } - visit(tree, 'blockquote', function (node) { + visitParents(tree, 'blockquote', function (node, parents) { const start = pointStart(node) - const head = pointStart(node.children[0]) + const headStart = pointStart(node.children[0]) - if (head && start) { - const count = head.column - start.column + if (headStart && start) { + const actual = headStart.column - start.column - if (option === 'consistent') { - option = count - } else { - const diff = option - count - - if (diff !== 0) { - const abs = Math.abs(diff) + if (expected) { + const difference = expected - actual + const differenceAbsolute = Math.abs(difference) + if (difference !== 0) { file.message( - (diff > 0 ? 'Add' : 'Remove') + - ' ' + - abs + - ' ' + - pluralize('space', abs) + - ' between block quote and content', - head + 'Unexpected `' + + actual + + '` ' + + pluralize('space', actual) + + ' between block quote marker and content, expected `' + + expected + + '` ' + + pluralize('space', expected) + + ', ' + + (difference > 0 ? 'add' : 'remove') + + ' `' + + differenceAbsolute + + '` ' + + pluralize('space', differenceAbsolute), + {ancestors: [...parents, node], place: headStart} ) } + } else { + expected = actual } } }) diff --git a/packages/remark-lint-blockquote-indentation/package.json b/packages/remark-lint-blockquote-indentation/package.json index 89215737..b59db7dc 100644 --- a/packages/remark-lint-blockquote-indentation/package.json +++ b/packages/remark-lint-blockquote-indentation/package.json @@ -36,7 +36,7 @@ "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-blockquote-indentation/readme.md b/packages/remark-lint-blockquote-indentation/readme.md index 9f80fcb7..8a1fd014 100644 --- a/packages/remark-lint-blockquote-indentation/readme.md +++ b/packages/remark-lint-blockquote-indentation/readme.md @@ -171,36 +171,48 @@ Due to this, it’s recommended to configure this rule with `2`. ## Examples -##### `ok.md` +##### `ok-2.md` -When configured with `4`. +When configured with `2`. ###### In ```markdown -> Hello +> Mercury. -Paragraph. +Venus. -> World +> Earth. ``` ###### Out No messages. -##### `ok.md` +##### `ok-4.md` -When configured with `2`. +When configured with `4`. ###### In ```markdown -> Hello +> Mercury. -Paragraph. +Venus. -> World +> Earth. +``` + +###### Out + +No messages. + +##### `ok-tab.md` + +###### In + +```markdown +>␉Mercury. ``` ###### Out @@ -212,22 +224,32 @@ No messages. ###### In ```markdown -> Hello +> Mercury. + +Venus. + +> Earth. -Paragraph. +Mars. -> World +> Jupiter +``` -Paragraph. +###### Out -> World +```text +5:5: Unexpected `4` spaces between block quote marker and content, expected `3` spaces, remove `1` space +9:3: Unexpected `2` spaces between block quote marker and content, expected `3` spaces, add `1` space ``` +##### `not-ok-options.md` + +When configured with `'🌍'`. + ###### Out ```text -5:5: Remove 1 space between block quote and content -9:3: Add 1 space between block quote and content +1:1: Unexpected value `🌍` for `options`, expected `number` or `'consistent'` ``` ## Compatibility diff --git a/packages/remark-lint-checkbox-character-style/index.js b/packages/remark-lint-checkbox-character-style/index.js index 7cc101af..2655e402 100644 --- a/packages/remark-lint-checkbox-character-style/index.js +++ b/packages/remark-lint-checkbox-character-style/index.js @@ -10,6 +10,8 @@ * * 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 * @@ -63,6 +65,7 @@ * [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 * @@ -70,55 +73,60 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md", "config": {"checked": "x"}, "gfm": true} + * {"config": {"checked": "x"}, "gfm": true, "name": "ok-x.md"} * - * - [x] List item - * - [x] List item + * - [x] Mercury. + * - [x] Venus. * * @example - * {"name": "ok.md", "config": {"checked": "X"}, "gfm": true} + * {"config": {"checked": "X"}, "gfm": true, "name": "ok-x-upper.md"} * - * - [X] List item - * - [X] List item + * - [X] Mercury. + * - [X] Venus. * * @example - * {"name": "ok.md", "config": {"unchecked": " "}, "gfm": true} + * {"config": {"unchecked": " "}, "gfm": true, "name": "ok-space.md"} * - * - [ ] List item - * - [ ] List item + * - [ ] Mercury. + * - [ ] Venus. * - [ ]␠␠ * - [ ] * * @example - * {"name": "ok.md", "config": {"unchecked": "\t"}, "gfm": true} + * {"config": {"unchecked": "\t"}, "gfm": true, "name": "ok-tab.md"} + * + * - [␉] Mercury. + * - [␉] Venus. * - * - [␉] List item - * - [␉] List item + * @example + * {"label": "input", "gfm": true, "name": "not-ok-default.md"} * + * - [x] Mercury. + * - [X] Venus. + * - [ ] Earth. + * - [␉] Mars. * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} + * {"label": "output", "gfm": true, "name": "not-ok-default.md"} * - * - [x] List item - * - [X] List item - * - [ ] List item - * - [␉] List item + * 2:5: Unexpected checked checkbox value `X`, expected `x` + * 4:5: Unexpected unchecked checkbox value `\t`, expected ` ` * * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} + * {"config": "🌍", "label": "output", "name": "not-ok-option.md", "positionless": true} * - * 2:5: Checked checkboxes should use `x` as a marker - * 4:5: Unchecked checkboxes should use ` ` as a marker + * 1:1: Unexpected value `🌍` for `options`, expected an object or `'consistent'` * * @example - * {"config": {"unchecked": "💩"}, "name": "not-ok.md", "label": "output", "positionless": true, "gfm": true} + * {"config": {"unchecked": "🌍"}, "label": "output", "name": "not-ok-option-unchecked.md", "positionless": true} * - * 1:1: Incorrect unchecked checkbox marker `💩`: use either `'\t'`, or `' '` + * 1:1: Unexpected value `🌍` for `options.unchecked`, expected `'\t'`, `' '`, or `'consistent'` * * @example - * {"config": {"checked": "💩"}, "name": "not-ok.md", "label": "output", "positionless": true, "gfm": true} + * {"config": {"checked": "🌍"}, "label": "output", "name": "not-ok-option-checked.md", "positionless": true} * - * 1:1: Incorrect checked checkbox marker `💩`: use either `'x'`, or `'X'` + * 1:1: Unexpected value `🌍` for `options.checked`, expected `'X'`, `'x'`, or `'consistent'` */ /** @@ -139,7 +147,8 @@ import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintCheckboxCharacterStyle = lintRule( { @@ -156,77 +165,115 @@ const remarkLintCheckboxCharacterStyle = lintRule( */ function (tree, file, options) { const value = String(file) - /** @type {'X' | 'x' | 'consistent'} */ - let checked = 'consistent' - /** @type {'\x09' | ' ' | 'consistent'} */ - let unchecked = 'consistent' + /** @type {'X' | 'x' | undefined} */ + let checkedExpected + /** @type {VFileMessage | undefined} */ + let checkedConsistentCause + /** @type {'\t' | ' ' | undefined} */ + let uncheckedExpected + /** @type {VFileMessage | undefined} */ + let uncheckedConsistentCause - if (options && typeof options === 'object') { - checked = options.checked || 'consistent' - unchecked = options.unchecked || 'consistent' - } - - if (unchecked !== 'consistent' && unchecked !== ' ' && unchecked !== '\t') { - file.fail( - 'Incorrect unchecked checkbox marker `' + - unchecked + - "`: use either `'\\t'`, or `' '`" - ) - } + 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 (checked !== 'consistent' && checked !== 'x' && checked !== 'X') { + 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( - 'Incorrect checked checkbox marker `' + - checked + - "`: use either `'x'`, or `'X'`" + 'Unexpected value `' + + options + + "` for `options`, expected an object or `'consistent'`" ) } - visit(tree, 'listItem', function (node) { + visitParents(tree, 'listItem', function (node, parents) { const head = node.children[0] - const point = pointStart(head) + const headStart = pointStart(head) // Exit early for items without checkbox. // A list item cannot be checked and empty, according to GFM. if ( - !point || !head || + !headStart || typeof node.checked !== 'boolean' || - typeof point.offset !== 'number' + typeof headStart.offset !== 'number' ) { return } // Move back to before `] `. - point.offset -= 2 - point.column -= 2 + headStart.offset -= 2 + headStart.column -= 2 // Assume we start with a checkbox, because well, `checked` is set. const match = /\[([\t Xx])]/.exec( - value.slice(point.offset - 2, point.offset + 1) + 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 style = node.checked ? checked : unchecked + 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 (style === 'consistent') { if (node.checked) { - // @ts-expect-error: valid marker. - checked = match[1] + checkedExpected = /** @type {'X' | 'x'} */ (actual) + checkedConsistentCause = cause } else { - // @ts-expect-error: valid marker. - unchecked = match[1] + uncheckedExpected = /** @type {'\t' | ' '} */ (actual) + uncheckedConsistentCause = cause } - } else if (match[1] !== style) { + } else if (actual !== expected) { file.message( - (node.checked ? 'Checked' : 'Unchecked') + - ' checkboxes should use `' + - style + - '` as a marker', - point + 'Unexpected ' + + (node.checked ? '' : 'un') + + 'checked checkbox value `' + + actualDisplay + + '`, expected `' + + expectedDisplay + + '`', + { + ancestors: [...parents, node], + cause: node.checked + ? checkedConsistentCause + : uncheckedConsistentCause, + place: headStart + } ) } }) diff --git a/packages/remark-lint-checkbox-character-style/package.json b/packages/remark-lint-checkbox-character-style/package.json index 3cd16976..58389798 100644 --- a/packages/remark-lint-checkbox-character-style/package.json +++ b/packages/remark-lint-checkbox-character-style/package.json @@ -36,7 +36,8 @@ "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-checkbox-character-style/readme.md b/packages/remark-lint-checkbox-character-style/readme.md index 35b519c1..0fc7c4af 100644 --- a/packages/remark-lint-checkbox-character-style/readme.md +++ b/packages/remark-lint-checkbox-character-style/readme.md @@ -39,6 +39,8 @@ This package checks the character used in checkboxes. 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]. ## Presets @@ -176,7 +178,7 @@ using `'x'` (lowercase X) and unchecked checkboxes using `'␠'` (a space). ## Examples -##### `ok.md` +##### `ok-x.md` When configured with `{ checked: 'x' }`. @@ -186,15 +188,15 @@ When configured with `{ checked: 'x' }`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -- [x] List item -- [x] List item +- [x] Mercury. +- [x] Venus. ``` ###### Out No messages. -##### `ok.md` +##### `ok-x-upper.md` When configured with `{ checked: 'X' }`. @@ -204,15 +206,15 @@ When configured with `{ checked: 'X' }`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -- [X] List item -- [X] List item +- [X] Mercury. +- [X] Venus. ``` ###### Out No messages. -##### `ok.md` +##### `ok-space.md` When configured with `{ unchecked: ' ' }`. @@ -222,8 +224,8 @@ When configured with `{ unchecked: ' ' }`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -- [ ] List item -- [ ] List item +- [ ] Mercury. +- [ ] Venus. - [ ]␠␠ - [ ] ``` @@ -232,7 +234,7 @@ When configured with `{ unchecked: ' ' }`. No messages. -##### `ok.md` +##### `ok-tab.md` When configured with `{ unchecked: '\t' }`. @@ -242,15 +244,15 @@ When configured with `{ unchecked: '\t' }`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -- [␉] List item -- [␉] List item +- [␉] Mercury. +- [␉] Venus. ``` ###### Out No messages. -##### `not-ok.md` +##### `not-ok-default.md` ###### In @@ -258,37 +260,47 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -- [x] List item -- [X] List item -- [ ] List item -- [␉] List item +- [x] Mercury. +- [X] Venus. +- [ ] Earth. +- [␉] Mars. ``` ###### Out ```text -2:5: Checked checkboxes should use `x` as a marker -4:5: Unchecked checkboxes should use ` ` as a marker +2:5: Unexpected checked checkbox value `X`, expected `x` +4:5: Unexpected unchecked checkbox value `\t`, expected ` ` ``` -##### `not-ok.md` +##### `not-ok-option.md` -When configured with `{ unchecked: '💩' }`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect unchecked checkbox marker `💩`: use either `'\t'`, or `' '` +1:1: Unexpected value `🌍` for `options`, expected an object or `'consistent'` ``` -##### `not-ok.md` +##### `not-ok-option-unchecked.md` -When configured with `{ checked: '💩' }`. +When configured with `{ unchecked: '🌍' }`. ###### Out ```text -1:1: Incorrect checked checkbox marker `💩`: use either `'x'`, or `'X'` +1:1: Unexpected value `🌍` for `options.unchecked`, expected `'\t'`, `' '`, or `'consistent'` +``` + +##### `not-ok-option-checked.md` + +When configured with `{ checked: '🌍' }`. + +###### Out + +```text +1:1: Unexpected value `🌍` for `options.checked`, expected `'X'`, `'x'`, or `'consistent'` ``` ## Compatibility diff --git a/packages/remark-lint-checkbox-content-indent/index.js b/packages/remark-lint-checkbox-content-indent/index.js index 1a5f9d1b..2d323e73 100644 --- a/packages/remark-lint-checkbox-content-indent/index.js +++ b/packages/remark-lint-checkbox-content-indent/index.js @@ -55,38 +55,48 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md", "gfm": true} + * {"gfm": true, "name": "ok.md"} + * + * - [ ] Mercury. + * + [x] Venus. + * * [X] Earth. + * - [ ] Mars. * - * - [ ] List item - * + [x] List Item - * * [X] List item - * - [ ] List item + * @example + * {"gfm": true, "label": "input", "name": "not-ok.md"} * + * - [ ] Mercury. + * + [x] Venus. + * * [X] Earth. + * - [ ] Mars. * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} + * {"gfm": true, "label": "output", "name": "not-ok.md"} * - * - [ ] List item - * + [x] List item - * * [X] List item - * - [ ] List item + * 2:8: Unexpected `2` spaces between checkbox and content, expected `1` space, remove `1` space + * 3:9: Unexpected `3` spaces between checkbox and content, expected `1` space, remove `2` spaces + * 4:10: Unexpected `4` spaces between checkbox and content, expected `1` space, remove `3` spaces * * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} + * {"gfm": true, "label": "input", "name": "tab.md"} * - * 2:7-2:8: Checkboxes should be followed by a single character - * 3:7-3:9: Checkboxes should be followed by a single character - * 4:7-4:10: Checkboxes should be followed by a single character + * - [ ]␉Mercury. + * + [x]␉␉Venus. + * @example + * {"gfm": true, "label": "output", "name": "tab.md"} + * + * 2:8: Unexpected `2` spaces between checkbox and content, expected `1` space, remove `1` space */ /** * @typedef {import('mdast').Root} Root */ +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' -import {location} from 'vfile-location' +import {visitParents} from 'unist-util-visit-parents' const remarkLintCheckboxContentIndent = lintRule( { @@ -101,45 +111,59 @@ const remarkLintCheckboxContentIndent = lintRule( */ function (tree, file) { const value = String(file) - const loc = location(file) - visit(tree, 'listItem', function (node) { + visitParents(tree, 'listItem', function (node, parents) { const head = node.children[0] - const point = pointStart(head) + const headStart = pointStart(head) // Exit early for items without checkbox. - // A list item cannot be checked and empty, according to GFM. + // A list item cannot be checked and empty according to GFM. if ( - !point || !head || + !headStart || typeof node.checked !== 'boolean' || - typeof point.offset !== 'number' + typeof headStart.offset !== 'number' ) { return } - // Assume we start with a checkbox, because well, `checked` is set. + // Assume we start with a checkbox as `checked` is set. const match = /\[([\t xX])]/.exec( - value.slice(point.offset - 4, point.offset + 1) + value.slice(headStart.offset - 4, headStart.offset + 1) ) /* c8 ignore next -- make sure we don’t crash if there actually isn’t a checkbox. */ if (!match) return // Move past checkbox. - const initial = point.offset - let final = initial + let final = headStart.offset + let code = value.charCodeAt(final) - while (/[\t ]/.test(value.charAt(final))) final++ + while (code === 9 || code === 32) { + final++ + code = value.charCodeAt(final) + } - if (final - initial > 0) { - const start = loc.toPoint(initial) - const end = loc.toPoint(final) + const size = final - headStart.offset + if (size) { file.message( - 'Checkboxes should be followed by a single character', - /* c8 ignore next -- we get here if we have offsets. */ - start && end ? {start, end} : undefined + 'Unexpected `' + + (size + 1) + + '` ' + + pluralize('space', size + 1) + + ' between checkbox and content, expected `1` space, remove `' + + size + + '` ' + + pluralize('space', size), + { + ancestors: [...parents, node], + place: { + line: headStart.line, + column: headStart.column + size, + offset: headStart.offset + size + } + } ) } }) diff --git a/packages/remark-lint-checkbox-content-indent/package.json b/packages/remark-lint-checkbox-content-indent/package.json index 597c9bd9..e5910a15 100644 --- a/packages/remark-lint-checkbox-content-indent/package.json +++ b/packages/remark-lint-checkbox-content-indent/package.json @@ -34,10 +34,10 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile-location": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -49,7 +49,8 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-checkbox-content-indent/readme.md b/packages/remark-lint-checkbox-content-indent/readme.md index b21309e5..6ee51f7e 100644 --- a/packages/remark-lint-checkbox-content-indent/readme.md +++ b/packages/remark-lint-checkbox-content-indent/readme.md @@ -163,10 +163,10 @@ content after them with a single space between. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -- [ ] List item -+ [x] List Item -* [X] List item -- [ ] List item +- [ ] Mercury. ++ [x] Venus. +* [X] Earth. +- [ ] Mars. ``` ###### Out @@ -181,18 +181,36 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -- [ ] List item -+ [x] List item -* [X] List item -- [ ] List item +- [ ] Mercury. ++ [x] Venus. +* [X] Earth. +- [ ] Mars. ``` ###### Out ```text -2:7-2:8: Checkboxes should be followed by a single character -3:7-3:9: Checkboxes should be followed by a single character -4:7-4:10: Checkboxes should be followed by a single character +2:8: Unexpected `2` spaces between checkbox and content, expected `1` space, remove `1` space +3:9: Unexpected `3` spaces between checkbox and content, expected `1` space, remove `2` spaces +4:10: Unexpected `4` spaces between checkbox and content, expected `1` space, remove `3` spaces +``` + +##### `tab.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +- [ ]␉Mercury. ++ [x]␉␉Venus. +``` + +###### Out + +```text +2:8: Unexpected `2` spaces between checkbox and content, expected `1` space, remove `1` space ``` ## Compatibility diff --git a/packages/remark-lint-code-block-style/index.js b/packages/remark-lint-code-block-style/index.js index de2ceba4..c220e8f5 100644 --- a/packages/remark-lint-code-block-style/index.js +++ b/packages/remark-lint-code-block-style/index.js @@ -75,81 +75,79 @@ * @license MIT * * @example - * {"config": "indented", "name": "ok.md"} + * {"config": "indented", "name": "ok-indented.md"} * - * alpha() + * venus() * - * Paragraph. + * Mercury. * - * bravo() + * earth() * * @example - * {"config": "indented", "name": "not-ok.md", "label": "input"} + * {"config": "fenced", "name": "ok-fenced.md"} * * ``` - * alpha() + * venus() * ``` * - * Paragraph. + * Mercury. * * ``` - * bravo() + * earth() * ``` * * @example - * {"config": "indented", "name": "not-ok.md", "label": "output"} + * {"label": "input", "name": "not-ok-consistent.md"} * - * 1:1-3:4: Code blocks should be indented - * 7:1-9:4: Code blocks should be indented + * venus() * - * @example - * {"config": "fenced", "name": "ok.md"} + * Mercury. * * ``` - * alpha() + * earth() * ``` + * @example + * {"label": "output", "name": "not-ok-consistent.md"} * - * Paragraph. - * - * ``` - * bravo() - * ``` + * 5:1-7:4: Unexpected fenced code block, expected indented code blocks * * @example - * {"config": "fenced", "name": "not-ok-fenced.md", "label": "input"} - * - * alpha() + * {"config": "indented", "label": "input", "name": "not-ok-indented.md"} * - * Paragraph. + * ``` + * venus() + * ``` * - * bravo() + * Mercury. * + * ``` + * earth() + * ``` * @example - * {"config": "fenced", "name": "not-ok-fenced.md", "label": "output"} + * {"config": "indented", "label": "output", "name": "not-ok-indented.md"} * - * 1:1-1:12: Code blocks should be fenced - * 5:1-5:12: Code blocks should be fenced + * 1:1-3:4: Unexpected fenced code block, expected indented code blocks + * 7:1-9:4: Unexpected fenced code block, expected indented code blocks * * @example - * {"name": "not-ok-consistent.md", "label": "input"} + * {"config": "fenced", "label": "input", "name": "not-ok-fenced.md"} * - * alpha() + * venus() * - * Paragraph. + * Mercury. * - * ``` - * bravo() - * ``` + * earth() * * @example - * {"name": "not-ok-consistent.md", "label": "output"} + * {"config": "fenced", "label": "output", "name": "not-ok-fenced.md"} * - * 5:1-7:4: Code blocks should be indented + * 1:1-1:12: Unexpected indented code block, expected fenced code blocks + * 5:1-5:12: Unexpected indented code block, expected fenced code blocks * * @example - * {"config": "💩", "name": "not-ok-incorrect.md", "label": "output", "positionless": true} + * {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true} * - * 1:1: Incorrect code block style `💩`: use either `'consistent'`, `'fenced'`, or `'indented'` + * 1:1: Unexpected value `🌍` for `options`, expected `'fenced'`, `'indented'`, or `'consistent'` */ /** @@ -166,7 +164,8 @@ import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintCodeBlockStyle = lintRule( { @@ -182,22 +181,25 @@ const remarkLintCodeBlockStyle = lintRule( * Nothing. */ function (tree, file, options) { - let option = options || 'consistent' const value = String(file) + /** @type {VFileMessage | undefined} */ + let cause + /** @type {Style | undefined} */ + let expected - if ( - option !== 'consistent' && - option !== 'indented' && - option !== 'fenced' - ) { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (options === 'indented' || options === 'fenced') { + expected = options + } else { file.fail( - 'Incorrect code block style `' + - option + - "`: use either `'consistent'`, `'fenced'`, or `'indented'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'fenced'`, `'indented'`, or `'consistent'`" ) } - visit(tree, 'code', function (node) { + visitParents(tree, 'code', function (node, parents) { const end = pointEnd(node) const start = pointStart(node) @@ -210,16 +212,35 @@ const remarkLintCodeBlockStyle = lintRule( return } - const current = - node.lang || - /^\s*([~`])\1{2,}/.test(value.slice(start.offset, end.offset)) + const actual = + node.lang || /^ {0,3}([`~])/.test(value.slice(start.offset, end.offset)) ? 'fenced' : 'indented' - if (option === 'consistent') { - option = current - } else if (option !== current) { - file.message('Code blocks should be ' + option, node) + if (expected) { + if (expected !== actual) { + file.message( + 'Unexpected ' + + actual + + ' code block, expected ' + + expected + + ' code blocks', + {ancestors: [...parents, node], cause, place: {start, end}} + ) + } + } else { + expected = actual + cause = new VFileMessage( + "Code block style `'" + + actual + + "'` first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place: {start, end}, + source: 'remark-lint', + ruleId: 'code-block-style' + } + ) } }) } diff --git a/packages/remark-lint-code-block-style/package.json b/packages/remark-lint-code-block-style/package.json index d129800e..2316cfb8 100644 --- a/packages/remark-lint-code-block-style/package.json +++ b/packages/remark-lint-code-block-style/package.json @@ -34,7 +34,8 @@ "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-code-block-style/readme.md b/packages/remark-lint-code-block-style/readme.md index dc43b7bd..e7a598cd 100644 --- a/packages/remark-lint-code-block-style/readme.md +++ b/packages/remark-lint-code-block-style/readme.md @@ -180,120 +180,120 @@ language and as indented code otherwise. ## Examples -##### `ok.md` +##### `ok-indented.md` When configured with `'indented'`. ###### In ```markdown - alpha() + venus() -Paragraph. +Mercury. - bravo() + earth() ``` ###### Out No messages. -##### `not-ok.md` +##### `ok-fenced.md` -When configured with `'indented'`. +When configured with `'fenced'`. ###### In ````markdown ``` -alpha() +venus() ``` -Paragraph. +Mercury. ``` -bravo() +earth() ``` ```` ###### Out -```text -1:1-3:4: Code blocks should be indented -7:1-9:4: Code blocks should be indented -``` - -##### `ok.md` +No messages. -When configured with `'fenced'`. +##### `not-ok-consistent.md` ###### In ````markdown -``` -alpha() -``` + venus() -Paragraph. +Mercury. ``` -bravo() +earth() ``` ```` ###### Out -No messages. +```text +5:1-7:4: Unexpected fenced code block, expected indented code blocks +``` -##### `not-ok-fenced.md` +##### `not-ok-indented.md` -When configured with `'fenced'`. +When configured with `'indented'`. ###### In -```markdown - alpha() +````markdown +``` +venus() +``` -Paragraph. +Mercury. - bravo() ``` +earth() +``` +```` ###### Out ```text -1:1-1:12: Code blocks should be fenced -5:1-5:12: Code blocks should be fenced +1:1-3:4: Unexpected fenced code block, expected indented code blocks +7:1-9:4: Unexpected fenced code block, expected indented code blocks ``` -##### `not-ok-consistent.md` +##### `not-ok-fenced.md` + +When configured with `'fenced'`. ###### In -````markdown - alpha() +```markdown + venus() -Paragraph. +Mercury. + earth() ``` -bravo() -``` -```` ###### Out ```text -5:1-7:4: Code blocks should be indented +1:1-1:12: Unexpected indented code block, expected fenced code blocks +5:1-5:12: Unexpected indented code block, expected fenced code blocks ``` -##### `not-ok-incorrect.md` +##### `not-ok-options.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect code block style `💩`: use either `'consistent'`, `'fenced'`, or `'indented'` +1:1: Unexpected value `🌍` for `options`, expected `'fenced'`, `'indented'`, or `'consistent'` ``` ## Compatibility diff --git a/packages/remark-lint-definition-case/index.js b/packages/remark-lint-definition-case/index.js index bec81d64..16b717e9 100644 --- a/packages/remark-lint-definition-case/index.js +++ b/packages/remark-lint-definition-case/index.js @@ -37,30 +37,31 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * [example]: http://example.com "Example Domain" + * [mercury]: http://example.com "Mercury" * * @example - * {"name": "not-ok.md", "label": "input"} - * - * [Example]: http://example.com "Example Domain" + * {"label": "input", "name": "not-ok.md"} * + * [Mercury]: http://example.com "Mercury" * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:1-1:47: Do not use uppercase characters in definition labels + * 1:1-1:40: Unexpected uppercase characters in definition label, expected lowercase * * @example * {"gfm": true, "label": "input", "name": "gfm.md"} * - * [^X]: Footnote definitions (from GFM) are checked too. - * + * [^Mercury]: + * **Mercury** is the first planet from the Sun and the smallest + * in the Solar System. * @example * {"gfm": true, "label": "output", "name": "gfm.md"} * - * 1:1-1:55: Do not use uppercase characters in definition labels + * 1:1-3:25: Unexpected uppercase characters in footnote definition label, expected lowercase */ /** @@ -68,10 +69,7 @@ */ import {lintRule} from 'unified-lint-rule' -import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' - -const label = /^\s*\[((?:\\[\s\S]|[^[\]])+)]/ +import {visitParents} from 'unist-util-visit-parents' const remarkLintDefinitionCase = lintRule( { @@ -85,28 +83,19 @@ const remarkLintDefinitionCase = lintRule( * Nothing. */ function (tree, file) { - const value = String(file) - - visit(tree, function (node) { - if (node.type === 'definition' || node.type === 'footnoteDefinition') { - const end = pointEnd(node) - const start = pointStart(node) - - if ( - end && - start && - typeof end.offset === 'number' && - typeof start.offset === 'number' - ) { - const match = value.slice(start.offset, end.offset).match(label) - - if (match && match[1] !== match[1].toLowerCase()) { - file.message( - 'Do not use uppercase characters in definition labels', - node - ) - } - } + visitParents(tree, function (node, parents) { + if ( + (node.type === 'definition' || node.type === 'footnoteDefinition') && + node.position && + node.label && + node.label !== node.label.toLowerCase() + ) { + file.message( + 'Unexpected uppercase characters in ' + + (node.type === 'definition' ? '' : 'footnote ') + + 'definition label, expected lowercase', + {ancestors: [...parents, node], place: node.position} + ) } }) } diff --git a/packages/remark-lint-definition-case/package.json b/packages/remark-lint-definition-case/package.json index c5a153be..8c76c2e5 100644 --- a/packages/remark-lint-definition-case/package.json +++ b/packages/remark-lint-definition-case/package.json @@ -33,8 +33,7 @@ "dependencies": { "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-definition-case/readme.md b/packages/remark-lint-definition-case/readme.md index 4414f6c2..1b40a606 100644 --- a/packages/remark-lint-definition-case/readme.md +++ b/packages/remark-lint-definition-case/readme.md @@ -146,7 +146,7 @@ Due to this, it’s recommended to use lowercase and turn this rule on. ###### In ```markdown -[example]: http://example.com "Example Domain" +[mercury]: http://example.com "Mercury" ``` ###### Out @@ -158,13 +158,13 @@ No messages. ###### In ```markdown -[Example]: http://example.com "Example Domain" +[Mercury]: http://example.com "Mercury" ``` ###### Out ```text -1:1-1:47: Do not use uppercase characters in definition labels +1:1-1:40: Unexpected uppercase characters in definition label, expected lowercase ``` ##### `gfm.md` @@ -175,13 +175,15 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -[^X]: Footnote definitions (from GFM) are checked too. +[^Mercury]: + **Mercury** is the first planet from the Sun and the smallest + in the Solar System. ``` ###### Out ```text -1:1-1:55: Do not use uppercase characters in definition labels +1:1-3:25: Unexpected uppercase characters in footnote definition label, expected lowercase ``` ## Compatibility diff --git a/packages/remark-lint-definition-spacing/index.js b/packages/remark-lint-definition-spacing/index.js index 610ac1d2..d034f199 100644 --- a/packages/remark-lint-definition-spacing/index.js +++ b/packages/remark-lint-definition-spacing/index.js @@ -41,31 +41,41 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * [example domain]: http://example.com "Example Domain" + * [planet mercury]: http://example.com + * + * @example + * {"label": "input", "name": "not-ok-consecutive.md"} * + * [planet␠␠␠␠mercury]: http://example.com * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "output", "name": "not-ok-consecutive.md"} * - * [example␠␠␠␠domain]: http://example.com "Example Domain" + * 1:1-1:40: Unexpected `4` consecutive spaces in definition label, expected `1` space, remove `3` spaces * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "input", "name": "not-ok-non-space.md"} * - * 1:1-1:57: Do not use consecutive whitespace in definition labels + * [pla␉net␊mer␍cury]: http://e.com + * @example + * {"label": "output", "name": "not-ok-non-space.md"} + * + * 1:1-3:20: Unexpected non-space whitespace character `\t` in definition label, expected `1` space, replace it + * 1:1-3:20: Unexpected non-space whitespace character `\n` in definition label, expected `1` space, replace it + * 1:1-3:20: Unexpected non-space whitespace character `\r` in definition label, expected `1` space, replace it */ /** * @typedef {import('mdast').Root} Root */ +import {longestStreak} from 'longest-streak' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' -import {pointStart, pointEnd} from 'unist-util-position' -import {visit} from 'unist-util-visit' - -const label = /^\s*\[((?:\\[\s\S]|[^[\]])+)]/ +import {visitParents} from 'unist-util-visit-parents' const remarkLintDefinitionSpacing = lintRule( { @@ -79,27 +89,35 @@ const remarkLintDefinitionSpacing = lintRule( * Nothing. */ function (tree, file) { - const value = String(file) + visitParents(tree, function (node, parents) { + if (node.type === 'definition' && node.position && node.label) { + const size = longestStreak(node.label, ' ') - visit(tree, function (node) { - if (node.type === 'definition') { - const end = pointEnd(node) - const start = pointStart(node) + if (size > 1) { + file.message( + 'Unexpected `' + + size + + '` consecutive spaces in definition label, expected `1` space, remove `' + + (size - 1) + + '` ' + + pluralize('space', size - 1), + {ancestors: [...parents, node], place: node.position} + ) + } - if ( - end && - start && - typeof end.offset === 'number' && - typeof start.offset === 'number' - ) { - const match = value.slice(start.offset, end.offset).match(label) + /** @type {Array} */ + const disallowed = [] + if (node.label.includes('\t')) disallowed.push('\\t') + if (node.label.includes('\n')) disallowed.push('\\n') + if (node.label.includes('\r')) disallowed.push('\\r') - if (match && /[ \t\n]{2,}/.test(match[1])) { - file.message( - 'Do not use consecutive whitespace in definition labels', - node - ) - } + for (const disallow of disallowed) { + file.message( + 'Unexpected non-space whitespace character `' + + disallow + + '` in definition label, expected `1` space, replace it', + {ancestors: [...parents, node], place: node.position} + ) } } }) diff --git a/packages/remark-lint-definition-spacing/package.json b/packages/remark-lint-definition-spacing/package.json index 271a4178..d6cac90f 100644 --- a/packages/remark-lint-definition-spacing/package.json +++ b/packages/remark-lint-definition-spacing/package.json @@ -32,9 +32,10 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "longest-streak": "^3.0.0", + "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-definition-spacing/readme.md b/packages/remark-lint-definition-spacing/readme.md index 3d3482f4..525863c1 100644 --- a/packages/remark-lint-definition-spacing/readme.md +++ b/packages/remark-lint-definition-spacing/readme.md @@ -150,25 +150,41 @@ Due to this, it’s recommended to use one space and turn this rule on. ###### In ```markdown -[example domain]: http://example.com "Example Domain" +[planet mercury]: http://example.com ``` ###### Out No messages. -##### `not-ok.md` +##### `not-ok-consecutive.md` ###### In ```markdown -[example␠␠␠␠domain]: http://example.com "Example Domain" +[planet␠␠␠␠mercury]: http://example.com ``` ###### Out ```text -1:1-1:57: Do not use consecutive whitespace in definition labels +1:1-1:40: Unexpected `4` consecutive spaces in definition label, expected `1` space, remove `3` spaces +``` + +##### `not-ok-non-space.md` + +###### In + +```markdown +[pla␉net␊mer␍cury]: http://e.com +``` + +###### Out + +```text +1:1-3:20: Unexpected non-space whitespace character `\t` in definition label, expected `1` space, replace it +1:1-3:20: Unexpected non-space whitespace character `\n` in definition label, expected `1` space, replace it +1:1-3:20: Unexpected non-space whitespace character `\r` in definition label, expected `1` space, replace it ``` ## Compatibility diff --git a/packages/remark-lint-emphasis-marker/index.js b/packages/remark-lint-emphasis-marker/index.js index d63cfa75..6108d144 100644 --- a/packages/remark-lint-emphasis-marker/index.js +++ b/packages/remark-lint-emphasis-marker/index.js @@ -77,51 +77,51 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"config": "*", "name": "ok.md"} + * {"config": "*", "name": "ok-asterisk.md"} * - * *foo* + * *Mercury*. * * @example - * {"config": "*", "name": "not-ok.md", "label": "input"} + * {"config": "*", "label": "input", "name": "not-ok-asterisk.md"} * - * _foo_ + * _Mercury_. * * @example - * {"config": "*", "name": "not-ok.md", "label": "output"} + * {"config": "*", "label": "output", "name": "not-ok-asterisk.md"} * - * 1:1-1:6: Emphasis should use `*` as a marker + * 1:1-1:10: Unexpected emphasis marker `_`, expected `*` * * @example - * {"config": "_", "name": "ok.md"} + * {"config": "_", "name": "ok-underscore.md"} * - * _foo_ + * _Mercury_. * * @example - * {"config": "_", "name": "not-ok.md", "label": "input"} + * {"config": "_", "label": "input", "name": "not-ok-underscore.md"} * - * *foo* + * *Mercury*. * * @example - * {"config": "_", "name": "not-ok.md", "label": "output"} + * {"config": "_", "label": "output", "name": "not-ok-underscore.md"} * - * 1:1-1:6: Emphasis should use `_` as a marker + * 1:1-1:10: Unexpected emphasis marker `*`, expected `_` * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok-consistent.md"} * - * *foo* - * _bar_ + * *Mercury* and _Venus_. * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok-consistent.md"} * - * 2:1-2:6: Emphasis should use `*` as a marker + * 1:15-1:22: Unexpected emphasis marker `_`, expected `*` * * @example - * {"config": "💩", "name": "not-ok.md", "label": "output", "positionless": true} + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} * - * 1:1: Incorrect emphasis marker `💩`: use either `'consistent'`, `'*'`, or `'_'` + * 1:1: Unexpected value `🌍` for `options`, expected `'*'`, `'_'`, or `'consistent'` */ /** @@ -138,7 +138,8 @@ import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintEmphasisMarker = lintRule( { @@ -155,26 +156,56 @@ const remarkLintEmphasisMarker = lintRule( */ function (tree, file, options) { const value = String(file) - let option = options || 'consistent' + /** @type {VFileMessage | undefined} */ + let cause + /** @type {Marker | undefined} */ + let expected - if (option !== '*' && option !== '_' && option !== 'consistent') { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (options === '*' || options === '_') { + expected = options + } else { file.fail( - 'Incorrect emphasis marker `' + - option + - "`: use either `'consistent'`, `'*'`, or `'_'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'*'`, `'_'`, or `'consistent'`" ) } - visit(tree, 'emphasis', function (node) { + visitParents(tree, 'emphasis', function (node, parents) { const start = pointStart(node) if (start && typeof start.offset === 'number') { - const marker = /** @type {Marker} */ (value.charAt(start.offset)) + const actual = value.charAt(start.offset) + + /* c8 ignore next -- should not happen. */ + if (actual !== '*' && actual !== '_') return - if (option === 'consistent') { - option = marker - } else if (marker !== option) { - file.message('Emphasis should use `' + option + '` as a marker', node) + if (expected) { + if (actual !== expected) { + file.message( + 'Unexpected emphasis marker `' + + actual + + '`, expected `' + + expected + + '`', + {ancestors: [...parents, node], cause, place: node.position} + ) + } + } else { + expected = actual + cause = new VFileMessage( + "Emphasis marker style `'" + + actual + + "'` first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place: node.position, + ruleId: 'emphasis-marker', + source: 'remark-lint' + } + ) } } }) diff --git a/packages/remark-lint-emphasis-marker/package.json b/packages/remark-lint-emphasis-marker/package.json index 8a0344cc..a41ff194 100644 --- a/packages/remark-lint-emphasis-marker/package.json +++ b/packages/remark-lint-emphasis-marker/package.json @@ -34,7 +34,8 @@ "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-emphasis-marker/readme.md b/packages/remark-lint-emphasis-marker/readme.md index 59349fdd..1690a131 100644 --- a/packages/remark-lint-emphasis-marker/readme.md +++ b/packages/remark-lint-emphasis-marker/readme.md @@ -184,89 +184,88 @@ Pass `emphasis: '_'` to always use underscores. ## Examples -##### `ok.md` +##### `ok-asterisk.md` When configured with `'*'`. ###### In ```markdown -*foo* +*Mercury*. ``` ###### Out No messages. -##### `not-ok.md` +##### `not-ok-asterisk.md` When configured with `'*'`. ###### In ```markdown -_foo_ +_Mercury_. ``` ###### Out ```text -1:1-1:6: Emphasis should use `*` as a marker +1:1-1:10: Unexpected emphasis marker `_`, expected `*` ``` -##### `ok.md` +##### `ok-underscore.md` When configured with `'_'`. ###### In ```markdown -_foo_ +_Mercury_. ``` ###### Out No messages. -##### `not-ok.md` +##### `not-ok-underscore.md` When configured with `'_'`. ###### In ```markdown -*foo* +*Mercury*. ``` ###### Out ```text -1:1-1:6: Emphasis should use `_` as a marker +1:1-1:10: Unexpected emphasis marker `*`, expected `_` ``` -##### `not-ok.md` +##### `not-ok-consistent.md` ###### In ```markdown -*foo* -_bar_ +*Mercury* and _Venus_. ``` ###### Out ```text -2:1-2:6: Emphasis should use `*` as a marker +1:15-1:22: Unexpected emphasis marker `_`, expected `*` ``` ##### `not-ok.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect emphasis marker `💩`: use either `'consistent'`, `'*'`, or `'_'` +1:1: Unexpected value `🌍` for `options`, expected `'*'`, `'_'`, or `'consistent'` ``` ## Compatibility diff --git a/packages/remark-lint-fenced-code-flag/index.js b/packages/remark-lint-fenced-code-flag/index.js index 3d6b2f06..58f2a8e3 100644 --- a/packages/remark-lint-fenced-code-flag/index.js +++ b/packages/remark-lint-fenced-code-flag/index.js @@ -59,66 +59,79 @@ * @example * {"name": "ok.md"} * - * ```alpha - * bravo() + * ```markdown + * # Mercury * ``` * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * * ``` - * alpha() + * mercury() * ``` - * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:1-3:4: Missing code language flag + * 1:1-3:4: Unexpected missing fenced code language flag in info string, expected keyword * * @example - * {"name": "ok.md", "config": {"allowEmpty": true}} + * {"config": {"allowEmpty": true}, "name": "ok-allow-empty.md"} * * ``` - * alpha() + * mercury() * ``` * * @example - * {"name": "not-ok.md", "config": {"allowEmpty": false}, "label": "input"} + * {"config": {"allowEmpty": false}, "label": "input", "name": "not-ok-allow-empty.md"} * * ``` - * alpha() + * mercury() * ``` + * @example + * {"config": {"allowEmpty": false}, "label": "output", "name": "not-ok-allow-empty.md"} + * + * 1:1-3:4: Unexpected missing fenced code language flag in info string, expected keyword * * @example - * {"name": "not-ok.md", "config": {"allowEmpty": false}, "label": "output"} + * {"config": ["markdown"], "name": "ok-array.md"} * - * 1:1-3:4: Missing code language flag + * ```markdown + * # Mercury + * ``` * * @example - * {"name": "ok.md", "config": ["alpha"]} + * {"config": {"flags":["markdown"]}, "name": "ok-options.md"} * - * ```alpha - * bravo() + * ```markdown + * # Mercury * ``` * * @example - * {"name": "ok.md", "config": {"flags":["alpha"]}} + * {"config": ["markdown"], "label": "input", "name": "not-ok-array.md"} * - * ```alpha - * bravo() + * ```javascript + * mercury() * ``` + * @example + * {"config": ["markdown"], "label": "output", "name": "not-ok-array.md"} + * + * 1:1-3:4: Unexpected fenced code language flag `javascript` in info string, expected `markdown` * * @example - * {"name": "not-ok.md", "config": ["charlie"], "label": "input"} + * {"config": ["javascript", "markdown", "mdx", "typescript"], "label": "input", "name": "not-ok-long-array.md"} * - * ```alpha - * bravo() + * ```html + *

Mercury

* ``` + * @example + * {"config": ["javascript", "markdown", "mdx", "typescript"], "label": "output", "name": "not-ok-long-array.md"} + * + * 1:1-3:4: Unexpected fenced code language flag `html` in info string, expected `javascript`, `markdown`, `mdx`, … * * @example - * {"name": "not-ok.md", "config": ["charlie"], "label": "output"} + * {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true} * - * 1:1-3:4: Incorrect code language flag + * 1:1: Unexpected value `🌍` for `options`, expected array or object */ /** @@ -135,13 +148,15 @@ * other flags will result in a warning (optional). */ +import {quotation} from 'quotation' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const fence = /^ {0,3}([~`])\1{2,}/ -/** @type {ReadonlyArray} */ -const emptyFlags = [] + +const listFormat = new Intl.ListFormat('en', {type: 'disjunction'}) +const listFormatUnit = new Intl.ListFormat('en', {type: 'unit'}) const remarkLintFencedCodeFlag = lintRule( { @@ -159,24 +174,45 @@ const remarkLintFencedCodeFlag = lintRule( function (tree, file, options) { const value = String(file) let allowEmpty = false - let allowed = emptyFlags + /** @type {ReadonlyArray | undefined} */ + let allowed - if (options && typeof options === 'object') { + if (options === null || options === undefined) { + // Empty. + } else if (typeof options === 'object') { // Note: casts because `isArray` and `readonly` don’t mix. if (Array.isArray(options)) { const flags = /** @type {ReadonlyArray} */ (options) allowed = flags } else { const settings = /** @type {Options} */ (options) - allowEmpty = Boolean(settings.allowEmpty) + allowEmpty = settings.allowEmpty === true if (settings.flags) { allowed = settings.flags } } + } else { + file.fail( + 'Unexpected value `' + + options + + '` for `options`, expected array or object' + ) + } + + /** @type {string} */ + let allowedDisplay + + if (allowed) { + allowedDisplay = + allowed.length > 3 + ? listFormatUnit.format([...quotation(allowed.slice(0, 3), '`'), '…']) + : listFormat.format(quotation(allowed, '`')) + } else { + allowedDisplay = 'keyword' } - visit(tree, 'code', function (node) { + visitParents(tree, 'code', function (node, parents) { const end = pointEnd(node) const start = pointStart(node) @@ -187,14 +223,24 @@ const remarkLintFencedCodeFlag = lintRule( typeof start.offset === 'number' ) { if (node.lang) { - if (allowed.length > 0 && !allowed.includes(node.lang)) { - file.message('Incorrect code language flag', node) + if (allowed && !allowed.includes(node.lang)) { + file.message( + 'Unexpected fenced code language flag `' + + node.lang + + '` in info string, expected ' + + allowedDisplay, + {ancestors: [...parents, node], place: node.position} + ) } - } else { + } else if (!allowEmpty) { const slice = value.slice(start.offset, end.offset) - if (!allowEmpty && fence.test(slice)) { - file.message('Missing code language flag', node) + if (fence.test(slice)) { + file.message( + 'Unexpected missing fenced code language flag in info string, expected ' + + allowedDisplay, + {ancestors: [...parents, node], place: node.position} + ) } } } diff --git a/packages/remark-lint-fenced-code-flag/package.json b/packages/remark-lint-fenced-code-flag/package.json index e41be38b..92a2cf28 100644 --- a/packages/remark-lint-fenced-code-flag/package.json +++ b/packages/remark-lint-fenced-code-flag/package.json @@ -34,9 +34,10 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "quotation": "^2.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-fenced-code-flag/readme.md b/packages/remark-lint-fenced-code-flag/readme.md index 05f65280..4bf0faac 100644 --- a/packages/remark-lint-fenced-code-flag/readme.md +++ b/packages/remark-lint-fenced-code-flag/readme.md @@ -165,8 +165,8 @@ It’s recommended to instead use a certain flag for plain text (such as ###### In ````markdown -```alpha -bravo() +```markdown +# Mercury ``` ```` @@ -180,17 +180,17 @@ No messages. ````markdown ``` -alpha() +mercury() ``` ```` ###### Out ```text -1:1-3:4: Missing code language flag +1:1-3:4: Unexpected missing fenced code language flag in info string, expected keyword ``` -##### `ok.md` +##### `ok-allow-empty.md` When configured with `{ allowEmpty: true }`. @@ -198,7 +198,7 @@ When configured with `{ allowEmpty: true }`. ````markdown ``` -alpha() +mercury() ``` ```` @@ -206,7 +206,7 @@ alpha() No messages. -##### `not-ok.md` +##### `not-ok-allow-empty.md` When configured with `{ allowEmpty: false }`. @@ -214,25 +214,25 @@ When configured with `{ allowEmpty: false }`. ````markdown ``` -alpha() +mercury() ``` ```` ###### Out ```text -1:1-3:4: Missing code language flag +1:1-3:4: Unexpected missing fenced code language flag in info string, expected keyword ``` -##### `ok.md` +##### `ok-array.md` -When configured with `[ 'alpha' ]`. +When configured with `[ 'markdown' ]`. ###### In ````markdown -```alpha -bravo() +```markdown +# Mercury ``` ```` @@ -240,15 +240,15 @@ bravo() No messages. -##### `ok.md` +##### `ok-options.md` -When configured with `{ flags: [ 'alpha' ] }`. +When configured with `{ flags: [ 'markdown' ] }`. ###### In ````markdown -```alpha -bravo() +```markdown +# Mercury ``` ```` @@ -256,22 +256,50 @@ bravo() No messages. -##### `not-ok.md` +##### `not-ok-array.md` + +When configured with `[ 'markdown' ]`. + +###### In + +````markdown +```javascript +mercury() +``` +```` + +###### Out + +```text +1:1-3:4: Unexpected fenced code language flag `javascript` in info string, expected `markdown` +``` -When configured with `[ 'charlie' ]`. +##### `not-ok-long-array.md` + +When configured with `[ 'javascript', 'markdown', 'mdx', 'typescript' ]`. ###### In ````markdown -```alpha -bravo() +```html +

Mercury

``` ```` ###### Out ```text -1:1-3:4: Incorrect code language flag +1:1-3:4: Unexpected fenced code language flag `html` in info string, expected `javascript`, `markdown`, `mdx`, … +``` + +##### `not-ok-options.md` + +When configured with `'🌍'`. + +###### Out + +```text +1:1: Unexpected value `🌍` for `options`, expected array or object ``` ## Compatibility diff --git a/packages/remark-lint-fenced-code-marker/index.js b/packages/remark-lint-fenced-code-marker/index.js index 6e2808ac..d23657ca 100644 --- a/packages/remark-lint-fenced-code-marker/index.js +++ b/packages/remark-lint-fenced-code-marker/index.js @@ -68,71 +68,70 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md"} + * {"name": "ok-indented.md"} * * Indented code blocks are not affected by this rule: * - * bravo() + * mercury() * * @example - * {"name": "ok.md", "config": "`"} + * {"config": "`", "name": "ok-tick.md"} * - * ```alpha - * bravo() + * ```javascript + * mercury() * ``` * * ``` - * charlie() + * venus() * ``` * * @example - * {"name": "ok.md", "config": "~"} + * {"config": "~", "name": "ok-tilde.md"} * - * ~~~alpha - * bravo() + * ~~~javascript + * mercury() * ~~~ * * ~~~ - * charlie() + * venus() * ~~~ * * @example - * {"name": "not-ok-consistent-tick.md", "label": "input"} + * {"label": "input", "name": "not-ok-consistent-tick.md"} * - * ```alpha - * bravo() + * ```javascript + * mercury() * ``` * * ~~~ - * charlie() + * venus() * ~~~ - * * @example - * {"name": "not-ok-consistent-tick.md", "label": "output"} + * {"label": "output", "name": "not-ok-consistent-tick.md"} * - * 5:1-7:4: Fenced code should use `` ` `` as a marker + * 5:1-7:4: Unexpected fenced code marker `~`, expected `` ` `` * * @example - * {"name": "not-ok-consistent-tilde.md", "label": "input"} + * {"label": "input", "name": "not-ok-consistent-tilde.md"} * - * ~~~alpha - * bravo() + * ~~~javascript + * mercury() * ~~~ * * ``` - * charlie() + * venus() * ``` - * * @example - * {"name": "not-ok-consistent-tilde.md", "label": "output"} + * {"label": "output", "name": "not-ok-consistent-tilde.md"} * - * 5:1-7:4: Fenced code should use `~` as a marker + * 5:1-7:4: Unexpected fenced code marker `` ` ``, expected `~` * * @example - * {"name": "not-ok-incorrect.md", "config": "💩", "label": "output", "positionless": true} + * {"config": "🌍", "label": "output", "name": "not-ok-incorrect.md", "positionless": true} * - * 1:1: Incorrect fenced code marker `💩`: use either `'consistent'`, `` '`' ``, or `'~'` + * 1:1: Unexpected value `🌍` for `options`, expected ``'`'``, `'~'`, or `'consistent'` */ /** @@ -149,7 +148,8 @@ import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintFencedCodeMarker = lintRule( { @@ -165,38 +165,59 @@ const remarkLintFencedCodeMarker = lintRule( * Nothing. */ function (tree, file, options) { - let option = options || 'consistent' - const contents = String(file) + const value = String(file) + /** @type {VFileMessage | undefined} */ + let cause + /** @type {Marker | undefined} */ + let expected - if (option !== 'consistent' && option !== '~' && option !== '`') { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (options === '`' || options === '~') { + expected = options + } else { file.fail( - 'Incorrect fenced code marker `' + - option + - "`: use either `'consistent'`, `` '`' ``, or `'~'`" + 'Unexpected value `' + + options + + "` for `options`, expected ``'`'``, `'~'`, or `'consistent'`" ) } - visit(tree, 'code', function (node) { + visitParents(tree, 'code', function (node, parents) { const start = pointStart(node) if (start && typeof start.offset === 'number') { - const marker = contents + const actual = value .slice(start.offset, start.offset + 4) .replace(/^\s+/, '') .charAt(0) // Ignore unfenced code blocks. - if (marker === '`' || marker === '~') { - if (option === 'consistent') { - option = marker - } else if (marker !== option) { + if (actual !== '`' && actual !== '~') return + + if (expected) { + if (actual !== expected) { file.message( - 'Fenced code should use `' + - (option === '~' ? option : '` ` `') + - '` as a marker', - node + 'Unexpected fenced code marker ' + + (actual === '~' ? '`~`' : '`` ` ``') + + ', expected ' + + (expected === '~' ? '`~`' : '`` ` ``'), + {ancestors: [...parents, node], cause, place: node.position} ) } + } else { + expected = actual + cause = new VFileMessage( + 'Fenced code marker style ' + + (actual === '~' ? "`'~'`" : "``'`'``") + + " first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place: node.position, + ruleId: 'fenced-code-marker', + source: 'remark-lint' + } + ) } } }) diff --git a/packages/remark-lint-fenced-code-marker/package.json b/packages/remark-lint-fenced-code-marker/package.json index 9ba80a97..c4b542b4 100644 --- a/packages/remark-lint-fenced-code-marker/package.json +++ b/packages/remark-lint-fenced-code-marker/package.json @@ -35,7 +35,8 @@ "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-fenced-code-marker/readme.md b/packages/remark-lint-fenced-code-marker/readme.md index 68895855..48636ee4 100644 --- a/packages/remark-lint-fenced-code-marker/readme.md +++ b/packages/remark-lint-fenced-code-marker/readme.md @@ -175,33 +175,33 @@ Pass `fence: '~'` to always use tildes. ## Examples -##### `ok.md` +##### `ok-indented.md` ###### In ```markdown Indented code blocks are not affected by this rule: - bravo() + mercury() ``` ###### Out No messages. -##### `ok.md` +##### `ok-tick.md` When configured with ``'`'``. ###### In ````markdown -```alpha -bravo() +```javascript +mercury() ``` ``` -charlie() +venus() ``` ```` @@ -209,19 +209,19 @@ charlie() No messages. -##### `ok.md` +##### `ok-tilde.md` When configured with `'~'`. ###### In ```markdown -~~~alpha -bravo() +~~~javascript +mercury() ~~~ ~~~ -charlie() +venus() ~~~ ``` @@ -234,19 +234,19 @@ No messages. ###### In ````markdown -```alpha -bravo() +```javascript +mercury() ``` ~~~ -charlie() +venus() ~~~ ```` ###### Out ```text -5:1-7:4: Fenced code should use `` ` `` as a marker +5:1-7:4: Unexpected fenced code marker `~`, expected `` ` `` ``` ##### `not-ok-consistent-tilde.md` @@ -254,29 +254,29 @@ charlie() ###### In ````markdown -~~~alpha -bravo() +~~~javascript +mercury() ~~~ ``` -charlie() +venus() ``` ```` ###### Out ```text -5:1-7:4: Fenced code should use `~` as a marker +5:1-7:4: Unexpected fenced code marker `` ` ``, expected `~` ``` ##### `not-ok-incorrect.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect fenced code marker `💩`: use either `'consistent'`, `` '`' ``, or `'~'` +1:1: Unexpected value `🌍` for `options`, expected ``'`'``, `'~'`, or `'consistent'` ``` ## Compatibility diff --git a/packages/remark-lint-file-extension/index.js b/packages/remark-lint-file-extension/index.js index ee876c9b..a39a18be 100644 --- a/packages/remark-lint-file-extension/index.js +++ b/packages/remark-lint-file-extension/index.js @@ -62,6 +62,7 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "readme.md"} * @@ -74,18 +75,23 @@ * @example * {"config": {"allowExtensionless": false}, "label": "output", "name": "readme", "positionless": true} * - * 1:1: Incorrect extension: use `mdx` or `md` + * 1:1: Unexpected missing file extension, expected `mdx` or `md` * * @example * {"label": "output", "name": "readme.mkd", "positionless": true} * - * 1:1: Incorrect extension: use `mdx` or `md` + * 1:1: Unexpected file extension `mkd`, expected `mdx` or `md` * * @example * {"config": "mkd", "name": "readme.mkd"} * * @example - * {"config": ["mkd"], "name": "readme.mkd"} + * {"config": ["markdown", "md", "mdown", "mdwn", "mdx", "mkd", "mkdn", "mkdown", "ron"], "label": "input", "name": "readme.css", "positionless": true} + * + * @example + * {"config": ["markdown", "md", "mdown", "mdwn", "mdx", "mkd", "mkdn", "mkdown", "ron"], "label": "output", "name": "readme.css"} + * + * 1:1: Unexpected file extension `css`, expected `markdown`, `md`, `mdown`, … */ /** @@ -93,24 +99,25 @@ */ /** - * @typedef {ReadonlyArray | string} Extensions + * @typedef {Array | string} Extensions * File extension(s). * * @typedef Options * Configuration. * @property {boolean | null | undefined} [allowExtensionless=true] * Allow no file extension such as `AUTHORS` or `LICENSE` (default: `true`). - * @property {Extensions | null | undefined} [extensions=['mdx', 'md']] + * @property {Readonly | null | undefined} [extensions=['mdx', 'md']] * Allowed file extension(s) (default: `['mdx', 'md']`). */ -import {lintRule} from 'unified-lint-rule' import {quotation} from 'quotation' +import {lintRule} from 'unified-lint-rule' /** @type {ReadonlyArray} */ const defaultExtensions = ['mdx', 'md'] const listFormat = new Intl.ListFormat('en', {type: 'disjunction'}) +const listFormatUnit = new Intl.ListFormat('en', {type: 'unit'}) const remarkLintFileExtension = lintRule( { @@ -126,7 +133,7 @@ const remarkLintFileExtension = lintRule( * Nothing. */ function (_, file, options) { - let extensions = defaultExtensions + let expected = defaultExtensions let allowExtensionless = true /** @type {Readonly | null | undefined} */ let extensionsValue @@ -147,18 +154,25 @@ const remarkLintFileExtension = lintRule( } if (Array.isArray(extensionsValue)) { - extensions = /** @type {ReadonlyArray} */ (extensionsValue) + expected = /** @type {ReadonlyArray} */ (extensionsValue) } else if (typeof extensionsValue === 'string') { - extensions = [extensionsValue] + expected = [extensionsValue] } const extname = file.extname - const extension = extname ? extname.slice(1) : undefined + const actual = extname ? extname.slice(1) : undefined + const expectedDisplay = + expected.length > 3 + ? listFormatUnit.format([...quotation(expected.slice(0, 3), '`'), '…']) + : listFormat.format(quotation(expected, '`')) - if (extension ? !extensions.includes(extension) : !allowExtensionless) { + if (actual ? !expected.includes(actual) : !allowExtensionless) { file.message( - 'Incorrect extension: use ' + - listFormat.format(quotation(extensions, '`')) + (actual + ? 'Unexpected file extension `' + actual + '`' + : 'Unexpected missing file extension') + + ', expected ' + + expectedDisplay ) } } diff --git a/packages/remark-lint-file-extension/readme.md b/packages/remark-lint-file-extension/readme.md index c3f5c50d..83a79138 100644 --- a/packages/remark-lint-file-extension/readme.md +++ b/packages/remark-lint-file-extension/readme.md @@ -193,7 +193,7 @@ When configured with `{ allowExtensionless: false }`. ###### Out ```text -1:1: Incorrect extension: use `mdx` or `md` +1:1: Unexpected missing file extension, expected `mdx` or `md` ``` ##### `readme.mkd` @@ -201,7 +201,7 @@ When configured with `{ allowExtensionless: false }`. ###### Out ```text -1:1: Incorrect extension: use `mdx` or `md` +1:1: Unexpected file extension `mkd`, expected `mdx` or `md` ``` ##### `readme.mkd` @@ -212,13 +212,21 @@ When configured with `'mkd'`. No messages. -##### `readme.mkd` +##### `readme.css` -When configured with `[ 'mkd' ]`. +When configured with `[ + 'markdown', 'md', + 'mdown', 'mdwn', + 'mdx', 'mkd', + 'mkdn', 'mkdown', + 'ron' +]`. ###### Out -No messages. +```text +1:1: Unexpected file extension `css`, expected `markdown`, `md`, `mdown`, … +``` ## Compatibility diff --git a/packages/remark-lint-final-definition/index.js b/packages/remark-lint-final-definition/index.js index 252c140f..5ee19f2d 100644 --- a/packages/remark-lint-final-definition/index.js +++ b/packages/remark-lint-final-definition/index.js @@ -38,64 +38,83 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * Paragraph. + * Mercury. + * + * [venus]: http://example.com + * + * @example + * {"name": "ok.md"} * - * [example]: http://example.com "Example Domain" + * [mercury]: http://example.com/mercury/ + * [venus]: http://example.com/venus/ * * @example - * {"name": "not-ok.md", "label": "input"} + * {"name": "ok-html-comments.md"} * - * Paragraph. + * Mercury. * - * [example]: http://example.com "Example Domain" + * [venus]: http://example.com/venus/ * - * Another paragraph. + * + * + * [earth]: http://example.com/earth/ * * @example - * {"name": "not-ok.md", "label": "output"} + * {"name": "ok-mdx-comments.mdx", "mdx": true} + * + * Mercury. + * + * [venus]: http://example.com/venus/ + * + * {/* Comments in expressions in MDX are ignored. *␀/} * - * 3:1-3:47: Move definitions to the end of the file (after `5:19`) + * [earth]: http://example.com/earth/ * * @example - * {"name": "ok-html-comments.md"} + * {"label": "input", "name": "not-ok.md"} * - * Paragraph. + * Mercury. * - * [example-1]: http://example.com/one/ + * [venus]: https://example.com/venus/ * - * + * Earth. + * @example + * {"label": "output", "name": "not-ok.md"} * - * [example-2]: http://example.com/two/ + * 3:1-3:36: Unexpected definition before last content, expected definitions after line `5` * * @example - * {"name": "ok-mdx-comments.mdx", "mdx": true} + * {"gfm": true, "label": "input", "name": "gfm.md"} * - * Paragraph. + * Mercury. * - * [example-1]: http://example.com/one/ + * [^venus]: + * **Venus** is the second planet from + * the Sun. * - * {/* Comments are fine in MDX. *␀/} + * Earth. + * @example + * {"gfm": true, "label": "output", "name": "gfm.md"} * - * [example-2]: http://example.com/two/ + * 3:1-5:13: Unexpected footnote definition before last content, expected definitions after line `7` */ /** - * @typedef {import('mdast').Definition} Definition - * @typedef {import('mdast').FootnoteDefinition} FootnoteDefinition + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root - * - * @typedef {import('unist').Point} Point */ /// +import {ok as assert} from 'devlop' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {stringifyPosition} from 'unist-util-stringify-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintFinalDefinition = lintRule( { @@ -109,14 +128,14 @@ const remarkLintFinalDefinition = lintRule( * Nothing. */ function (tree, file) { - /** @type {Array} */ - const definitions = [] - /** @type {Point | undefined} */ - let last + /** @type {Array>} */ + const definitionStacks = [] + /** @type {Array | undefined} */ + let contentAncestors - visit(tree, function (node) { + visitParents(tree, function (node, parents) { if (node.type === 'definition' || node.type === 'footnoteDefinition') { - definitions.push(node) + definitionStacks.push([...parents, node]) } else if ( node.type === 'root' || // Ignore HTML comments. @@ -128,24 +147,42 @@ const remarkLintFinalDefinition = lintRule( ) { // Empty. } else { - const place = pointEnd(node) - - if (place) { - last = place - } + contentAncestors = [...parents, node] } }) - for (const node of definitions) { - const point = pointStart(node) + const content = contentAncestors ? contentAncestors.at(-1) : undefined + const contentEnd = pointEnd(content) + + if (contentEnd) { + assert(content) // Always defined. + assert(contentAncestors) // Always defined. + + for (const definitionAncestors of definitionStacks) { + const definition = definitionAncestors.at(-1) + assert(definition) // Always defined. - if (point && last && point.line < last.line) { - file.message( - 'Move definitions to the end of the file (after `' + - stringifyPosition(last) + - '`)', - node - ) + const definitionStart = pointStart(definition) + + if (definitionStart && definitionStart.line < contentEnd.line) { + file.message( + 'Unexpected ' + + (definition.type === 'footnoteDefinition' ? 'footnote ' : '') + + 'definition before last content, expected definitions after line `' + + contentEnd.line + + '`', + { + ancestors: definitionAncestors, + cause: new VFileMessage('Last content defined here', { + ancestors: contentAncestors, + place: content.position, + ruleId: 'final-definition', + source: 'remark-lint' + }), + place: definition.position + } + ) + } } } } diff --git a/packages/remark-lint-final-definition/package.json b/packages/remark-lint-final-definition/package.json index 77c964f3..e93d269c 100644 --- a/packages/remark-lint-final-definition/package.json +++ b/packages/remark-lint-final-definition/package.json @@ -33,12 +33,12 @@ ], "dependencies": { "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", + "devlop": "^1.0.0", "mdast-util-mdx": "^3.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-final-definition/readme.md b/packages/remark-lint-final-definition/readme.md index 3f3c3003..7588df7e 100644 --- a/packages/remark-lint-final-definition/readme.md +++ b/packages/remark-lint-final-definition/readme.md @@ -147,45 +147,40 @@ If you prefer that, turn on this rule. ###### In ```markdown -Paragraph. +Mercury. -[example]: http://example.com "Example Domain" +[venus]: http://example.com ``` ###### Out No messages. -##### `not-ok.md` +##### `ok.md` ###### In ```markdown -Paragraph. - -[example]: http://example.com "Example Domain" - -Another paragraph. +[mercury]: http://example.com/mercury/ +[venus]: http://example.com/venus/ ``` ###### Out -```text -3:1-3:47: Move definitions to the end of the file (after `5:19`) -``` +No messages. ##### `ok-html-comments.md` ###### In ```markdown -Paragraph. +Mercury. -[example-1]: http://example.com/one/ +[venus]: http://example.com/venus/ - + -[example-2]: http://example.com/two/ +[earth]: http://example.com/earth/ ``` ###### Out @@ -200,19 +195,60 @@ No messages. > MDX ([`remark-mdx`][github-remark-mdx]). ```mdx -Paragraph. +Mercury. -[example-1]: http://example.com/one/ +[venus]: http://example.com/venus/ -{/* Comments are fine in MDX. */} +{/* Comments in expressions in MDX are ignored. */} -[example-2]: http://example.com/two/ +[earth]: http://example.com/earth/ ``` ###### Out No messages. +##### `not-ok.md` + +###### In + +```markdown +Mercury. + +[venus]: https://example.com/venus/ + +Earth. +``` + +###### Out + +```text +3:1-3:36: Unexpected definition before last content, expected definitions after line `5` +``` + +##### `gfm.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Mercury. + +[^venus]: + **Venus** is the second planet from + the Sun. + +Earth. +``` + +###### Out + +```text +3:1-5:13: Unexpected footnote definition before last content, expected definitions after line `7` +``` + ## Compatibility Projects maintained by the unified collective are compatible with maintained @@ -282,6 +318,8 @@ abide by its terms. [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c +[github-remark-gfm]: https://github.com/remarkjs/remark-gfm + [github-remark-lint]: https://github.com/remarkjs/remark-lint [github-remark-mdx]: https://mdxjs.com/packages/remark-mdx/ diff --git a/packages/remark-lint-final-newline/index.js b/packages/remark-lint-final-newline/index.js index 6de935a5..a2d80910 100644 --- a/packages/remark-lint-final-newline/index.js +++ b/packages/remark-lint-final-newline/index.js @@ -45,7 +45,7 @@ * ###### In * * ```markdown - * Alpha␊ + * Mercury␊ * ``` * * ###### Out @@ -57,13 +57,13 @@ * ###### In * * ```markdown - * Bravo␀ + * Mercury␀ * ``` * * ###### Out * * ```text - * 1:6: Missing newline character at end of file + * 1:8: Unexpected missing final newline character, expected line feed (`\n`) at end of file * ``` * * @module final-newline @@ -76,6 +76,7 @@ * @typedef {import('mdast').Root} Root */ +import {ok as assert} from 'devlop' import {lintRule} from 'unified-lint-rule' import {location} from 'vfile-location' @@ -95,8 +96,17 @@ const remarkLintFinalNewline = lintRule( const end = location(file).toPoint(value.length) const last = value.length - 1 - if (end && last > -1 && value.charAt(last) !== '\n') { - file.message('Missing newline character at end of file', end) + assert(end) // Always defined. + + if ( + // Empty is fine. + last !== -1 && + value.charAt(last) !== '\n' + ) { + file.message( + 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', + end + ) } } ) diff --git a/packages/remark-lint-final-newline/package.json b/packages/remark-lint-final-newline/package.json index 52f26dc7..c5e98a55 100644 --- a/packages/remark-lint-final-newline/package.json +++ b/packages/remark-lint-final-newline/package.json @@ -33,6 +33,7 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "unified-lint-rule": "^2.0.0", "vfile-location": "^5.0.0" }, diff --git a/packages/remark-lint-final-newline/readme.md b/packages/remark-lint-final-newline/readme.md index 3a7ffff7..92096adb 100644 --- a/packages/remark-lint-final-newline/readme.md +++ b/packages/remark-lint-final-newline/readme.md @@ -150,7 +150,7 @@ always adds final line endings. ###### In ```markdown -Alpha␊ +Mercury␊ ``` ###### Out @@ -162,13 +162,13 @@ No messages. ###### In ```markdown -Bravo␀ +Mercury␀ ``` ###### Out ```text -1:6: Missing newline character at end of file +1:8: Unexpected missing final newline character, expected line feed (`\n`) at end of file ``` ## Compatibility diff --git a/packages/remark-lint-first-heading-level/index.js b/packages/remark-lint-first-heading-level/index.js index c31098da..1ef8e274 100644 --- a/packages/remark-lint-first-heading-level/index.js +++ b/packages/remark-lint-first-heading-level/index.js @@ -25,16 +25,6 @@ * * Transform ([`Transformer` from `unified`][github-unified-transformer]). * - * ### `Depth` - * - * Depth (TypeScript type). - * - * ###### Type - * - * ```ts - * type Depth = 1 | 2 | 3 | 4 | 5 | 6 - * ``` - * * ### `Options` * * Configuration (TypeScript type). @@ -42,7 +32,7 @@ * ###### Type * * ```ts - * type Options = Depth + * type Options = 1 | 2 | 3 | 4 | 5 | 6 * ``` * * ## Recommendation @@ -55,7 +45,6 @@ * in which case a value of `2` can be defined here or the rule can be turned * off. * - * [api-depth]: #depth * [api-options]: #options * [api-remark-lint-first-heading-level]: #unifieduseremarklintfirstheadinglevel-options * [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer @@ -64,93 +53,55 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT - * @example - * {"name": "ok.md"} - * - * # The default is to expect a level one heading - * - * @example - * {"name": "ok-html.md"} - * - *

An HTML heading is also seen by this rule.

- * - * @example - * {"name": "ok-delayed.md"} - * - * You can use markdown content before the heading. - * - *
Or non-heading HTML
- * - *

So the first heading, be it HTML or markdown, is checked

- * - * @example - * {"name": "not-ok.md", "label": "input"} - * - * ## Bravo - * - * Paragraph. * * @example - * {"name": "not-ok.md", "label": "output"} + * {"name": "ok.md"} * - * 1:1-1:9: First heading level should be `1` + * # Mercury * * @example - * {"name": "not-ok-html.md", "label": "input"} + * {"name": "ok-delay.md"} * - *

Charlie

+ * Mercury. * - * Paragraph. + * # Venus * * @example - * {"name": "not-ok-html.md", "label": "output"} + * {"label": "input", "name": "not-ok.md"} * - * 1:1-1:17: First heading level should be `1` + * ## Mercury * + * Venus. * @example - * {"name": "ok.md", "config": 2} - * - * ## Delta + * {"label": "output", "name": "not-ok.md"} * - * Paragraph. + * 1:1-1:11: Unexpected first heading rank `2`, expected rank `1` * * @example - * {"name": "ok-html.md", "config": 2} + * {"config": 2, "name": "ok.md"} * - *

Echo

+ * ## Mercury * - * Paragraph. + * Venus. * * @example - * {"name": "not-ok.md", "config": 2, "label": "input"} - * - * # Foxtrot - * - * Paragraph. + * {"name": "ok-html.md"} * - * @example - * {"name": "not-ok.md", "config": 2, "label": "output"} + *
Mercury.
* - * 1:1-1:10: First heading level should be `2` + *

Venus

* * @example - * {"name": "not-ok-html.md", "config": 2, "label": "input"} - * - *

Golf

+ * {"mdx": true, "name": "ok-mdx.mdx"} * - * Paragraph. - * - * @example - * {"name": "not-ok-html.md", "config": 2, "label": "output"} + *
Mercury.
* - * 1:1-1:14: First heading level should be `2` + *

Venus

* * @example - * {"mdx": true, "name": "ok.mdx"} + * {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true} * - * In MDX, JSX is supported. - * - *

First heading

+ * 1:1: Unexpected value `🌍` for `options`, expected `1`, `2`, `3`, `4`, `5`, or `6` */ /** @@ -159,18 +110,14 @@ */ /** - * @typedef {Heading['depth']} Depth - * Styles. - * - * @typedef {Depth} Options + * @typedef {1 | 2 | 3 | 4 | 5 | 6} Options * Configuration. */ /// import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {EXIT, visit} from 'unist-util-visit' +import {EXIT, visitParents} from 'unist-util-visit-parents' const htmlRe = /An HTML heading is also seen by this rule. -``` +Mercury. -###### Out - -No messages. - -##### `ok-delayed.md` - -###### In - -```markdown -You can use markdown content before the heading. - -
Or non-heading HTML
- -

So the first heading, be it HTML or markdown, is checked

+# Venus ``` ###### Out @@ -211,31 +185,15 @@ No messages. ###### In ```markdown -## Bravo - -Paragraph. -``` - -###### Out - -```text -1:1-1:9: First heading level should be `1` -``` - -##### `not-ok-html.md` - -###### In - -```markdown -

Charlie

+## Mercury -Paragraph. +Venus. ``` ###### Out ```text -1:1-1:17: First heading level should be `1` +1:1-1:11: Unexpected first heading rank `2`, expected rank `1` ``` ##### `ok.md` @@ -245,9 +203,9 @@ When configured with `2`. ###### In ```markdown -## Delta +## Mercury -Paragraph. +Venus. ``` ###### Out @@ -256,73 +214,45 @@ No messages. ##### `ok-html.md` -When configured with `2`. - ###### In ```markdown -

Echo

+
Mercury.
-Paragraph. +

Venus

``` ###### Out No messages. -##### `not-ok.md` - -When configured with `2`. +##### `ok-mdx.mdx` ###### In -```markdown -# Foxtrot - -Paragraph. -``` +> 👉 **Note**: this example uses +> MDX ([`remark-mdx`][github-remark-mdx]). -###### Out +```mdx +
Mercury.
-```text -1:1-1:10: First heading level should be `2` +

Venus

``` -##### `not-ok-html.md` +###### Out -When configured with `2`. +No messages. -###### In +##### `not-ok-options.md` -```markdown -

Golf

- -Paragraph. -``` +When configured with `'🌍'`. ###### Out ```text -1:1-1:14: First heading level should be `2` -``` - -##### `ok.mdx` - -###### In - -> 👉 **Note**: this example uses -> MDX ([`remark-mdx`][github-remark-mdx]). - -```mdx -In MDX, JSX is supported. - -

First heading

+1:1: Unexpected value `🌍` for `options`, expected `1`, `2`, `3`, `4`, `5`, or `6` ``` -###### Out - -No messages. - ## Compatibility Projects maintained by the unified collective are compatible with maintained @@ -348,8 +278,6 @@ abide by its terms. [MIT][file-license] © [Titus Wormer][author] -[api-depth]: #depth - [api-options]: #options [api-remark-lint-first-heading-level]: #unifieduseremarklintfirstheadinglevel-options diff --git a/packages/remark-lint-hard-break-spaces/index.js b/packages/remark-lint-hard-break-spaces/index.js index 49f5eab8..3d011588 100644 --- a/packages/remark-lint-hard-break-spaces/index.js +++ b/packages/remark-lint-hard-break-spaces/index.js @@ -41,19 +41,29 @@ * @example * {"name": "ok.md"} * - * Lorem ipsum␠␠ - * dolor sit amet + * **Mercury** is the first planet from the Sun␠␠ + * and the smallest in the Solar System. * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * Lorem ipsum␠␠␠ - * dolor sit amet. + * **Mercury** is the first planet from the Sun␠␠␠ + * and the smallest in the Solar System. + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 1:45-2:1: Unexpected `3` spaces for hard break, expected `2` spaces + * + * @example + * {"gfm": true, "label": "input", "name": "containers.md"} * + * [^mercury]: + * > * > * **Mercury** is the first planet from the Sun␠␠␠ + * > > and the smallest in the Solar System. * @example - * {"name": "not-ok.md", "label": "output"} + * {"gfm": true, "label": "output", "name": "containers.md"} * - * 1:12-2:1: Use two spaces for hard line breaks + * 2:57-3:1: Unexpected `3` spaces for hard break, expected `2` spaces */ /** @@ -79,8 +89,8 @@ const remarkLintHardBreakSpaces = lintRule( const value = String(file) visit(tree, 'break', function (node) { - const start = pointStart(node) const end = pointEnd(node) + const start = pointStart(node) if ( end && @@ -88,13 +98,18 @@ const remarkLintHardBreakSpaces = lintRule( typeof end.offset === 'number' && typeof start.offset === 'number' ) { - const slice = value - .slice(start.offset, end.offset) - .split('\n', 1)[0] - .replace(/\r$/, '') + const slice = value.slice(start.offset, end.offset) + + let actual = 0 + while (slice.charCodeAt(actual) === 32) actual++ - if (slice.length > 2) { - file.message('Use two spaces for hard line breaks', node) + if (actual > 2) { + file.message( + 'Unexpected `' + + actual + + '` spaces for hard break, expected `2` spaces', + node + ) } } }) diff --git a/packages/remark-lint-hard-break-spaces/package.json b/packages/remark-lint-hard-break-spaces/package.json index 29681692..fafe5bb6 100644 --- a/packages/remark-lint-hard-break-spaces/package.json +++ b/packages/remark-lint-hard-break-spaces/package.json @@ -48,7 +48,8 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-hard-break-spaces/readme.md b/packages/remark-lint-hard-break-spaces/readme.md index b1b5911a..2804c010 100644 --- a/packages/remark-lint-hard-break-spaces/readme.md +++ b/packages/remark-lint-hard-break-spaces/readme.md @@ -148,8 +148,8 @@ Due to this, it’s recommended to turn this rule on. ###### In ```markdown -Lorem ipsum␠␠ -dolor sit amet +**Mercury** is the first planet from the Sun␠␠ +and the smallest in the Solar System. ``` ###### Out @@ -161,14 +161,33 @@ No messages. ###### In ```markdown -Lorem ipsum␠␠␠ -dolor sit amet. +**Mercury** is the first planet from the Sun␠␠␠ +and the smallest in the Solar System. ``` ###### Out ```text -1:12-2:1: Use two spaces for hard line breaks +1:45-2:1: Unexpected `3` spaces for hard break, expected `2` spaces +``` + +##### `containers.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +[^mercury]: + > * > * **Mercury** is the first planet from the Sun␠␠␠ + > > and the smallest in the Solar System. +``` + +###### Out + +```text +2:57-3:1: Unexpected `3` spaces for hard break, expected `2` spaces ``` ## Compatibility @@ -240,6 +259,8 @@ abide by its terms. [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c +[github-remark-gfm]: https://github.com/remarkjs/remark-gfm + [github-remark-lint]: https://github.com/remarkjs/remark-lint [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer diff --git a/packages/remark-lint-heading-increment/index.js b/packages/remark-lint-heading-increment/index.js index 754b24a5..8bb5e3ea 100644 --- a/packages/remark-lint-heading-increment/index.js +++ b/packages/remark-lint-heading-increment/index.js @@ -44,50 +44,88 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * # Alpha + * # Mercury * - * ## Bravo + * ## Nomenclature * * @example - * {"name": "not-ok.md", "label": "input"} + * {"name": "also-ok.md"} + * + * #### Impact basins and craters + * + * #### Plains + * + * #### Compressional features + * + * @example + * {"label": "input", "name": "not-ok.md"} + * + * # Mercury * - * # Charlie + * ### Internal structure * - * ### Delta + * ### Surface geology + * + * ## Observation history + * + * #### Mariner 10 * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 3:1-3:10: Heading levels should increment by one level at a time + * 3:1-3:23: Unexpected heading rank `3`, exected rank `2` + * 5:1-5:20: Unexpected heading rank `3`, exected rank `2` + * 9:1-9:16: Unexpected heading rank `4`, exected rank `3` * * @example - * {"name": "html.md"} + * {"label": "input", "name": "html.md"} * - * In markdown, HTML is supported. + * # Mercury * - *

First heading

+ * Mercury is the first planet from the Sun and the smallest + * in the Solar System. * + *

Internal structure

+ * + *

Orbit, rotation, and longitude

* @example - * {"name": "ok.mdx", "mdx": true} + * {"label": "output", "name": "html.md"} + * + * 6:1-6:28: Unexpected heading rank `3`, exected rank `2` + * + * @example + * {"mdx": true, "name": "mdx.mdx"} + * + * # Mercury * - * In MDX, JSX is supported. + * Mercury is the first planet from the Sun and the smallest + * in the Solar System. * - *

First heading

+ *

Internal structure

+ * + *

Orbit, rotation, and longitude

+ * @example + * {"label": "output", "mdx": true, "name": "mdx.mdx"} + * + * 6:1-6:28: Unexpected heading rank `3`, exected rank `2` */ /** * @typedef {import('mdast').Heading} Heading + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root */ /// +import {ok as assert} from 'devlop' import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const htmlRe = / | undefined>} */ + const stack = [] - visit(tree, function (node) { - const place = position(node) + visitParents(tree, function (node, parents) { + const rank = inferRank(node) - if (place) { - /** @type {Heading['depth'] | undefined} */ - let rank + if (rank) { + let index = rank + /** @type {Array | undefined} */ + let closestAncestors - if (node.type === 'heading') { - rank = node.depth - } else if (node.type === 'html') { - const results = node.value.match(htmlRe) - rank = results - ? /** @type {Heading['depth']} */ (Number(results[1])) - : undefined - } else if ( - (node.type === 'mdxJsxFlowElement' || - node.type === 'mdxJsxTextElement') && - node.name - ) { - const results = node.name.match(jsxNameRe) - rank = results - ? /** @type {Heading['depth']} */ (Number(results[1])) - : undefined + while (index--) { + if (stack[index]) { + closestAncestors = stack[index] + break + } } - if (rank) { - if (previous && rank > previous + 1) { + if (closestAncestors) { + const parent = closestAncestors.at(-1) + assert(parent) // Always defined. + const parentRank = inferRank(parent) + assert(parentRank) // Always defined. + + if (node.position && rank > parentRank + 1) { file.message( - 'Heading levels should increment by one level at a time', - place + 'Unexpected heading rank `' + + rank + + '`, exected rank `' + + (parentRank + 1) + + '`', + { + ancestors: [...parents, node], + cause: new VFileMessage('Parent heading defined here', { + ancestors: closestAncestors, + place: parent.position, + source: 'remark-lint', + ruleId: 'heading-increment' + }), + place: node.position + } ) } - - previous = rank } + + stack[rank] = [...parents, node] + // Drop things after it. + stack.length = rank + 1 } }) } ) export default remarkLintHeadingIncrement + +/** + * Get rank of a node. + * + * @param {Nodes} node + * Node. + * @returns {Heading['depth'] | undefined} + * Rank, if heading. + */ +function inferRank(node) { + /** @type {Heading['depth'] | undefined} */ + let rank + + if (node.type === 'heading') { + rank = node.depth + } else if (node.type === 'html') { + const results = node.value.match(htmlRe) + rank = results + ? /** @type {Heading['depth']} */ (Number(results[1])) + : undefined + } else if ( + (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') && + node.name + ) { + const results = node.name.match(jsxNameRe) + rank = results + ? /** @type {Heading['depth']} */ (Number(results[1])) + : undefined + } + + return rank +} diff --git a/packages/remark-lint-heading-increment/package.json b/packages/remark-lint-heading-increment/package.json index ba17bb0e..f73ccb58 100644 --- a/packages/remark-lint-heading-increment/package.json +++ b/packages/remark-lint-heading-increment/package.json @@ -33,10 +33,11 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "mdast-util-mdx": "^3.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-heading-increment/readme.md b/packages/remark-lint-heading-increment/readme.md index 55551d02..c0bba9fe 100644 --- a/packages/remark-lint-heading-increment/readme.md +++ b/packages/remark-lint-heading-increment/readme.md @@ -153,9 +153,25 @@ it’s recommended that this rule is turned on. ###### In ```markdown -# Alpha +# Mercury -## Bravo +## Nomenclature +``` + +###### Out + +No messages. + +##### `also-ok.md` + +###### In + +```markdown +#### Impact basins and craters + +#### Plains + +#### Compressional features ``` ###### Out @@ -167,15 +183,23 @@ No messages. ###### In ```markdown -# Charlie +# Mercury + +### Internal structure -### Delta +### Surface geology + +## Observation history + +#### Mariner 10 ``` ###### Out ```text -3:1-3:10: Heading levels should increment by one level at a time +3:1-3:23: Unexpected heading rank `3`, exected rank `2` +5:1-5:20: Unexpected heading rank `3`, exected rank `2` +9:1-9:16: Unexpected heading rank `4`, exected rank `3` ``` ##### `html.md` @@ -183,16 +207,23 @@ No messages. ###### In ```markdown -In markdown, HTML is supported. +# Mercury + +Mercury is the first planet from the Sun and the smallest +in the Solar System. -

First heading

+

Internal structure

+ +

Orbit, rotation, and longitude

``` ###### Out -No messages. +```text +6:1-6:28: Unexpected heading rank `3`, exected rank `2` +``` -##### `ok.mdx` +##### `mdx.mdx` ###### In @@ -200,14 +231,21 @@ No messages. > MDX ([`remark-mdx`][github-remark-mdx]). ```mdx -In MDX, JSX is supported. +# Mercury + +Mercury is the first planet from the Sun and the smallest +in the Solar System. + +

Internal structure

-

First heading

+

Orbit, rotation, and longitude

``` ###### Out -No messages. +```text +6:1-6:28: Unexpected heading rank `3`, exected rank `2` +``` ## Compatibility diff --git a/packages/remark-lint-heading-style/index.js b/packages/remark-lint-heading-style/index.js index c1b1b275..76fc34c0 100644 --- a/packages/remark-lint-heading-style/index.js +++ b/packages/remark-lint-heading-style/index.js @@ -81,55 +81,55 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md", "config": "atx"} + * {"config": "atx", "name": "ok.md"} * - * # Alpha + * # Mercury * - * ## Bravo + * ## Venus * - * ### Charlie + * ### Earth * * @example - * {"name": "ok.md", "config": "atx-closed"} + * {"config": "atx-closed", "name": "ok.md"} * - * # Delta ## + * # Mercury ## * - * ## Echo ## + * ## Venus ## * - * ### Foxtrot ### + * ### Earth ### * * @example - * {"name": "ok.md", "config": "setext"} + * {"config": "setext", "name": "ok.md"} * - * Golf - * ==== + * Mercury + * ======= * - * Hotel + * Venus * ----- * - * ### India + * ### Earth * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * Juliett + * Mercury * ======= * - * ## Kilo - * - * ### Lima ### + * ## Venus * + * ### Earth ### * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 4:1-4:8: Headings should use setext - * 6:1-6:13: Headings should use setext + * 4:1-4:9: Unexpected ATX heading, expected setext + * 6:1-6:14: Unexpected ATX (closed) heading, expected setext * * @example - * {"name": "not-ok.md", "config": "💩", "label": "output", "positionless": true} + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} * - * 1:1: Incorrect heading style type `💩`: use either `'consistent'`, `'atx'`, `'atx-closed'`, or `'setext'` + * 1:1: Unexpected value `🌍` for `options`, expected `'atx'`, `'atx-closed'`, `'setext'`, or `'consistent'` */ /** @@ -147,7 +147,8 @@ import {headingStyle} from 'mdast-util-heading-style' import {lintRule} from 'unified-lint-rule' import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintHeadingStyle = lintRule( { @@ -163,30 +164,55 @@ const remarkLintHeadingStyle = lintRule( * Nothing. */ function (tree, file, options) { - let option = options || 'consistent' + /** @type {VFileMessage | undefined} */ + let cause + /** @type {Style | undefined} */ + let expected - if ( - option !== 'atx' && - option !== 'atx-closed' && - option !== 'consistent' && - option !== 'setext' + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if ( + options === 'atx' || + options === 'atx-closed' || + options === 'setext' ) { + expected = options + } else { file.fail( - 'Incorrect heading style type `' + - option + - "`: use either `'consistent'`, `'atx'`, `'atx-closed'`, or `'setext'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'atx'`, `'atx-closed'`, `'setext'`, or `'consistent'`" ) } - visit(tree, 'heading', function (node) { + visitParents(tree, 'heading', function (node, parents) { const place = position(node) + const actual = headingStyle(node, expected) - if (place) { - if (option === 'consistent') { - /* c8 ignore next -- funky nodes perhaps cannot be detected. */ - option = headingStyle(node) || 'consistent' - } else if (headingStyle(node, option) !== option) { - file.message('Headings should use ' + option, place) + if (actual) { + if (expected) { + if (place && actual !== expected) { + file.message( + 'Unexpected ' + + displayStyle(actual) + + ' heading, expected ' + + displayStyle(expected), + {ancestors: [...parents, node], cause, place} + ) + } + } else { + expected = actual + cause = new VFileMessage( + 'Heading style ' + + displayStyle(expected) + + " first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place, + ruleId: 'heading-style', + source: 'remark-lint' + } + ) } } }) @@ -194,3 +220,17 @@ const remarkLintHeadingStyle = lintRule( ) export default remarkLintHeadingStyle + +/** + * @param {Style} style + * Style. + * @returns {string} + * Display. + */ +function displayStyle(style) { + return style === 'atx' + ? 'ATX' + : style === 'atx-closed' + ? 'ATX (closed)' + : 'setext' +} diff --git a/packages/remark-lint-heading-style/package.json b/packages/remark-lint-heading-style/package.json index cf307477..117c8544 100644 --- a/packages/remark-lint-heading-style/package.json +++ b/packages/remark-lint-heading-style/package.json @@ -37,7 +37,8 @@ "mdast-util-heading-style": "^3.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-heading-style/readme.md b/packages/remark-lint-heading-style/readme.md index eb0e3e9f..d00d744f 100644 --- a/packages/remark-lint-heading-style/readme.md +++ b/packages/remark-lint-heading-style/readme.md @@ -195,11 +195,11 @@ When configured with `'atx'`. ###### In ```markdown -# Alpha +# Mercury -## Bravo +## Venus -### Charlie +### Earth ``` ###### Out @@ -213,11 +213,11 @@ When configured with `'atx-closed'`. ###### In ```markdown -# Delta ## +# Mercury ## -## Echo ## +## Venus ## -### Foxtrot ### +### Earth ### ``` ###### Out @@ -231,13 +231,13 @@ When configured with `'setext'`. ###### In ```markdown -Golf -==== +Mercury +======= -Hotel +Venus ----- -### India +### Earth ``` ###### Out @@ -249,29 +249,29 @@ No messages. ###### In ```markdown -Juliett +Mercury ======= -## Kilo +## Venus -### Lima ### +### Earth ### ``` ###### Out ```text -4:1-4:8: Headings should use setext -6:1-6:13: Headings should use setext +4:1-4:9: Unexpected ATX heading, expected setext +6:1-6:14: Unexpected ATX (closed) heading, expected setext ``` ##### `not-ok.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect heading style type `💩`: use either `'consistent'`, `'atx'`, `'atx-closed'`, or `'setext'` +1:1: Unexpected value `🌍` for `options`, expected `'atx'`, `'atx-closed'`, `'setext'`, or `'consistent'` ``` ## Compatibility diff --git a/packages/remark-lint-linebreak-style/index.js b/packages/remark-lint-linebreak-style/index.js index ef18622a..959f20e1 100644 --- a/packages/remark-lint-linebreak-style/index.js +++ b/packages/remark-lint-linebreak-style/index.js @@ -55,7 +55,7 @@ * * ## Fix * - * [`remark-stringify`][github-remark-stringify] always uses Unix linebreaks. + * [`remark-stringify`][github-remark-stringify] always uses Unix line endings. * * [api-options]: #options * [api-remark-lint-linebreak-style]: #unifieduseremarklintlinebreakstyle-options @@ -67,35 +67,56 @@ * @author Titus Wormer * @copyright 2017 Titus Wormer * @license MIT + * * @example * {"name": "ok-consistent-as-windows.md"} * - * Alpha␍␊Bravo␍␊ + * Mercury␍␊and␍␊Venus. * * @example * {"name": "ok-consistent-as-unix.md"} * - * Alpha␊Bravo␊ + * Mercury␊and␊Venus. + * + * @example + * {"config": "unix", "label": "input", "name": "not-ok-unix.md", "positionless": true} + * + * Mercury.␍␊ * * @example - * {"name": "not-ok-unix.md", "label": "input", "config": "unix", "positionless": true} + * {"config": "unix", "label": "output", "name": "not-ok-unix.md", "positionless": true} * - * Alpha␍␊ + * 1:10: Unexpected windows (`\r\n`) line ending, expected unix (`\n`) line endings * * @example - * {"name": "not-ok-unix.md", "label": "output", "config": "unix"} + * {"config": "windows", "label": "input", "name": "not-ok-windows.md", "positionless": true} * - * 1:7: Expected linebreaks to be unix (`\n`), not windows (`\r\n`) + * Mercury.␊ * * @example - * {"name": "not-ok-windows.md", "label": "input", "config": "windows", "positionless": true} + * {"config": "windows", "label": "output", "name": "not-ok-windows.md", "positionless": true} * - * Alpha␊ + * 1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings * * @example - * {"name": "not-ok-windows.md", "label": "output", "config": "windows"} + * {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true} + * + * 1:1: Unexpected value `🌍` for `options`, expected `'unix'`, `'windows'`, or `'consistent'` + * + * @example + * {"config": "windows", "label": "input", "name": "many.md", "positionless": true} + * + * Mercury.␊Venus.␊Earth.␊Mars.␊Jupiter.␊Saturn.␊Uranus.␊Neptune.␊ * - * 1:6: Expected linebreaks to be windows (`\r\n`), not unix (`\n`) + * @example + * {"config": "windows", "label": "output", "name": "many.md", "positionless": true} + * + * 1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings + * 2:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings + * 3:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings + * 4:6: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings + * 5:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings + * 6:8: Unexpected large number of incorrect line endings, stopping */ /** @@ -110,10 +131,12 @@ * Styles. */ +import {ok as assert} from 'devlop' import {lintRule} from 'unified-lint-rule' import {location} from 'vfile-location' +import {VFileMessage} from 'vfile-message' -const escaped = {unix: '\\n', windows: '\\r\\n'} +const max = 5 const remarkLintLinebreakStyle = lintRule( { @@ -129,28 +152,60 @@ const remarkLintLinebreakStyle = lintRule( * Nothing. */ function (_, file, options) { - let option = options || 'consistent' const value = String(file) const toPoint = location(value).toPoint let index = value.indexOf('\n') + /** @type {VFileMessage | undefined} */ + let cause + /** @type {Style | undefined} */ + let expected + + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (options === 'unix' || options === 'windows') { + expected = options + } else { + file.fail( + 'Unexpected value `' + + options + + "` for `options`, expected `'unix'`, `'windows'`, or `'consistent'`" + ) + } + + let messages = 0 while (index !== -1) { - const type = value.charAt(index - 1) === '\r' ? 'windows' : 'unix' + const actual = value.charAt(index - 1) === '\r' ? 'windows' : 'unix' + const place = toPoint(index) + assert(place) // Always defined. - if (option === 'consistent') { - option = type - } else if (option !== type) { - file.message( - 'Expected linebreaks to be ' + - option + - ' (`' + - escaped[option] + - '`), not ' + - type + - ' (`' + - escaped[type] + - '`)', - toPoint(index) + if (expected) { + if (expected !== actual) { + if (messages === max) { + file.info( + 'Unexpected large number of incorrect line endings, stopping', + {place} + ) + return + } + + file.message( + 'Unexpected ' + + displayStyle(actual) + + ' line ending, expected ' + + displayStyle(expected) + + ' line endings', + {cause, place} + ) + messages++ + } + } else { + expected = actual + cause = new VFileMessage( + 'Line ending style ' + + displayStyle(expected) + + " first defined for `'consistent'` here", + {place, ruleId: 'linebreak-style', source: 'remark-lint'} ) } @@ -160,3 +215,13 @@ const remarkLintLinebreakStyle = lintRule( ) export default remarkLintLinebreakStyle + +/** + * @param {Style} style + * Style. + * @returns {string} + * Display. + */ +function displayStyle(style) { + return style === 'unix' ? 'unix (`\\n`)' : 'windows (`\\r\\n`)' +} diff --git a/packages/remark-lint-linebreak-style/package.json b/packages/remark-lint-linebreak-style/package.json index 00d3ef97..5eea647f 100644 --- a/packages/remark-lint-linebreak-style/package.json +++ b/packages/remark-lint-linebreak-style/package.json @@ -38,8 +38,10 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "unified-lint-rule": "^2.0.0", - "vfile-location": "^5.0.0" + "vfile-location": "^5.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-linebreak-style/readme.md b/packages/remark-lint-linebreak-style/readme.md index e1fd8d6c..a35a80fa 100644 --- a/packages/remark-lint-linebreak-style/readme.md +++ b/packages/remark-lint-linebreak-style/readme.md @@ -165,7 +165,7 @@ used. ## Fix -[`remark-stringify`][github-remark-stringify] always uses Unix linebreaks. +[`remark-stringify`][github-remark-stringify] always uses Unix line endings. ## Examples @@ -174,7 +174,7 @@ used. ###### In ```markdown -Alpha␍␊Bravo␍␊ +Mercury␍␊and␍␊Venus. ``` ###### Out @@ -186,7 +186,7 @@ No messages. ###### In ```markdown -Alpha␊Bravo␊ +Mercury␊and␊Venus. ``` ###### Out @@ -200,13 +200,13 @@ When configured with `'unix'`. ###### In ```markdown -Alpha␍␊ +Mercury.␍␊ ``` ###### Out ```text -1:7: Expected linebreaks to be unix (`\n`), not windows (`\r\n`) +1:10: Unexpected windows (`\r\n`) line ending, expected unix (`\n`) line endings ``` ##### `not-ok-windows.md` @@ -216,13 +216,44 @@ When configured with `'windows'`. ###### In ```markdown -Alpha␊ +Mercury.␊ ``` ###### Out ```text -1:6: Expected linebreaks to be windows (`\r\n`), not unix (`\n`) +1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings +``` + +##### `not-ok-options.md` + +When configured with `'🌍'`. + +###### Out + +```text +1:1: Unexpected value `🌍` for `options`, expected `'unix'`, `'windows'`, or `'consistent'` +``` + +##### `many.md` + +When configured with `'windows'`. + +###### In + +```markdown +Mercury.␊Venus.␊Earth.␊Mars.␊Jupiter.␊Saturn.␊Uranus.␊Neptune.␊ +``` + +###### Out + +```text +1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings +2:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings +3:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings +4:6: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings +5:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings +6:8: Unexpected large number of incorrect line endings, stopping ``` ## Compatibility diff --git a/packages/remark-lint-link-title-style/index.js b/packages/remark-lint-link-title-style/index.js index 2a4564d2..781f9d1a 100644 --- a/packages/remark-lint-link-title-style/index.js +++ b/packages/remark-lint-link-title-style/index.js @@ -3,7 +3,8 @@ * * ## What is this? * - * This package checks the style of link title markers. + * This package checks the style of link (*and* image and definition) title + * markers. * * ## When should I use this? * @@ -59,7 +60,7 @@ * * ## Fix * - * [`remark-stringify`][github-remark-stringify] formats titles with double + * [`remark-stringify`][github-remark-stringify] formats titles with double * quotes by default. * Pass `quote: "'"` to use single quotes. * There is no option to use parens. @@ -74,82 +75,90 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT - * @example - * {"name": "ok.md", "config": "\""} - * - * [Example](http://example.com#without-title) - * [Example](http://example.com "Example Domain") - * ![Example](http://example.com "Example Domain") * - * [Example]: http://example.com "Example Domain" + * @example + * {"name": "ok-consistent.md"} * - * You can use parens in URLs if they’re not a title (see GH-166): + * [Mercury](http://example.com/mercury/), + * [Venus](http://example.com/venus/ "Go to Venus"), and + * ![Earth](http://example.com/earth/ "Go to Earth"). * - * [Example](#Heading-(optional)) + * [Mars]: http://example.com/mars/ "Go to Mars" * * @example - * {"name": "not-ok.md", "label": "input", "config": "\""} + * {"label": "input", "name": "not-ok-consistent.md"} * - * [Example]: http://example.com 'Example Domain' + * [Mercury](http://example.com/mercury/ "Go to Mercury") and + * ![Venus](http://example.com/venus/ 'Go to Venus'). * + * [Earth]: http://example.com/earth/ (Go to Earth) * @example - * {"name": "not-ok.md", "label": "output", "config": "\""} + * {"label": "output", "name": "not-ok-consistent.md"} * - * 1:31-1:47: Titles should use `"` as a quote + * 2:1-2:50: Unexpected title markers `'`, expected `"` + * 4:1-4:49: Unexpected title markers `'('` and `')'`, expected `"` * * @example - * {"name": "ok.md", "config": "'"} + * {"config": "\"", "name": "ok-double.md"} * - * [Example](http://example.com#without-title) - * [Example](http://example.com 'Example Domain') - * ![Example](http://example.com 'Example Domain') + * [Mercury](http://example.com/mercury/ "Go to Mercury"). * - * [Example]: http://example.com 'Example Domain' + * @example + * {"config": "\"", "label": "input", "name": "not-ok-double.md"} * + * [Mercury](http://example.com/mercury/ 'Go to Mercury'). * @example - * {"name": "not-ok.md", "label": "input", "config": "'"} + * {"config": "\"", "label": "output", "name": "not-ok-double.md"} * - * [Example]: http://example.com "Example Domain" + * 1:1-1:55: Unexpected title markers `'`, expected `"` * * @example - * {"name": "not-ok.md", "label": "output", "config": "'"} + * {"config": "'", "name": "ok-single.md"} * - * 1:31-1:47: Titles should use `'` as a quote + * [Mercury](http://example.com/mercury/ 'Go to Mercury'). * * @example - * {"name": "ok.md", "config": "()"} + * {"config": "'", "label": "input", "name": "not-ok-single.md"} * - * [Example](http://example.com#without-title) - * [Example](http://example.com (Example Domain)) - * ![Example](http://example.com (Example Domain)) + * [Mercury](http://example.com/mercury/ "Go to Mercury"). + * @example + * {"config": "'", "label": "output", "name": "not-ok-single.md"} * - * [Example]: http://example.com (Example Domain) + * 1:1-1:55: Unexpected title markers `"`, expected `'` * * @example - * {"name": "not-ok.md", "label": "input", "config": "()"} + * {"config": "()", "name": "ok-paren.md"} + * + * [Mercury](http://example.com/mercury/ (Go to Mercury)). * - * [Example](http://example.com 'Example Domain') + * @example + * {"config": "()", "label": "input", "name": "not-ok-paren.md"} * + * [Mercury](http://example.com/mercury/ "Go to Mercury"). * @example - * {"name": "not-ok.md", "label": "output", "config": "()"} + * {"config": "()", "label": "output", "name": "not-ok-paren.md"} * - * 1:30-1:46: Titles should use `()` as a quote + * 1:1-1:55: Unexpected title markers `"`, expected `'('` and `')'` * * @example - * {"name": "not-ok.md", "label": "input"} + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} * - * [Example](http://example.com "Example Domain") - * [Example](http://example.com 'Example Domain') + * 1:1: Unexpected value `🌍` for `options`, expected `'"'`, `"'"`, `'()'`, or `'consistent'` * * @example - * {"name": "not-ok.md", "label": "output"} + * {"config": "\"", "name": "ok-parens-in-url.md"} + * + * Parens in URLs work correctly: * - * 2:30-2:46: Titles should use `"` as a quote + * [Mercury](http://example.com/(mercury) "Go to Mercury") and + * [Venus](http://example.com/(venus)). * * @example - * {"name": "not-ok.md", "config": "💩", "label": "output", "positionless": true} + * {"config": "\"", "name": "ok-whitespace.md"} + * + * Trailing whitespace works correctly: * - * 1:1: Incorrect link title style marker `💩`: use either `'consistent'`, `'"'`, `'\''`, or `'()'` + * [Mercury](http://example.com/mercury/␠"Go to Mercury"␠). */ /** @@ -165,15 +174,9 @@ */ import {lintRule} from 'unified-lint-rule' -import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' -import {location} from 'vfile-location' - -const markers = { - '"': '"', - "'": "'", - ')': '(' -} +import {pointEnd} from 'unist-util-position' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintLinkTitleStyle = lintRule( { @@ -190,78 +193,88 @@ const remarkLintLinkTitleStyle = lintRule( */ function (tree, file, options) { const value = String(file) - const loc = location(file) - const option = options || 'consistent' - // @ts-expect-error: allow `(` too, even though untyped. - let look = option === '()' || option === '(' ? ')' : option + /** @type {Style | undefined} */ + let expected + /** @type {VFileMessage | undefined} */ + let cause - if (look !== 'consistent' && !Object.hasOwn(markers, look)) { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + /* c8 ignore next 3 */ + // @ts-expect-error: to do: remove. + } else if (options === '(') { + expected = '()' + } else if (options === '"' || options === "'" || options === '()') { + expected = options + } else { file.fail( - 'Incorrect link title style marker `' + - look + - "`: use either `'consistent'`, `'\"'`, `'\\''`, or `'()'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'\"'`, `\"'\"`, `'()'`, or `'consistent'`" ) } - visit(tree, function (node) { + visitParents(tree, function (node, parents) { if ( node.type === 'definition' || node.type === 'image' || node.type === 'link' ) { - const tail = - 'children' in node - ? node.children[node.children.length - 1] - : undefined - const begin = tail ? pointEnd(tail) : pointStart(node) - const end = pointEnd(node) + // Exit w/o title. + if (!node.title) return - if ( - !begin || - !end || - typeof begin.offset !== 'number' || - typeof end.offset !== 'number' - ) { - return - } - - let last = end.offset - 1 + const end = pointEnd(node) + let endIndex = end ? end.offset : undefined - if (node.type !== 'definition') { - last-- - } + // Exit w/o position. + if (!endIndex) return - const final = /** @type {keyof markers} */ (value.charAt(last)) + // `)` + if (node.type !== 'definition') endIndex-- - // Exit if the final marker is not a known marker. - if (!(final in markers)) { - return + // Whitespace. + let before = value.charCodeAt(endIndex - 1) + while (before === 9 || before === 32) { + endIndex-- + before = value.charCodeAt(endIndex - 1) } - const initial = markers[final] - - // Find the starting delimiter - const first = value.lastIndexOf(initial, last - 1) + /** @type {Style | undefined} */ + const actual = + before === 34 /* `"` */ + ? '"' + : before === 39 /* `'` */ + ? "'" + : before === 41 /* `)` */ + ? '()' + : /* c8 ignore next -- we should find a correct marker. */ + undefined - // Exit if there’s no starting delimiter, the starting delimiter is before - // the start of the node, or if it’s not preceded by whitespace. - if (first <= begin.offset || !/\s/.test(value.charAt(first - 1))) { - return - } + /* c8 ignore next -- we should find a correct marker. */ + if (!actual) return - if (look === 'consistent') { - look = final - } else if (look !== final) { - const start = loc.toPoint(first) - const end = loc.toPoint(last + 1) - /* c8 ignore next -- we get here if we have offsets. */ - const place = start && end ? {start, end} : undefined - - file.message( - 'Titles should use `' + - (look === ')' ? '()' : look) + - '` as a quote', - place + if (expected) { + if (actual !== expected) { + file.message( + 'Unexpected title markers ' + + displayStyle(actual) + + ', expected ' + + displayStyle(expected), + {ancestors: [...parents, node], cause, place: node.position} + ) + } + } else { + expected = actual + cause = new VFileMessage( + 'Title marker style ' + + displayStyle(expected) + + " first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place: node.position, + ruleId: 'link-title-style', + source: 'remark-lint' + } ) } } @@ -270,3 +283,13 @@ const remarkLintLinkTitleStyle = lintRule( ) export default remarkLintLinkTitleStyle + +/** + * @param {Style} style + * Style. + * @returns {string} + * Display. + */ +function displayStyle(style) { + return style === '"' ? '`"`' : style === "'" ? "`'`" : "`'('` and `')'`" +} diff --git a/packages/remark-lint-link-title-style/package.json b/packages/remark-lint-link-title-style/package.json index be78fed0..ddefa23f 100644 --- a/packages/remark-lint-link-title-style/package.json +++ b/packages/remark-lint-link-title-style/package.json @@ -36,8 +36,8 @@ "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile-location": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { @@ -50,7 +50,9 @@ "prettier": true, "rules": { "capitalized-comments": "off", - "unicorn/prefer-at": "off" + "unicorn/prefer-at": "off", + "unicorn/prefer-code-point": "off", + "unicorn/prefer-switch": "off" } } } diff --git a/packages/remark-lint-link-title-style/readme.md b/packages/remark-lint-link-title-style/readme.md index 727eab06..bed22146 100644 --- a/packages/remark-lint-link-title-style/readme.md +++ b/packages/remark-lint-link-title-style/readme.md @@ -32,7 +32,8 @@ ## What is this? -This package checks the style of link title markers. +This package checks the style of link (*and* image and definition) title +markers. ## When should I use this? @@ -174,144 +175,180 @@ markdown, so it’s recommended to configure this rule with `'"'`. ## Fix -[`remark-stringify`][github-remark-stringify] formats titles with double +[`remark-stringify`][github-remark-stringify] formats titles with double quotes by default. Pass `quote: "'"` to use single quotes. There is no option to use parens. ## Examples -##### `ok.md` +##### `ok-consistent.md` -When configured with `'"'`. +###### In + +```markdown +[Mercury](http://example.com/mercury/), +[Venus](http://example.com/venus/ "Go to Venus"), and +![Earth](http://example.com/earth/ "Go to Earth"). + +[Mars]: http://example.com/mars/ "Go to Mars" +``` + +###### Out + +No messages. + +##### `not-ok-consistent.md` ###### In ```markdown -[Example](http://example.com#without-title) -[Example](http://example.com "Example Domain") -![Example](http://example.com "Example Domain") +[Mercury](http://example.com/mercury/ "Go to Mercury") and +![Venus](http://example.com/venus/ 'Go to Venus'). + +[Earth]: http://example.com/earth/ (Go to Earth) +``` -[Example]: http://example.com "Example Domain" +###### Out + +```text +2:1-2:50: Unexpected title markers `'`, expected `"` +4:1-4:49: Unexpected title markers `'('` and `')'`, expected `"` +``` -You can use parens in URLs if they’re not a title (see GH-166): +##### `ok-double.md` -[Example](#Heading-(optional)) +When configured with `'"'`. + +###### In + +```markdown +[Mercury](http://example.com/mercury/ "Go to Mercury"). ``` ###### Out No messages. -##### `not-ok.md` +##### `not-ok-double.md` When configured with `'"'`. ###### In ```markdown -[Example]: http://example.com 'Example Domain' +[Mercury](http://example.com/mercury/ 'Go to Mercury'). ``` ###### Out ```text -1:31-1:47: Titles should use `"` as a quote +1:1-1:55: Unexpected title markers `'`, expected `"` ``` -##### `ok.md` +##### `ok-single.md` When configured with `"'"`. ###### In ```markdown -[Example](http://example.com#without-title) -[Example](http://example.com 'Example Domain') -![Example](http://example.com 'Example Domain') - -[Example]: http://example.com 'Example Domain' +[Mercury](http://example.com/mercury/ 'Go to Mercury'). ``` ###### Out No messages. -##### `not-ok.md` +##### `not-ok-single.md` When configured with `"'"`. ###### In ```markdown -[Example]: http://example.com "Example Domain" +[Mercury](http://example.com/mercury/ "Go to Mercury"). ``` ###### Out ```text -1:31-1:47: Titles should use `'` as a quote +1:1-1:55: Unexpected title markers `"`, expected `'` ``` -##### `ok.md` +##### `ok-paren.md` When configured with `'()'`. ###### In ```markdown -[Example](http://example.com#without-title) -[Example](http://example.com (Example Domain)) -![Example](http://example.com (Example Domain)) - -[Example]: http://example.com (Example Domain) +[Mercury](http://example.com/mercury/ (Go to Mercury)). ``` ###### Out No messages. -##### `not-ok.md` +##### `not-ok-paren.md` When configured with `'()'`. ###### In ```markdown -[Example](http://example.com 'Example Domain') +[Mercury](http://example.com/mercury/ "Go to Mercury"). ``` ###### Out ```text -1:30-1:46: Titles should use `()` as a quote +1:1-1:55: Unexpected title markers `"`, expected `'('` and `')'` ``` ##### `not-ok.md` +When configured with `'🌍'`. + +###### Out + +```text +1:1: Unexpected value `🌍` for `options`, expected `'"'`, `"'"`, `'()'`, or `'consistent'` +``` + +##### `ok-parens-in-url.md` + +When configured with `'"'`. + ###### In ```markdown -[Example](http://example.com "Example Domain") -[Example](http://example.com 'Example Domain') +Parens in URLs work correctly: + +[Mercury](http://example.com/(mercury) "Go to Mercury") and +[Venus](http://example.com/(venus)). ``` ###### Out -```text -2:30-2:46: Titles should use `"` as a quote -``` +No messages. -##### `not-ok.md` +##### `ok-whitespace.md` -When configured with `'💩'`. +When configured with `'"'`. -###### Out +###### In -```text -1:1: Incorrect link title style marker `💩`: use either `'consistent'`, `'"'`, `'\''`, or `'()'` +```markdown +Trailing whitespace works correctly: + +[Mercury](http://example.com/mercury/␠"Go to Mercury"␠). ``` +###### Out + +No messages. + ## Compatibility Projects maintained by the unified collective are compatible with maintained diff --git a/packages/remark-lint-list-item-bullet-indent/index.js b/packages/remark-lint-list-item-bullet-indent/index.js index 609089d7..48d9f2cd 100644 --- a/packages/remark-lint-list-item-bullet-indent/index.js +++ b/packages/remark-lint-list-item-bullet-indent/index.js @@ -30,9 +30,9 @@ * While it is possible to use an indent to align ordered lists on their marker: * * ```markdown - * 1. One - * 10. Ten - * 100. Hundred + * 1. Mercury + * 10. Venus + * 100. Earth * ``` * * …such a style is uncommon and hard to maintain as adding a 10th item @@ -56,34 +56,33 @@ * @example * {"name": "ok.md"} * - * Paragraph. + * Mercury. * - * * List item - * * List item + * * Venus. + * * Earth. * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * Paragraph. + * Mercury. * - * ␠* List item - * ␠* List item + * ␠* Venus. + * ␠* Earth. * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 3:2: Incorrect indentation before bullet: remove 1 space - * 4:2: Incorrect indentation before bullet: remove 1 space + * 3:2: Unexpected `1` space before list item, expected `0` spaces, remove them + * 4:2: Unexpected `1` space before list item, expected `0` spaces, remove them */ /** * @typedef {import('mdast').Root} Root */ -import plural from 'pluralize' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' const remarkLintListItemBulletIndent = lintRule( { @@ -97,34 +96,37 @@ const remarkLintListItemBulletIndent = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'list', function (list, _, grandparent) { - let index = -1 - const pointStartGrandparent = pointStart(grandparent) + const treeStart = pointStart(tree) - while (++index < list.children.length) { - const item = list.children[index] - const itemStart = pointStart(item) + // Unknown containers are not supported. + if (!tree || tree.type !== 'root' || !treeStart) return - if ( - grandparent && - pointStartGrandparent && - itemStart && - grandparent.type === 'root' - ) { - const indent = itemStart.column - pointStartGrandparent.column + for (const child of tree.children) { + if (child.type !== 'list') continue - if (indent) { - file.message( - 'Incorrect indentation before bullet: remove ' + - indent + - ' ' + - plural('space', indent), - itemStart - ) - } + const list = child + + for (const item of list.children) { + const place = pointStart(item) + + /* c8 ignore next 2 -- doesn’t happen in tests as the whole tree is + * generated. */ + if (!place) continue + + const actual = place.column - treeStart.column + + if (actual) { + file.message( + 'Unexpected `' + + actual + + '` ' + + pluralize('space', actual) + + ' before list item, expected `0` spaces, remove them', + {ancestors: [tree, list, item], place} + ) } } - }) + } } ) diff --git a/packages/remark-lint-list-item-bullet-indent/package.json b/packages/remark-lint-list-item-bullet-indent/package.json index e6119f3a..61c7fe34 100644 --- a/packages/remark-lint-list-item-bullet-indent/package.json +++ b/packages/remark-lint-list-item-bullet-indent/package.json @@ -35,8 +35,7 @@ "@types/mdast": "^4.0.0", "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-position": "^5.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-list-item-bullet-indent/readme.md b/packages/remark-lint-list-item-bullet-indent/readme.md index 4eab2520..5e9c6f1c 100644 --- a/packages/remark-lint-list-item-bullet-indent/readme.md +++ b/packages/remark-lint-list-item-bullet-indent/readme.md @@ -140,9 +140,9 @@ There is no specific handling of indented list items in markdown. While it is possible to use an indent to align ordered lists on their marker: ```markdown - 1. One - 10. Ten -100. Hundred + 1. Mercury + 10. Venus +100. Earth ``` …such a style is uncommon and hard to maintain as adding a 10th item @@ -162,10 +162,10 @@ indent. ###### In ```markdown -Paragraph. +Mercury. -* List item -* List item +* Venus. +* Earth. ``` ###### Out @@ -177,17 +177,17 @@ No messages. ###### In ```markdown -Paragraph. +Mercury. -␠* List item -␠* List item +␠* Venus. +␠* Earth. ``` ###### Out ```text -3:2: Incorrect indentation before bullet: remove 1 space -4:2: Incorrect indentation before bullet: remove 1 space +3:2: Unexpected `1` space before list item, expected `0` spaces, remove them +4:2: Unexpected `1` space before list item, expected `0` spaces, remove them ``` ## Compatibility diff --git a/packages/remark-lint-list-item-content-indent/index.js b/packages/remark-lint-list-item-content-indent/index.js index 2eacaa00..61cec616 100644 --- a/packages/remark-lint-list-item-content-indent/index.js +++ b/packages/remark-lint-list-item-content-indent/index.js @@ -5,6 +5,8 @@ * ## What is this? * * This package checks the indent of list item content. + * It checks the first thing in a list item and makes sure that all other + * children have the same indent. * * ## When should I use this? * @@ -42,32 +44,76 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md", "gfm": true} + * {"name": "ok.md"} + * + * 1.␠Mercury. + * ␠␠␠*** + * ␠␠␠* Venus. * - * 1.␠[x] Alpha - * ␠␠␠1. Bravo + * @example + * {"label": "input", "name": "not-ok.md"} * + * 1.␠Mercury. + * ␠␠␠␠␠*** + * ␠␠␠␠* Venus. * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} + * {"label": "output", "name": "not-ok.md"} + * + * 2:6: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces + * 3:5: Unexpected unaligned list item child, expected to align with first child, remove `1` space + * + * @example + * {"name": "ok-more.md"} + * + * *␠␠␠Mercury. + * ␠␠␠␠*** * - * 1.␠[x] Charlie - * ␠␠␠␠1. Delta + * @example + * {"label": "input", "name": "not-ok-more.md"} + * + * *␠␠␠Mercury. + * ␠␠␠␠␠␠*** + * @example + * {"label": "output", "name": "not-ok-more.md"} + * + * 2:7: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces + * + * @example + * {"label": "input", "gfm": true, "name": "gfm-nok.md"} * + * 1.␠[x] Mercury + * ␠␠␠␠␠*** + * ␠␠␠␠* Venus * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} + * {"label": "output", "gfm": true, "name": "gfm-nok.md"} * - * 2:5: Don’t use mixed indentation for children, remove 1 space + * 2:6: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces + * 3:5: Unexpected unaligned list item child, expected to align with first child, remove `1` space + * + * @example + * {"label": "input", "name": "initial-blank.md"} + * + * * + * ␠␠␠␠␠asd + * + * ␠␠*** + * @example + * {"label": "output", "name": "initial-blank.md"} + * + * 4:3: Unexpected unaligned list item child, expected to align with first child, add `3` spaces */ /** * @typedef {import('mdast').Root} Root */ -import plural from 'pluralize' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintListItemContentIndent = lintRule( { @@ -82,57 +128,67 @@ const remarkLintListItemContentIndent = lintRule( */ function (tree, file) { const value = String(file) + /** @type {VFileMessage | undefined} */ + let cause - visit(tree, 'listItem', function (node) { + visitParents(tree, 'listItem', function (node, parents) { let index = -1 /** @type {number | undefined} */ - let style + let expected while (++index < node.children.length) { - const item = node.children[index] - const begin = pointStart(item) + const child = node.children[index] + const childStart = pointStart(child) - if (!begin || typeof begin.offset !== 'number') { + if (!childStart || typeof childStart.offset !== 'number') { continue } - let column = begin.column + let actual = childStart.column // Get indentation for the first child. - // Only the first item can have a checkbox, so here we remove that from - // the column. - if (index === 0) { - // If there’s a checkbox before the content, look backwards to find - // the start of that checkbox. - if (typeof node.checked === 'boolean') { - let char = begin.offset - 1 + // Only the first item can have a checkbox, + // when it’s a paragraph, + // so here we remove that from the column. + if (index === 0 && typeof node.checked === 'boolean') { + let beforeIndex = childStart.offset - 1 - while (char > 0 && value.charAt(char) !== '[') { - char-- - } - - column -= begin.offset - char + while ( + beforeIndex > 0 && + value.charCodeAt(beforeIndex) !== 91 /* `[` */ + ) { + beforeIndex-- } - style = column - - continue + actual -= childStart.offset - beforeIndex } - // Warn for violating children. - if (style && column !== style) { - const diff = style - column - const abs = Math.abs(diff) + if (expected) { + // Warn for violating children. + if (actual !== expected) { + const difference = expected - actual + const differenceAbsolute = Math.abs(difference) - file.message( - 'Don’t use mixed indentation for children, ' + - /* c8 ignore next -- hard to test, I couldn’t find it at least. */ - (diff > 0 ? 'add' : 'remove') + - ' ' + - abs + - ' ' + - plural('space', abs), - {line: begin.line, column} + file.message( + 'Unexpected unaligned list item child, expected to align with first child, ' + + (difference > 0 ? 'add' : 'remove') + + ' `' + + differenceAbsolute + + '` ' + + pluralize('space', differenceAbsolute), + {ancestors: [...parents, node, child], cause, place: childStart} + ) + } + } else { + expected = actual + cause = new VFileMessage( + 'Alignment of first child first defined here', + { + ancestors: [...parents, node, child], + place: childStart, + ruleId: 'list-item-content-indent', + source: 'remark-lint' + } ) } } diff --git a/packages/remark-lint-list-item-content-indent/package.json b/packages/remark-lint-list-item-content-indent/package.json index 0557908d..2706c92a 100644 --- a/packages/remark-lint-list-item-content-indent/package.json +++ b/packages/remark-lint-list-item-content-indent/package.json @@ -37,7 +37,8 @@ "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { @@ -49,7 +50,8 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-list-item-content-indent/readme.md b/packages/remark-lint-list-item-content-indent/readme.md index 7df92586..6df5d132 100644 --- a/packages/remark-lint-list-item-content-indent/readme.md +++ b/packages/remark-lint-list-item-content-indent/readme.md @@ -32,6 +32,8 @@ consistent. ## What is this? This package checks the indent of list item content. +It checks the first thing in a list item and makes sure that all other +children have the same indent. ## When should I use this? @@ -151,12 +153,10 @@ Further children should align with it. ###### In -> 👉 **Note**: this example uses -> GFM ([`remark-gfm`][github-remark-gfm]). - ```markdown -1.␠[x] Alpha -␠␠␠1. Bravo +1.␠Mercury. +␠␠␠*** +␠␠␠* Venus. ``` ###### Out @@ -167,18 +167,82 @@ No messages. ###### In +```markdown +1.␠Mercury. +␠␠␠␠␠*** +␠␠␠␠* Venus. +``` + +###### Out + +```text +2:6: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces +3:5: Unexpected unaligned list item child, expected to align with first child, remove `1` space +``` + +##### `ok-more.md` + +###### In + +```markdown +*␠␠␠Mercury. +␠␠␠␠*** +``` + +###### Out + +No messages. + +##### `not-ok-more.md` + +###### In + +```markdown +*␠␠␠Mercury. +␠␠␠␠␠␠*** +``` + +###### Out + +```text +2:7: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces +``` + +##### `gfm-nok.md` + +###### In + > 👉 **Note**: this example uses > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -1.␠[x] Charlie -␠␠␠␠1. Delta +1.␠[x] Mercury +␠␠␠␠␠*** +␠␠␠␠* Venus +``` + +###### Out + +```text +2:6: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces +3:5: Unexpected unaligned list item child, expected to align with first child, remove `1` space +``` + +##### `initial-blank.md` + +###### In + +```markdown +* +␠␠␠␠␠asd + +␠␠*** ``` ###### Out ```text -2:5: Don’t use mixed indentation for children, remove 1 space +4:3: Unexpected unaligned list item child, expected to align with first child, add `3` spaces ``` ## Compatibility diff --git a/packages/remark-lint-list-item-indent/index.js b/packages/remark-lint-list-item-indent/index.js index 5d082c00..e1274716 100644 --- a/packages/remark-lint-list-item-indent/index.js +++ b/packages/remark-lint-list-item-indent/index.js @@ -92,114 +92,204 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * *␠List - * ␠␠item. + * *␠Mercury. + * *␠Venus. + * + * 111.␠Earth + * ␠␠␠␠␠and Mars. + * + * *␠**Jupiter**. + * + * ␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar + * ␠␠System. + * + * *␠Saturn. + * + * ␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. + * + * @example + * {"config": "mixed", "name": "ok.md"} + * + * *␠Mercury. + * *␠Venus. + * + * 111.␠Earth + * ␠␠␠␠␠and Mars. + * + * *␠␠␠**Jupiter**. + * + * ␠␠␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar + * ␠␠␠␠System. + * + * *␠␠␠Saturn. * - * Paragraph. + * ␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. * - * 11.␠List - * ␠␠␠␠item. + * @example + * {"config": "mixed", "label": "input", "name": "not-ok.md"} + * + * *␠␠␠Mercury. + * *␠␠␠Venus. * - * Paragraph. + * 111.␠␠␠␠Earth + * ␠␠␠␠␠␠␠␠and Mars. * - * *␠List - * ␠␠item. + * *␠**Jupiter**. * - * *␠List - * ␠␠item. + * ␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar + * ␠␠System. * + * *␠Saturn. + * + * ␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. * @example - * {"name": "ok.md", "config": "mixed"} + * {"config": "mixed", "label": "output", "name": "not-ok.md"} * - * *␠List item. + * 1:5: Unexpected `3` spaces between list item marker and content in tight list, expected `1` space, remove `2` spaces + * 2:5: Unexpected `3` spaces between list item marker and content in tight list, expected `1` space, remove `2` spaces + * 4:9: Unexpected `4` spaces between list item marker and content in tight list, expected `1` space, remove `3` spaces + * 7:3: Unexpected `1` space between list item marker and content in loose list, expected `3` spaces, add `2` spaces + * 12:3: Unexpected `1` space between list item marker and content in loose list, expected `3` spaces, add `2` spaces * - * Paragraph. + * @example + * {"config": "one", "name": "ok.md"} * - * 11.␠List item + * *␠Mercury. + * *␠Venus. * - * Paragraph. + * 111.␠Earth + * ␠␠␠␠␠and Mars. * - * *␠␠␠List - * ␠␠␠␠item. + * *␠**Jupiter**. * - * *␠␠␠List - * ␠␠␠␠item. + * ␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar + * ␠␠System. + * + * *␠Saturn. + * + * ␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. * * @example - * {"name": "ok.md", "config": "one"} + * {"config": "one", "label": "input", "name": "not-ok.md"} * - * *␠List item. + * *␠␠␠Mercury. + * *␠␠␠Venus. * - * Paragraph. + * 111.␠␠␠␠Earth + * ␠␠␠␠␠␠␠␠and Mars. * - * 11.␠List item + * *␠␠␠**Jupiter**. * - * Paragraph. + * ␠␠␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar + * ␠␠␠␠System. * - * *␠List - * ␠␠item. + * *␠␠␠Saturn. * - * *␠List - * ␠␠item. + * ␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. + * @example + * {"config": "one", "label": "output", "name": "not-ok.md"} + * + * 1:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces + * 2:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces + * 4:9: Unexpected `4` spaces between list item marker and content, expected `1` space, remove `3` spaces + * 7:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces + * 12:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces * * @example * {"config": "tab", "name": "ok.md"} * - * *␠␠␠List - * ␠␠␠␠item. + * *␠␠␠Mercury. + * *␠␠␠Venus. * - * Paragraph. + * 111.␠␠␠␠Earth + * ␠␠␠␠␠␠␠␠and Mars. * - * 11.␠List - * ␠␠␠␠item. + * *␠␠␠**Jupiter**. * - * Paragraph. + * ␠␠␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar + * ␠␠␠␠System. * - * *␠␠␠List - * ␠␠␠␠item. + * *␠␠␠Saturn. * - * *␠␠␠List - * ␠␠␠␠item. + * ␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. * * @example - * {"name": "not-ok.md", "config": "one", "label": "input"} + * {"config": "tab", "label": "input", "name": "not-ok.md"} + * + * *␠Mercury. + * *␠Venus. + * + * 111.␠Earth + * ␠␠␠␠␠and Mars. * - * *␠␠␠List - * ␠␠␠␠item. + * *␠**Jupiter**. * + * ␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar + * ␠␠System. + * + * *␠Saturn. + * + * ␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. * @example - * {"name": "not-ok.md", "config": "one", "label": "output"} + * {"config": "tab", "label": "output", "name": "not-ok.md"} * - * 1:5: Incorrect list-item indent: remove 2 spaces + * 1:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces + * 2:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces + * 4:6: Unexpected `1` space between list item marker and content, expected `4` spaces, add `3` spaces + * 7:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces + * 12:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces * * @example - * {"name": "not-ok.md", "config": "tab", "label": "input"} + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} * - * *␠List - * ␠␠item. + * 1:1: Unexpected value `🌍` for `options`, expected `'mixed'`, `'one'`, or `'tab'` * * @example - * {"name": "not-ok.md", "config": "tab", "label": "output"} + * {"config": "mixed", "gfm": true, "label": "input", "name": "gfm.md"} + * + * *␠[x] Mercury. * - * 1:3: Incorrect list-item indent: add 2 spaces + * 1.␠␠[ ] Venus. + * + * 2.␠␠[ ] Earth. * * @example - * {"name": "not-ok.md", "config": "mixed", "label": "input"} + * {"config": "one", "gfm": true, "name": "gfm.md"} + * + * *␠[x] Mercury. * - * *␠␠␠List item. + * 1.␠[ ] Venus. + * + * 2.␠[ ] Earth. * * @example - * {"name": "not-ok.md", "config": "mixed", "label": "output"} + * {"config": "tab", "gfm": true, "name": "gfm.md"} + * + * *␠␠␠[x] Mercury. * - * 1:5: Incorrect list-item indent: remove 2 spaces + * 1.␠␠[ ] Venus. + * + * 2.␠␠[ ] Earth. * * @example - * {"name": "not-ok.md", "config": "💩", "label": "output", "positionless": true} + * {"config": "mixed", "name": "loose-tight.md"} + * + * Loose lists have blank lines between items: + * + * *␠␠␠Mercury. * - * 1:1: Incorrect list-item indent style `💩`: use either `'mixed'`, `'one'`, or `'tab'` + * *␠␠␠Venus. + * + * …or between children of items: + * + * 1.␠␠Earth. + * + * ␠␠␠␠Earth is the third planet from the Sun and the only astronomical + * ␠␠␠␠object known to harbor life. */ /** @@ -211,10 +301,10 @@ * Configuration. */ -import plural from 'pluralize' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const remarkLintListItemIndent = lintRule( { @@ -231,70 +321,106 @@ const remarkLintListItemIndent = lintRule( */ function (tree, file, options) { const value = String(file) - const option = options || 'one' + /** @type {Options} */ + let expected - /* c8 ignore next 13 -- previous names. */ - // @ts-expect-error: old name. - if (option === 'space') { + if (options === null || options === undefined) { + expected = 'one' + /* c8 ignore next 10 -- previous names. */ + // @ts-expect-error: old name. + } else if (options === 'space') { file.fail( - 'Incorrect list-item indent style `' + option + "`: use `'one'` instead" + 'Unexpected value `' + options + "` for `options`, expected `'one'`" ) - } - - // @ts-expect-error: old name. - if (option === 'tab-size') { + // @ts-expect-error: old name. + } else if (options === 'tab-size') { file.fail( - 'Incorrect list-item indent style `' + option + "`: use `'tab'` instead" + 'Unexpected value `' + options + "` for `options`, expected `'tab'`" ) - } - - if (option !== 'mixed' && option !== 'one' && option !== 'tab') { + } else if (options === 'mixed' || options === 'one' || options === 'tab') { + expected = options + } else { file.fail( - 'Incorrect list-item indent style `' + - option + - "`: use either `'mixed'`, `'one'`, or `'tab'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'mixed'`, `'one'`, or `'tab'`" ) } - visit(tree, 'list', function (node) { - const spread = node.spread - let index = -1 + visitParents(tree, 'list', function (list, parents) { + let loose = list.spread + + if (!loose) { + for (const item of list.children) { + if (item.spread) { + loose = true + break + } + } + } - while (++index < node.children.length) { - const item = node.children[index] + for (const item of list.children) { const head = item.children[0] - const start = pointStart(item) - const final = pointStart(head) + const itemStart = pointStart(item) + const headStart = pointStart(head) if ( - start && - final && - typeof start.offset === 'number' && - typeof final.offset === 'number' + itemStart && + headStart && + typeof itemStart.offset === 'number' && + typeof headStart.offset === 'number' ) { - const marker = value - .slice(start.offset, final.offset) - .replace(/\[[x ]?]\s*$/i, '') + let slice = value.slice(itemStart.offset, headStart.offset) + + // GFM tasklist. + const checkboxIndex = slice.indexOf('[') + if (checkboxIndex !== -1) slice = slice.slice(0, checkboxIndex) - const bulletSize = marker.replace(/\s+$/, '').length + const actualIndent = slice.length + + // To do: actual hard tabs? + // Remove whitespace. + let end = actualIndent + let previous = slice.charCodeAt(end - 1) + + while (previous === 9 || previous === 32) { + end-- + previous = slice.charCodeAt(end - 1) + } + + let expectedIndent = end + 1 // One space needed after marker. + + if (expected === 'tab' || (expected === 'mixed' && loose)) { + expectedIndent = Math.ceil(expectedIndent / 4) * 4 + } - const style = - option === 'tab' || (option === 'mixed' && spread) - ? Math.ceil(bulletSize / 4) * 4 - : bulletSize + 1 + const expectedSpaces = expectedIndent - end + const actualSpaces = actualIndent - end - if (marker.length !== style) { - const diff = style - marker.length - const abs = Math.abs(diff) + if (actualSpaces !== expectedSpaces) { + const difference = expectedSpaces - actualSpaces + const differenceAbsolute = Math.abs(difference) file.message( - 'Incorrect list-item indent: ' + - (diff > 0 ? 'add' : 'remove') + - ' ' + - abs + - ' ' + - plural('space', abs), - final + 'Unexpected `' + + actualSpaces + + '` ' + + pluralize('space', actualSpaces) + + ' between list item marker and content' + + (expected === 'mixed' + ? ' in ' + (loose ? 'loose' : 'tight') + ' list' + : '') + + ', expected `' + + expectedSpaces + + '` ' + + pluralize('space', expectedSpaces) + + ', ' + + (difference > 0 ? 'add' : 'remove') + + ' `' + + differenceAbsolute + + '` ' + + pluralize('space', differenceAbsolute), + {ancestors: [...parents, list, item], place: headStart} ) } } diff --git a/packages/remark-lint-list-item-indent/package.json b/packages/remark-lint-list-item-indent/package.json index 0023f115..202aad5c 100644 --- a/packages/remark-lint-list-item-indent/package.json +++ b/packages/remark-lint-list-item-indent/package.json @@ -36,7 +36,7 @@ "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -48,7 +48,9 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "unicorn/prefer-code-point": "off", + "unicorn/prefer-switch": "off" } } } diff --git a/packages/remark-lint-list-item-indent/readme.md b/packages/remark-lint-list-item-indent/readme.md index cb510294..0d40d1bb 100644 --- a/packages/remark-lint-list-item-indent/readme.md +++ b/packages/remark-lint-list-item-indent/readme.md @@ -203,21 +203,20 @@ by default. ###### In ```markdown -*␠List -␠␠item. +*␠Mercury. +*␠Venus. -Paragraph. +111.␠Earth +␠␠␠␠␠and Mars. -11.␠List -␠␠␠␠item. +*␠**Jupiter**. -Paragraph. +␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar +␠␠System. -*␠List -␠␠item. +*␠Saturn. -*␠List -␠␠item. +␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. ``` ###### Out @@ -231,73 +230,80 @@ When configured with `'mixed'`. ###### In ```markdown -*␠List item. +*␠Mercury. +*␠Venus. -Paragraph. +111.␠Earth +␠␠␠␠␠and Mars. -11.␠List item +*␠␠␠**Jupiter**. -Paragraph. +␠␠␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar +␠␠␠␠System. -*␠␠␠List -␠␠␠␠item. +*␠␠␠Saturn. -*␠␠␠List -␠␠␠␠item. +␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. ``` ###### Out No messages. -##### `ok.md` +##### `not-ok.md` -When configured with `'one'`. +When configured with `'mixed'`. ###### In ```markdown -*␠List item. +*␠␠␠Mercury. +*␠␠␠Venus. -Paragraph. +111.␠␠␠␠Earth +␠␠␠␠␠␠␠␠and Mars. -11.␠List item +*␠**Jupiter**. -Paragraph. +␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar +␠␠System. -*␠List -␠␠item. +*␠Saturn. -*␠List -␠␠item. +␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. ``` ###### Out -No messages. +```text +1:5: Unexpected `3` spaces between list item marker and content in tight list, expected `1` space, remove `2` spaces +2:5: Unexpected `3` spaces between list item marker and content in tight list, expected `1` space, remove `2` spaces +4:9: Unexpected `4` spaces between list item marker and content in tight list, expected `1` space, remove `3` spaces +7:3: Unexpected `1` space between list item marker and content in loose list, expected `3` spaces, add `2` spaces +12:3: Unexpected `1` space between list item marker and content in loose list, expected `3` spaces, add `2` spaces +``` ##### `ok.md` -When configured with `'tab'`. +When configured with `'one'`. ###### In ```markdown -*␠␠␠List -␠␠␠␠item. +*␠Mercury. +*␠Venus. -Paragraph. +111.␠Earth +␠␠␠␠␠and Mars. -11.␠List -␠␠␠␠item. +*␠**Jupiter**. -Paragraph. +␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar +␠␠System. -*␠␠␠List -␠␠␠␠item. +*␠Saturn. -*␠␠␠List -␠␠␠␠item. +␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. ``` ###### Out @@ -311,16 +317,59 @@ When configured with `'one'`. ###### In ```markdown -*␠␠␠List -␠␠␠␠item. +*␠␠␠Mercury. +*␠␠␠Venus. + +111.␠␠␠␠Earth +␠␠␠␠␠␠␠␠and Mars. + +*␠␠␠**Jupiter**. + +␠␠␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar +␠␠␠␠System. + +*␠␠␠Saturn. + +␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. ``` ###### Out ```text -1:5: Incorrect list-item indent: remove 2 spaces +1:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces +2:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces +4:9: Unexpected `4` spaces between list item marker and content, expected `1` space, remove `3` spaces +7:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces +12:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces +``` + +##### `ok.md` + +When configured with `'tab'`. + +###### In + +```markdown +*␠␠␠Mercury. +*␠␠␠Venus. + +111.␠␠␠␠Earth +␠␠␠␠␠␠␠␠and Mars. + +*␠␠␠**Jupiter**. + +␠␠␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar +␠␠␠␠System. + +*␠␠␠Saturn. + +␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. ``` +###### Out + +No messages. + ##### `not-ok.md` When configured with `'tab'`. @@ -328,42 +377,130 @@ When configured with `'tab'`. ###### In ```markdown -*␠List -␠␠item. +*␠Mercury. +*␠Venus. + +111.␠Earth +␠␠␠␠␠and Mars. + +*␠**Jupiter**. + +␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar +␠␠System. + +*␠Saturn. + +␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. ``` ###### Out ```text -1:3: Incorrect list-item indent: add 2 spaces +1:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces +2:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces +4:6: Unexpected `1` space between list item marker and content, expected `4` spaces, add `3` spaces +7:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces +12:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces ``` ##### `not-ok.md` +When configured with `'🌍'`. + +###### Out + +```text +1:1: Unexpected value `🌍` for `options`, expected `'mixed'`, `'one'`, or `'tab'` +``` + +##### `gfm.md` + When configured with `'mixed'`. ###### In +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + ```markdown -*␠␠␠List item. +*␠[x] Mercury. + +1.␠␠[ ] Venus. + +2.␠␠[ ] Earth. ``` ###### Out -```text -1:5: Incorrect list-item indent: remove 2 spaces +No messages. + +##### `gfm.md` + +When configured with `'one'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +*␠[x] Mercury. + +1.␠[ ] Venus. + +2.␠[ ] Earth. ``` -##### `not-ok.md` +###### Out + +No messages. + +##### `gfm.md` + +When configured with `'tab'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). -When configured with `'💩'`. +```markdown +*␠␠␠[x] Mercury. + +1.␠␠[ ] Venus. + +2.␠␠[ ] Earth. +``` ###### Out -```text -1:1: Incorrect list-item indent style `💩`: use either `'mixed'`, `'one'`, or `'tab'` +No messages. + +##### `loose-tight.md` + +When configured with `'mixed'`. + +###### In + +```markdown +Loose lists have blank lines between items: + +*␠␠␠Mercury. + +*␠␠␠Venus. + +…or between children of items: + +1.␠␠Earth. + +␠␠␠␠Earth is the third planet from the Sun and the only astronomical +␠␠␠␠object known to harbor life. ``` +###### Out + +No messages. + ## Compatibility Projects maintained by the unified collective are compatible with maintained @@ -435,6 +572,8 @@ abide by its terms. [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c +[github-remark-gfm]: https://github.com/remarkjs/remark-gfm + [github-remark-lint]: https://github.com/remarkjs/remark-lint [github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify diff --git a/packages/remark-lint-list-item-spacing/index.js b/packages/remark-lint-list-item-spacing/index.js index 6ca10076..923fcb3c 100644 --- a/packages/remark-lint-list-item-spacing/index.js +++ b/packages/remark-lint-list-item-spacing/index.js @@ -65,97 +65,77 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * A tight list: - * - * - item 1 - * - item 2 - * - item 3 - * - * A loose list: - * - * - Wrapped - * item + * * Mercury. + * * Venus. * - * - item 2 + * + Mercury and + * Venus. * - * - item 3 + * + Earth. * * @example - * {"name": "not-ok.md", "label": "input"} - * - * A tight list: - * - * - Wrapped - * item - * - item 2 - * - item 3 - * - * A loose list: + * {"config": {"checkBlanks": true}, "name": "ok-check-blanks.md"} * - * - item 1 + * * Mercury. + * * Venus. * - * - item 2 + * + Mercury * - * - item 3 + * Mercury is the first planet from the Sun and the smallest in the Solar + * System. * - * @example - * {"name": "not-ok.md", "label": "output"} - * - * 4:9-5:1: Missing new line after list item - * 5:11-6:1: Missing new line after list item - * 10:11-12:1: Extraneous new line after list item - * 12:11-14:1: Extraneous new line after list item + * + Earth. * * @example - * {"name": "ok.md", "config": {"checkBlanks": true}} - * - * A tight list: - * - * - item 1 - * - item 1.A - * - item 2 - * > Block quote + * {"label": "input", "name": "not-ok.md"} * - * A loose list: + * * Mercury. * - * - item 1 + * * Venus. * - * - item 1.A + * + Mercury and + * Venus. + * + Earth. * - * - item 2 - * - * > Block quote + * * Mercury. * + * Mercury is the first planet from the Sun and the smallest in the Solar + * System. + * * Earth. * @example - * {"name": "not-ok.md", "config": {"checkBlanks": true}, "label": "input"} + * {"label": "output", "name": "not-ok.md"} * - * A tight list: + * 1:11-3:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line + * 6:11-7:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line + * 12:12-13:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line * - * - item 1 + * @example + * {"config": {"checkBlanks": true}, "label": "input", "name": "not-ok-blank.md"} * - * - item 1.A - * - item 2 + * * Mercury. * - * > Block quote - * - item 3 + * * Venus. * - * A loose list: + * + Mercury and + * Venus. * - * - item 1 - * - item 1.A + * + Earth. * - * - item 2 - * > Block quote + * * Mercury. * + * Mercury is the first planet from the Sun and the smallest in the Solar + * System. + * * Earth. * @example - * {"name": "not-ok.md", "config": {"checkBlanks": true}, "label": "output"} + * {"config": {"checkBlanks": true}, "label": "output", "name": "not-ok-blank.md"} * - * 5:15-6:1: Missing new line after list item - * 8:18-9:1: Missing new line after list item - * 14:15-16:1: Extraneous new line after list item + * 1:11-3:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line + * 6:11-8:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line + * 13:12-14:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line */ /** @@ -171,9 +151,11 @@ * preference (default: `false`). */ +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' /** @type {Readonly} */ const emptyOptions = {} @@ -193,82 +175,83 @@ const remarkLintListItemSpacing = lintRule( */ function (tree, file, options) { const settings = options || emptyOptions + // To do: change options. Maybe to `Style = 'markdown' | 'markdown-style-guide'`? const checkBlanks = settings.checkBlanks || false - const infer = checkBlanks ? blanksBetween : multiline - visit(tree, 'list', function (node) { - let index = -1 - let anySpaced = false + visitParents(tree, 'list', function (list, parents) { + /** @type {VFileMessage | undefined} */ + let spacedCause + + for (const item of list.children) { + /** @type {boolean | null | undefined} */ + let spaced = false - while (++index < node.children.length) { - const spaced = infer(node.children[index]) + if (checkBlanks) { + spaced = item.spread + } else { + const tail = item.children.at(-1) + const end = pointEnd(tail) + const start = pointStart(item) + spaced = end && start && end.line - start.line > 0 + } if (spaced) { - anySpaced = true + spacedCause = new VFileMessage( + 'Spaced list item first defined here', + { + ancestors: [...parents, list, item], + place: item.position, + ruleId: 'list-item-spacing', + source: 'remark-lint' + } + ) break } } - index = 0 // Skip first. + const expected = spacedCause ? 1 : 0 + /** @type {ListItem | undefined} */ + let previous - while (++index < node.children.length) { - const previous = node.children[index - 1] - const current = node.children[index] + for (const item of list.children) { const previousEnd = pointEnd(previous) - const start = pointStart(current) + const itemStart = pointStart(item) + + if (previousEnd && itemStart) { + const actual = itemStart.line - previousEnd.line - 1 - if (previousEnd && start) { - const spaced = start.line - previousEnd.line > 1 + if (actual !== expected) { + const difference = expected - actual + const differenceAbsolute = Math.abs(difference) - if (spaced !== anySpaced) { file.message( - anySpaced - ? 'Missing new line after list item' - : 'Extraneous new line after list item', - {start: previousEnd, end: start} + 'Unexpected `' + + actual + + '` blank ' + + pluralize('line', actual) + + ' between list items, expected `' + + expected + + '` blank ' + + pluralize('line', expected) + + ', ' + + (difference > 0 ? 'add' : 'remove') + + ' `' + + differenceAbsolute + + '` blank ' + + pluralize('line', differenceAbsolute), + { + ancestors: [...parents, list, item], + cause: spacedCause, + place: {start: previousEnd, end: itemStart} + } ) } } + + previous = item } }) } ) export default remarkLintListItemSpacing - -/** - * @param {ListItem} node - * Item. - * @returns {boolean} - * Whether there is a blank line between one of the children. - */ -function blanksBetween(node) { - let index = 0 // Skip first. - - while (++index < node.children.length) { - const previousEnd = pointEnd(node.children[index - 1]) - const start = pointStart(node.children[index]) - - // Note: all children in `listItem`s are flow. - if (start && previousEnd && start.line - previousEnd.line > 1) { - return true - } - } - - return false -} - -/** - * @param {ListItem} node - * Item. - * @returns {boolean} - * Whether `node` spans multiple lines. - */ -function multiline(node) { - const head = node.children[0] - const tail = node.children[node.children.length - 1] - const end = pointEnd(tail) - const start = pointStart(head) - - return Boolean(end && start && end.line - start.line > 0) -} diff --git a/packages/remark-lint-list-item-spacing/package.json b/packages/remark-lint-list-item-spacing/package.json index 71100066..4cdd812a 100644 --- a/packages/remark-lint-list-item-spacing/package.json +++ b/packages/remark-lint-list-item-spacing/package.json @@ -34,9 +34,11 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-list-item-spacing/readme.md b/packages/remark-lint-list-item-spacing/readme.md index 8ce49cdb..cf424771 100644 --- a/packages/remark-lint-list-item-spacing/readme.md +++ b/packages/remark-lint-list-item-spacing/readme.md @@ -174,117 +174,98 @@ all items must be loose. ###### In ```markdown -A tight list: +* Mercury. +* Venus. -- item 1 -- item 2 -- item 3 ++ Mercury and + Venus. -A loose list: - -- Wrapped - item - -- item 2 - -- item 3 ++ Earth. ``` ###### Out No messages. -##### `not-ok.md` +##### `ok-check-blanks.md` + +When configured with `{ checkBlanks: true }`. ###### In ```markdown -A tight list: +* Mercury. +* Venus. -- Wrapped - item -- item 2 -- item 3 ++ Mercury -A loose list: + Mercury is the first planet from the Sun and the smallest in the Solar + System. -- item 1 - -- item 2 - -- item 3 ++ Earth. ``` ###### Out -```text -4:9-5:1: Missing new line after list item -5:11-6:1: Missing new line after list item -10:11-12:1: Extraneous new line after list item -12:11-14:1: Extraneous new line after list item -``` - -##### `ok.md` +No messages. -When configured with `{ checkBlanks: true }`. +##### `not-ok.md` ###### In ```markdown -A tight list: - -- item 1 - - item 1.A -- item 2 - > Block quote - -A loose list: +* Mercury. -- item 1 +* Venus. - - item 1.A ++ Mercury and + Venus. ++ Earth. -- item 2 +* Mercury. - > Block quote + Mercury is the first planet from the Sun and the smallest in the Solar + System. +* Earth. ``` ###### Out -No messages. +```text +1:11-3:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line +6:11-7:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line +12:12-13:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line +``` -##### `not-ok.md` +##### `not-ok-blank.md` When configured with `{ checkBlanks: true }`. ###### In ```markdown -A tight list: - -- item 1 +* Mercury. - - item 1.A -- item 2 +* Venus. - > Block quote -- item 3 ++ Mercury and + Venus. -A loose list: ++ Earth. -- item 1 - - item 1.A +* Mercury. -- item 2 - > Block quote + Mercury is the first planet from the Sun and the smallest in the Solar + System. +* Earth. ``` ###### Out ```text -5:15-6:1: Missing new line after list item -8:18-9:1: Missing new line after list item -14:15-16:1: Extraneous new line after list item +1:11-3:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line +6:11-8:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line +13:12-14:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line ``` ## Compatibility diff --git a/packages/remark-lint-maximum-heading-length/index.js b/packages/remark-lint-maximum-heading-length/index.js index f4bfa142..b4395abb 100644 --- a/packages/remark-lint-maximum-heading-length/index.js +++ b/packages/remark-lint-maximum-heading-length/index.js @@ -43,28 +43,31 @@ * @example * {"name": "ok.md"} * - * # Alpha bravo charlie delta echo foxtrot golf hotel - * - * # ![Alpha bravo charlie delta echo foxtrot golf hotel](http://example.com/nato.png) + * # Mercury is the first planet from the Sun * * @example - * {"name": "not-ok.md", "config": 40, "label": "input"} + * {"config": 30, "label": "input", "name": "not-ok.md"} * - * # Alpha bravo charlie delta echo foxtrot golf hotel + * # Mercury is the first planet from the Sun * * @example - * {"name": "not-ok.md", "config": 40, "label": "output"} + * {"config": 30, "label": "output", "name": "not-ok.md"} * - * 1:1-1:52: Use headings shorter than `40` + * 1:1-1:43: Unexpected `40` characters in heading, expected at most `30` characters * * @example - * {"config": 30, "label": "input", "mdx": true, "name": "ok.mdx"} + * {"config": 30, "label": "input", "mdx": true, "name": "mdx.mdx"} * - *

In MDX, headings are checked too

+ *

Mercury is the first planet from the Sun

* @example - * {"config": 30, "label": "output", "mdx": true, "name": "ok.mdx"} + * {"config": 30, "label": "output", "mdx": true, "name": "mdx.mdx"} + * + * 1:1-1:50: Unexpected `40` characters in heading, expected at most `30` characters * - * 1:1-1:42: Use headings shorter than `30` + * @example + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} + * + * 1:1: Unexpected value `🌍` for `options`, expected `number` */ /** @@ -76,7 +79,7 @@ import {toString} from 'mdast-util-to-string' import {lintRule} from 'unified-lint-rule' import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const jsxNameRe = /^h([1-6])$/ @@ -94,12 +97,22 @@ const remarkLintMaximumHeadingLength = lintRule( * Nothing. */ function (tree, file, options) { - const option = options || 60 + let expected = 60 + + if (options === null || options === undefined) { + // Empty. + } else if (typeof options === 'number') { + expected = options + } else { + file.fail( + 'Unexpected value `' + options + '` for `options`, expected `number`' + ) + } // Note: HTML headings cannot properly be checked, // because for markdown, blocks are one single raw string. - visit(tree, function (node) { + visitParents(tree, function (node, parents) { if ( node.type === 'heading' || ((node.type === 'mdxJsxFlowElement' || @@ -108,10 +121,17 @@ const remarkLintMaximumHeadingLength = lintRule( jsxNameRe.test(node.name)) ) { const place = position(node) - const codePoints = Array.from(toString(node, {includeHtml: false})) + const actual = Array.from(toString(node, {includeHtml: false})).length - if (place && codePoints.length > option) { - file.message('Use headings shorter than `' + option + '`', place) + if (place && actual > expected) { + file.message( + 'Unexpected `' + + actual + + '` characters in heading, expected at most `' + + expected + + '` characters', + {ancestors: [...parents, node], place} + ) } } }) diff --git a/packages/remark-lint-maximum-heading-length/package.json b/packages/remark-lint-maximum-heading-length/package.json index 0a2b8c7c..301636a9 100644 --- a/packages/remark-lint-maximum-heading-length/package.json +++ b/packages/remark-lint-maximum-heading-length/package.json @@ -36,7 +36,7 @@ "mdast-util-to-string": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-maximum-heading-length/readme.md b/packages/remark-lint-maximum-heading-length/readme.md index 1ecfc84a..68cf81ea 100644 --- a/packages/remark-lint-maximum-heading-length/readme.md +++ b/packages/remark-lint-maximum-heading-length/readme.md @@ -149,9 +149,7 @@ every heading out loud to navigate within a page). ###### In ```markdown -# Alpha bravo charlie delta echo foxtrot golf hotel - -# ![Alpha bravo charlie delta echo foxtrot golf hotel](http://example.com/nato.png) +# Mercury is the first planet from the Sun ``` ###### Out @@ -160,21 +158,21 @@ No messages. ##### `not-ok.md` -When configured with `40`. +When configured with `30`. ###### In ```markdown -# Alpha bravo charlie delta echo foxtrot golf hotel +# Mercury is the first planet from the Sun ``` ###### Out ```text -1:1-1:52: Use headings shorter than `40` +1:1-1:43: Unexpected `40` characters in heading, expected at most `30` characters ``` -##### `ok.mdx` +##### `mdx.mdx` When configured with `30`. @@ -184,13 +182,23 @@ When configured with `30`. > MDX ([`remark-mdx`][github-remark-mdx]). ```mdx -

In MDX, headings are checked too

+

Mercury is the first planet from the Sun

+``` + +###### Out + +```text +1:1-1:50: Unexpected `40` characters in heading, expected at most `30` characters ``` +##### `not-ok.md` + +When configured with `'🌍'`. + ###### Out ```text -1:1-1:42: Use headings shorter than `30` +1:1: Unexpected value `🌍` for `options`, expected `number` ``` ## Compatibility diff --git a/packages/remark-lint-maximum-line-length/index.js b/packages/remark-lint-maximum-line-length/index.js index 1c2ba7bd..f9d937c9 100644 --- a/packages/remark-lint-maximum-line-length/index.js +++ b/packages/remark-lint-maximum-line-length/index.js @@ -43,80 +43,112 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md", "positionless": true, "gfm": true} + * {"name": "ok.md", "positionless": true} + * + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury mercury + * mercury. + * + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury `mercury()`. + * + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury . * - * This line is simply not toooooooooooooooooooooooooooooooooooooooooooo - * long. + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury [mercury](http://localhost). * - * This is also fine: + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury ![mercury](http://localhost). * - * + *
Mercury mercury mercury mercury mercury mercury mercury mercury mercury
* - * `alphaBravoCharlieDeltaEchoFoxtrotGolfHotelIndiaJuliettKiloLimaMikeNovemberOscarPapaQuebec.romeo()` + * [foo]: http://localhost/mercury/mercury/mercury/mercury/mercury/mercury/mercury/mercury * - * [foo](http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables) + * @example + * {"config": 20, "label": "input", "name": "not-ok.md", "positionless": true} + * + * Mercury mercury mercury + * mercury. * - * + * Mercury mercury mercury `mercury()`. * - * ![foo](http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables) + * Mercury mercury mercury . * - * | An | exception | is | line | length | in | long | tables | because | those | can’t | just | - * | -- | --------- | -- | ---- | ------ | -- | ---- | ------ | ------- | ----- | ----- | ---- | - * | be | helped | | | | | | | | | | . | + * Mercury mercury mercury [m](example.com). * - *

alpha bravo charlie delta echo foxtrot golf

+ * Mercury mercury mercury ![m](example.com). * - * The following is also fine (note the `.`), because there is no whitespace. + * `mercury()` mercury mercury mercury. * - * . + * mercury. * - * In addition, definitions are also fine: + * [m](example.com) mercury. * - * [foo]: + * ![m](example.com) mercury. + * + * Mercury mercury ![m](example.com) mercury. * * @example - * {"name": "not-ok.md", "config": 80, "label": "input", "positionless": true} + * {"config": 20, "label": "output", "name": "not-ok.md", "positionless": true} + * + * 1:24: Unexpected `23` character line, expected at most `20` characters, remove `3` characters + * 4:37: Unexpected `36` character line, expected at most `20` characters, remove `16` characters + * 6:44: Unexpected `43` character line, expected at most `20` characters, remove `23` characters + * 8:42: Unexpected `41` character line, expected at most `20` characters, remove `21` characters + * 10:43: Unexpected `42` character line, expected at most `20` characters, remove `22` characters + * 12:37: Unexpected `36` character line, expected at most `20` characters, remove `16` characters + * 14:28: Unexpected `27` character line, expected at most `20` characters, remove `7` characters + * 16:26: Unexpected `25` character line, expected at most `20` characters, remove `5` characters + * 18:27: Unexpected `26` character line, expected at most `20` characters, remove `6` characters + * 20:43: Unexpected `42` character line, expected at most `20` characters, remove `22` characters * - * This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo - * long. + * @example + * {"config": 20, "frontmatter": true, "name": "ok.md", "positionless": true} * - * Just like thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis one. + * --- + * description: Mercury mercury mercury mercury. + * --- * - * And this one is also very wrong: because the link starts aaaaaaafter the column: + * @example + * {"config": 20, "gfm": true, "name": "ok.md", "positionless": true} * - * and such. + * | Mercury | Mercury | Mercury | + * | ------- | ------- | ------- | * - * And this one is also very wrong: because the code starts aaaaaaafter the column: `alpha.bravo()` + * @example + * {"config": 20, "math": true, "name": "ok.md", "positionless": true} * - * `alphaBravoCharlieDeltaEchoFoxtrotGolfHotelIndiaJuliettKiloLimaMikeNovemberOscar.papa()` and such. + * $$ + * L = \frac{1}{2} \rho v^2 S C_L + * $$ * * @example - * {"name": "not-ok.md", "config": 80, "label": "output", "positionless": true} + * {"config": 20, "mdx": true, "name": "ok.md", "positionless": true} * - * 4:86: Line must be at most 80 characters - * 6:99: Line must be at most 80 characters - * 8:96: Line must be at most 80 characters - * 10:97: Line must be at most 80 characters - * 12:99: Line must be at most 80 characters + * export const description = 'Mercury mercury mercury mercury.' + * + * {description} * * @example - * {"name": "ok-mixed-line-endings.md", "config": 10, "positionless": true} + * {"config": 10, "name": "ok-mixed-line-endings.md", "positionless": true} * * 0123456789␍␊0123456789␊01234␍␊01234␊ * * @example - * {"name": "not-ok-mixed-line-endings.md", "config": 10, "label": "input", "positionless": true} + * {"config": 10, "label": "input", "name": "not-ok-mixed-line-endings.md", "positionless": true} * * 012345678901␍␊012345678901␊01234567890␍␊01234567890␊ * * @example - * {"name": "not-ok-mixed-line-endings.md", "config": 10, "label": "output", "positionless": true} + * {"config": 10, "label": "output", "name": "not-ok-mixed-line-endings.md", "positionless": true} + * + * 1:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters + * 2:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters + * 3:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character + * 4:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character + * + * @example + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} * - * 1:13: Line must be at most 10 characters - * 2:13: Line must be at most 10 characters - * 3:12: Line must be at most 10 characters - * 4:12: Line must be at most 10 characters + * 1:1: Unexpected value `🌍` for `options`, expected `number` */ /** @@ -125,9 +157,10 @@ /// +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {SKIP, visit} from 'unist-util-visit' const remarkLintMaximumLineLength = lintRule( { @@ -145,16 +178,29 @@ const remarkLintMaximumLineLength = lintRule( function (tree, file, options) { const value = String(file) const lines = value.split(/\r?\n/) - const option = options || 80 + let expected = 80 + + if (options === null || options === undefined) { + // Empty. + } else if (typeof options === 'number') { + expected = options + } else { + file.fail( + 'Unexpected value `' + options + '` for `options`, expected `number`' + ) + } - // Allow nodes that cannot be wrapped. - visit(tree, function (node) { + // eslint-disable-next-line complexity + visit(tree, function (node, index, parent) { + // Allow nodes that cannot be wrapped. if ( node.type === 'code' || node.type === 'definition' || node.type === 'heading' || node.type === 'html' || - node.type === 'mdxJsxTextElement' || + node.type === 'math' || + node.type === 'mdxjsEsm' || + node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression' || node.type === 'table' || // @ts-expect-error: TOML from frontmatter. @@ -165,44 +211,52 @@ const remarkLintMaximumLineLength = lintRule( const start = pointStart(node) if (end && start) { - allowList(start.line - 1, end.line) + let line = start.line - 1 + while (line < end.line) { + lines[line++] = '' + } } - } - }) - // Allow text spans to cross the border. - visit(tree, function (node, index, parent) { - const final = pointEnd(node) - const initial = pointStart(node) + return SKIP + } + // Allow text spans to cross the border. if ( - (node.type === 'image' || - node.type === 'inlineCode' || - node.type === 'link') && - initial && - final && - parent && - typeof index === 'number' + node.type === 'image' || + node.type === 'inlineCode' || + node.type === 'link' ) { - // Not allowing when starting after the border, or ending before it. - if (initial.column > option || final.column < option) { - return - } + const end = pointEnd(node) + const start = pointStart(node) - const next = parent.children[index + 1] - const nextStart = pointStart(next) - - // Not allowing when there’s whitespace after the link. - if ( - next && - nextStart && - nextStart.line === initial.line && - (!('value' in next) || /^(.+?[ \t].+?)/.test(next.value)) - ) { - return - } + if (end && start && parent && typeof index === 'number') { + // Not allowing when starting after the border. + if (start.column > expected) return + + // Not allowing when ending before it. + if (end.column < expected) return + + const next = parent.children[index + 1] + const nextStart = pointStart(next) + + // Not allowing when there’s a following child. + if ( + next && + nextStart && + nextStart.line === start.line && + // Either something with children: + (!('value' in next) || + // Or with whitespace: + /[ \t]/.test(next.value)) + ) { + return + } - allowList(initial.line - 1, final.line) + let line = start.line - 1 + while (line < end.line) { + lines[line++] = '' + } + } } }) @@ -210,29 +264,25 @@ const remarkLintMaximumLineLength = lintRule( let index = -1 while (++index < lines.length) { - const lineLength = lines[index].length - - if (lineLength > option) { - file.message('Line must be at most ' + option + ' characters', { - line: index + 1, - column: lineLength + 1 - }) - } - } + const actualBytes = lines[index].length + const actualCharacters = Array.from(lines[index]).length + const difference = actualCharacters - expected - /** - * Allowlist from `initial` to `final`, zero-based. - * - * @param {number} initial - * Initial line. - * @param {number} final - * Final line. - * @returns {undefined} - * Nothing. - */ - function allowList(initial, final) { - while (initial < final) { - lines[initial++] = '' + if (difference > 0) { + file.message( + 'Unexpected `' + + actualCharacters + + '` character line, expected at most `' + + expected + + '` characters, remove `' + + difference + + '` ' + + pluralize('character', difference), + { + line: index + 1, + column: actualBytes + 1 + } + ) } } } diff --git a/packages/remark-lint-maximum-line-length/package.json b/packages/remark-lint-maximum-line-length/package.json index cf95306f..fb2f1415 100644 --- a/packages/remark-lint-maximum-line-length/package.json +++ b/packages/remark-lint-maximum-line-length/package.json @@ -32,6 +32,7 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "pluralize": "^8.0.0", "mdast-util-mdx": "^3.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", diff --git a/packages/remark-lint-maximum-line-length/readme.md b/packages/remark-lint-maximum-line-length/readme.md index f04ec3bf..3369e891 100644 --- a/packages/remark-lint-maximum-line-length/readme.md +++ b/packages/remark-lint-maximum-line-length/readme.md @@ -151,75 +151,146 @@ Whether to wrap prose or not is a stylistic choice. ###### In -> 👉 **Note**: this example uses -> GFM ([`remark-gfm`][github-remark-gfm]). +```markdown +Mercury mercury mercury mercury mercury mercury mercury mercury mercury mercury +mercury. + +Mercury mercury mercury mercury mercury mercury mercury mercury mercury `mercury()`. + +Mercury mercury mercury mercury mercury mercury mercury mercury mercury . + +Mercury mercury mercury mercury mercury mercury mercury mercury mercury [mercury](http://localhost). + +Mercury mercury mercury mercury mercury mercury mercury mercury mercury ![mercury](http://localhost). + +
Mercury mercury mercury mercury mercury mercury mercury mercury mercury
+ +[foo]: http://localhost/mercury/mercury/mercury/mercury/mercury/mercury/mercury/mercury +``` + +###### Out + +No messages. + +##### `not-ok.md` + +When configured with `20`. + +###### In ```markdown -This line is simply not toooooooooooooooooooooooooooooooooooooooooooo -long. +Mercury mercury mercury +mercury. + +Mercury mercury mercury `mercury()`. + +Mercury mercury mercury . -This is also fine: +Mercury mercury mercury [m](example.com). - +Mercury mercury mercury ![m](example.com). -`alphaBravoCharlieDeltaEchoFoxtrotGolfHotelIndiaJuliettKiloLimaMikeNovemberOscarPapaQuebec.romeo()` +`mercury()` mercury mercury mercury. -[foo](http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables) + mercury. - +[m](example.com) mercury. -![foo](http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables) +![m](example.com) mercury. + +Mercury mercury ![m](example.com) mercury. +``` -| An | exception | is | line | length | in | long | tables | because | those | can’t | just | -| -- | --------- | -- | ---- | ------ | -- | ---- | ------ | ------- | ----- | ----- | ---- | -| be | helped | | | | | | | | | | . | +###### Out + +```text +1:24: Unexpected `23` character line, expected at most `20` characters, remove `3` characters +4:37: Unexpected `36` character line, expected at most `20` characters, remove `16` characters +6:44: Unexpected `43` character line, expected at most `20` characters, remove `23` characters +8:42: Unexpected `41` character line, expected at most `20` characters, remove `21` characters +10:43: Unexpected `42` character line, expected at most `20` characters, remove `22` characters +12:37: Unexpected `36` character line, expected at most `20` characters, remove `16` characters +14:28: Unexpected `27` character line, expected at most `20` characters, remove `7` characters +16:26: Unexpected `25` character line, expected at most `20` characters, remove `5` characters +18:27: Unexpected `26` character line, expected at most `20` characters, remove `6` characters +20:43: Unexpected `42` character line, expected at most `20` characters, remove `22` characters +``` -

alpha bravo charlie delta echo foxtrot golf

+##### `ok.md` -The following is also fine (note the `.`), because there is no whitespace. +When configured with `20`. -. +###### In -In addition, definitions are also fine: +> 👉 **Note**: this example uses +> frontmatter ([`remark-frontmatter`][github-remark-frontmatter]). -[foo]: +```markdown +--- +description: Mercury mercury mercury mercury. +--- ``` ###### Out No messages. -##### `not-ok.md` +##### `ok.md` -When configured with `80`. +When configured with `20`. ###### In +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + ```markdown -This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo -long. +| Mercury | Mercury | Mercury | +| ------- | ------- | ------- | +``` -Just like thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis one. +###### Out -And this one is also very wrong: because the link starts aaaaaaafter the column: +No messages. - and such. +##### `ok.md` -And this one is also very wrong: because the code starts aaaaaaafter the column: `alpha.bravo()` +When configured with `20`. -`alphaBravoCharlieDeltaEchoFoxtrotGolfHotelIndiaJuliettKiloLimaMikeNovemberOscar.papa()` and such. +###### In + +> 👉 **Note**: this example uses +> math ([`remark-math`][github-remark-math]). + +```markdown +$$ +L = \frac{1}{2} \rho v^2 S C_L +$$ ``` ###### Out -```text -4:86: Line must be at most 80 characters -6:99: Line must be at most 80 characters -8:96: Line must be at most 80 characters -10:97: Line must be at most 80 characters -12:99: Line must be at most 80 characters +No messages. + +##### `ok.md` + +When configured with `20`. + +###### In + +> 👉 **Note**: this example uses +> MDX ([`remark-mdx`][github-remark-mdx]). + +```mdx +export const description = 'Mercury mercury mercury mercury.' + +{description} ``` +###### Out + +No messages. + ##### `ok-mixed-line-endings.md` When configured with `10`. @@ -247,10 +318,20 @@ When configured with `10`. ###### Out ```text -1:13: Line must be at most 10 characters -2:13: Line must be at most 10 characters -3:12: Line must be at most 10 characters -4:12: Line must be at most 10 characters +1:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters +2:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters +3:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character +4:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character +``` + +##### `not-ok.md` + +When configured with `'🌍'`. + +###### Out + +```text +1:1: Unexpected value `🌍` for `options`, expected `number` ``` ## Compatibility @@ -322,10 +403,16 @@ abide by its terms. [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c +[github-remark-frontmatter]: https://github.com/remarkjs/remark-frontmatter + [github-remark-gfm]: https://github.com/remarkjs/remark-gfm [github-remark-lint]: https://github.com/remarkjs/remark-lint +[github-remark-math]: https://github.com/remarkjs/remark-math + +[github-remark-mdx]: https://mdxjs.com/packages/remark-mdx/ + [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer [npm-install]: https://docs.npmjs.com/cli/install diff --git a/packages/remark-lint-no-blockquote-without-marker/index.js b/packages/remark-lint-no-blockquote-without-marker/index.js index 09c49047..fef55506 100644 --- a/packages/remark-lint-no-blockquote-without-marker/index.js +++ b/packages/remark-lint-no-blockquote-without-marker/index.js @@ -45,50 +45,82 @@ * @example * {"name": "ok.md"} * - * > Foo… - * > …bar… - * > …baz. + * > Mercury, + * > Venus, + * > and Earth. + * + * Mars. * * @example * {"name": "ok-tabs.md"} * - * >␉Foo… - * >␉…bar… - * >␉…baz. + * >␉Mercury, + * >␉Venus, + * >␉and Earth. + * + * @example + * {"label": "input", "name": "not-ok.md"} * + * > Mercury, + * Venus, + * > and Earth. * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "output", "name": "not-ok.md"} * - * > Foo… - * …bar… - * > …baz. + * 2:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "input", "name": "not-ok-tabs.md"} * - * 2:1: Missing marker in block quote + * >␉Mercury, + * ␉Venus, + * and Earth. + * @example + * {"label": "output", "name": "not-ok-tabs.md"} + * + * 2:2: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker + * 3:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker * * @example - * {"name": "not-ok-tabs.md", "label": "input"} + * {"label": "input", "name": "containers.md"} + * + * * > Mercury and + * Venus. + * + * > * Mercury and + * Venus. * - * >␉Foo… - * ␉…bar… - * …baz. + * * > * Mercury and + * Venus. * + * > * > Mercury and + * Venus. + * + * *** + * + * > * > Mercury and + * > Venus. * @example - * {"name": "not-ok-tabs.md", "label": "output"} + * {"label": "output", "name": "containers.md"} * - * 2:1: Missing marker in block quote - * 3:1: Missing marker in block quote + * 2:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker + * 5:3: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker + * 8:5: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker + * 11:7: Unexpected `0` block quote markers before paragraph line, expected `2` markers, add `2` markers + * 16:7: Unexpected `1` block quote marker before paragraph line, expected `2` markers, add `1` marker */ /** * @typedef {import('mdast').Root} Root */ +/// + +import {ok as assert} from 'devlop' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {SKIP, visitParents} from 'unist-util-visit-parents' import {location} from 'vfile-location' const remarkLintNoBlockquoteWithoutMarker = lintRule( @@ -106,35 +138,81 @@ const remarkLintNoBlockquoteWithoutMarker = lintRule( const value = String(file) const loc = location(file) - visit(tree, 'blockquote', function (node) { - let index = -1 - - while (++index < node.children.length) { - const child = node.children[index] - const start = pointStart(child) - const end = pointEnd(child) - - if (child.type === 'paragraph' && start && end) { - const column = start.column - let line = start.line - - // Skip past the first line. - while (++line <= end.line) { - const offset = loc.toOffset({line, column}) - - if ( - typeof offset !== 'number' || - />[\t ]+$/.test(value.slice(offset - 5, offset)) - ) { - continue - } - - // Roughly here. - file.message('Missing marker in block quote', { - line, - column: column - 2 - }) + // Only paragraphs can be lazy. + visitParents(tree, 'paragraph', function (node, parents) { + let expected = 0 + + for (const parent of parents) { + if (parent.type === 'blockquote') { + expected++ + } + // All known parents that only use whitespace for indent. + else if ( + parent.type === 'containerDirective' || + parent.type === 'footnoteDefinition' || + parent.type === 'list' || + parent.type === 'listItem' || + parent.type === 'root' + ) { + // Empty. + /* c8 ignore next 3 -- exit on unknown nodes. */ + } else { + return SKIP + } + } + + if (!expected) return SKIP + + const end = pointEnd(node) + const start = pointStart(node) + + if (!end || !start) return SKIP + + let line = start.line + + while (++line <= end.line) { + // Skip first line. + const lineStart = loc.toOffset({line, column: 1}) + assert(lineStart !== undefined) // Always defined. + let actual = 0 + let index = lineStart + + while (index < value.length) { + const code = value.charCodeAt(index) + + if (code === 9 || code === 32) { + // Fine. + } else if (code === 62 /* `>` */) { + actual++ + } else { + break } + + index++ + } + + const point = loc.toPoint(index) + assert(point) // Always defined. + + const difference = expected - actual + + // Roughly here. + if (difference) { + file.message( + 'Unexpected `' + + actual + + '` block quote ' + + pluralize('marker', actual) + + ' before paragraph line, expected `' + + expected + + '` ' + + pluralize('marker', expected) + + ', add `' + + difference + + '` ' + + pluralize('marker', difference), + {ancestors: [...parents, node], place: point} + ) } } }) diff --git a/packages/remark-lint-no-blockquote-without-marker/package.json b/packages/remark-lint-no-blockquote-without-marker/package.json index 4ebe09c9..54dc69e4 100644 --- a/packages/remark-lint-no-blockquote-without-marker/package.json +++ b/packages/remark-lint-no-blockquote-without-marker/package.json @@ -33,9 +33,12 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-directive": "^3.0.0", + "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^5.0.0", "vfile-location": "^5.0.0" }, "scripts": {}, @@ -48,7 +51,8 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-no-blockquote-without-marker/readme.md b/packages/remark-lint-no-blockquote-without-marker/readme.md index ec9d1c02..56affb3f 100644 --- a/packages/remark-lint-no-blockquote-without-marker/readme.md +++ b/packages/remark-lint-no-blockquote-without-marker/readme.md @@ -152,9 +152,11 @@ in a block quote. ###### In ```markdown -> Foo… -> …bar… -> …baz. +> Mercury, +> Venus, +> and Earth. + +Mars. ``` ###### Out @@ -166,9 +168,9 @@ No messages. ###### In ```markdown ->␉Foo… ->␉…bar… ->␉…baz. +>␉Mercury, +>␉Venus, +>␉and Earth. ``` ###### Out @@ -180,15 +182,15 @@ No messages. ###### In ```markdown -> Foo… -…bar… -> …baz. +> Mercury, +Venus, +> and Earth. ``` ###### Out ```text -2:1: Missing marker in block quote +2:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker ``` ##### `not-ok-tabs.md` @@ -196,16 +198,49 @@ No messages. ###### In ```markdown ->␉Foo… -␉…bar… -…baz. +>␉Mercury, +␉Venus, +and Earth. +``` + +###### Out + +```text +2:2: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker +3:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker +``` + +##### `containers.md` + +###### In + +```markdown +* > Mercury and +Venus. + +> * Mercury and + Venus. + +* > * Mercury and + Venus. + +> * > Mercury and + Venus. + +*** + +> * > Mercury and +> Venus. ``` ###### Out ```text -2:1: Missing marker in block quote -3:1: Missing marker in block quote +2:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker +5:3: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker +8:5: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker +11:7: Unexpected `0` block quote markers before paragraph line, expected `2` markers, add `2` markers +16:7: Unexpected `1` block quote marker before paragraph line, expected `2` markers, add `1` marker ``` ## Compatibility diff --git a/packages/remark-lint-no-consecutive-blank-lines/index.js b/packages/remark-lint-no-consecutive-blank-lines/index.js index d8121b0c..2e84cb59 100644 --- a/packages/remark-lint-no-consecutive-blank-lines/index.js +++ b/packages/remark-lint-no-consecutive-blank-lines/index.js @@ -41,37 +41,207 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * Foo…␊␊…Bar. + * # Planets + * + * Mercury. + * + * Venus. + * + * @example + * {"label": "input", "name": "not-ok.md"} + * + * # Planets + * + * + * Mercury. + * + * + * + * Venus. + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 4:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line + * 8:1: Unexpected `3` blank lines before node, expected up to `1` blank line, remove `2` blank lines + * + * @example + * {"label": "input", "name": "initial.md"} + * + * ␊Mercury. + * @example + * {"label": "output", "name": "initial.md"} + * + * 2:1: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line + * + * @example + * {"name": "final-one.md"} + * + * Mercury.␊ + * + * @example + * {"label": "input", "name": "final-more.md"} + * + * Mercury.␊␊ + * @example + * {"label": "output", "name": "final-more.md"} + * + * 1:9: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line * * @example * {"name": "empty-document.md"} * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "block-quote.md"} + * + * > Mercury. + * + * Venus. + * + * > + * > Earth. + * > + * @example + * {"label": "output", "name": "block-quote.md"} + * + * 6:3: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line + * 6:9: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line + * + * @example + * {"directive": true, "label": "input", "name": "directive.md"} + * + * :::mercury + * Venus. + * + * + * Earth. + * ::: + * @example + * {"directive": true, "label": "output", "name": "directive.md"} + * + * 5:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line + * + * @example + * {"gfm": true, "label": "input", "name": "footnote.md"} + * + * [^x]: + * Mercury. + * + * Venus. + * + * [^y]: + * + * Earth. + * + * + * Mars. + * @example + * {"gfm": true, "label": "output", "name": "footnote.md"} + * + * 8:5: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line + * 11:5: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line + * + * @example + * {"label": "input", "mdx": true, "name": "jsx.md"} + * + * + * Venus. + * + * + * Earth. + * + * @example + * {"label": "output", "mdx": true, "name": "jsx.md"} + * + * 5:3: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line + * + * @example + * {"label": "input", "name": "list.md"} + * + * * Mercury. + * * Venus. + * + * *** + * + * * Mercury. + * + * * Venus. + * + * *** + * + * * Mercury. + * + * + * * Venus. + * @example + * {"label": "output", "name": "list.md"} + * + * 15:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line + * + * @example + * {"label": "input", "name": "list-item.md"} + * + * * Mercury. + * Venus. + * + * *** + * + * * Mercury. + * + * Venus. + * + * *** + * + * * Mercury. + * + * + * Venus. * - * Foo…␊␊␊…Bar␊␊␊ + * *** * + * * + * Mercury. * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "list-item.md"} * - * 4:1: Remove 1 line before node - * 4:5: Remove 2 lines after node + * 15:3: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line + * 20:3: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line + * + * @example + * {"label": "input", "name": "deep-block-quote.md"} + * + * * > * > # Venus␊␊ + * @example + * {"label": "output", "name": "deep-block-quote.md"} + * + * 1:16: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line + * + * @example + * {"label": "input", "name": "deep-list-item.md"} + * + * > * > * # Venus␊␊ + * @example + * {"label": "output", "name": "deep-list-item.md"} + * + * 1:16: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line */ /** + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root - * @typedef {import('unist').Point} Point */ -import plural from 'pluralize' +/// +/// + +import {phrasing} from 'mdast-util-phrasing' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' - -const unknownContainerSize = new Set(['mdxJsxFlowElement', 'mdxJsxTextElement']) +import {SKIP, visitParents} from 'unist-util-visit-parents' const remarkLintNoConsecutiveBlankLines = lintRule( { @@ -85,79 +255,108 @@ const remarkLintNoConsecutiveBlankLines = lintRule( * Nothing. */ function (tree, file) { - visit(tree, function (node) { - if ('children' in node) { + visitParents(tree, function (node, parents) { + const parent = parents.at(-1) + + // Ignore phrasing nodes and non-parents. + if (!parent) return + if (phrasing(node)) return SKIP + + const siblings = /** @type {Array} */ (parent.children) + const index = siblings.indexOf(node) + + // Compare parent and first child. + if ( + index === 0 && + // Container directives and JSX have arbitrary opening length. + parent.type !== 'containerDirective' && + parent.type !== 'mdxJsxFlowElement' + ) { + const parentStart = pointStart(parent) const start = pointStart(node) - const head = node.children[0] - const headStart = pointStart(head) - if (head && headStart && start) { - if (!unknownContainerSize.has(node.type)) { - // Compare parent and first child. - compare(start, headStart, 0) + if (parentStart && start) { + // For footnote definitions, the first line with the label can + // otherwise be empty. + const difference = + start.line - + parentStart.line - + (parent.type === 'footnoteDefinition' ? 1 : 0) + + if (difference > 0) { + file.message( + 'Unexpected `' + + difference + + '` blank ' + + pluralize('line', difference) + + ' before node, expected `0` blank lines, remove `' + + difference + + '` blank ' + + pluralize('line', difference), + {ancestors: [...parents, node], place: start} + ) } + } + } - // Compare between each child. - let index = -1 + const next = siblings[index + 1] + const end = pointEnd(node) + const nextStart = pointStart(next) - while (++index < node.children.length) { - const previous = node.children[index - 1] - const child = node.children[index] - const previousEnd = pointEnd(previous) - const childStart = pointStart(child) + // Compare child and next sibling. + if (end && nextStart) { + // `2` for line ending after node and optional line ending of blank + // line. + const difference = nextStart.line - end.line - 2 - if (previous && previousEnd && childStart) { - compare(previousEnd, childStart, 2) - } - } + if (difference > 0) { + const actual = difference + 1 - const end = pointEnd(node) - const tail = node.children[node.children.length - 1] - const tailEnd = pointEnd(tail) - - // Compare parent and last child. - if ( - end && - tailEnd && - tail !== head && - !unknownContainerSize.has(node.type) - ) { - compare(end, tailEnd, 1) - } + file.message( + 'Unexpected `' + + actual + + '` blank ' + + pluralize('line', actual) + + ' before node, expected up to `1` blank line, remove `' + + difference + + '` blank ' + + pluralize('line', difference), + {ancestors: [...parents, next], place: nextStart} + ) } } - }) - /** - * Compare the difference between `start` and `end`, and warn when that - * difference exceeds `max`. - * - * @param {Point} start - * Start. - * @param {Point} end - * End. - * @param {0 | 1 | 2} max - * Max. - * @returns {undefined} - * Nothing. - */ - function compare(start, end, max) { - const diff = end.line - start.line - const lines = Math.abs(diff) - max - - if (lines > 0) { - file.message( - 'Remove ' + - lines + - ' ' + - plural('line', Math.abs(lines)) + - ' ' + - (diff > 0 ? 'before' : 'after') + - ' node', - end - ) + const parentEnd = pointEnd(parent) + + // Compare parent and last child. + if ( + !next && + parentEnd && + end && + // Container directives and JSX have arbitrary closing length. + parent.type !== 'containerDirective' && + parent.type !== 'mdxJsxFlowElement' + ) { + // Block quote can have extra blank lines in them if with `>`. + // Other containers cannot. + const difference = + parentEnd.line - end.line - (parent.type === 'blockquote' ? 0 : 1) + + if (difference > 0) { + file.message( + 'Unexpected `' + + difference + + '` blank ' + + pluralize('line', difference) + + ' after node, expected `0` blank lines, remove `' + + difference + + '` blank ' + + pluralize('line', difference), + {ancestors: [...parents, node], place: end} + ) + } } - } + }) } ) diff --git a/packages/remark-lint-no-consecutive-blank-lines/package.json b/packages/remark-lint-no-consecutive-blank-lines/package.json index b45ecd44..8198a385 100644 --- a/packages/remark-lint-no-consecutive-blank-lines/package.json +++ b/packages/remark-lint-no-consecutive-blank-lines/package.json @@ -32,11 +32,13 @@ ], "dependencies": { "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", + "mdast-util-directive": "^3.0.0", + "mdast-util-mdx": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -49,7 +51,8 @@ "prettier": true, "rules": { "capitalized-comments": "off", - "unicorn/prefer-at": "off" + "unicorn/prefer-at": "off", + "unicorn/prefer-set-has": "off" } } } diff --git a/packages/remark-lint-no-consecutive-blank-lines/readme.md b/packages/remark-lint-no-consecutive-blank-lines/readme.md index 69ef2c47..f64d0a4d 100644 --- a/packages/remark-lint-no-consecutive-blank-lines/readme.md +++ b/packages/remark-lint-no-consecutive-blank-lines/readme.md @@ -150,32 +150,266 @@ It has a `join` option to configure more complex cases. ###### In ```markdown -Foo…␊␊…Bar. +# Planets + +Mercury. + +Venus. ``` ###### Out No messages. +##### `not-ok.md` + +###### In + +```markdown +# Planets + + +Mercury. + + + +Venus. +``` + +###### Out + +```text +4:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line +8:1: Unexpected `3` blank lines before node, expected up to `1` blank line, remove `2` blank lines +``` + +##### `initial.md` + +###### In + +```markdown +␊Mercury. +``` + +###### Out + +```text +2:1: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line +``` + +##### `final-one.md` + +###### In + +```markdown +Mercury.␊ +``` + +###### Out + +No messages. + +##### `final-more.md` + +###### In + +```markdown +Mercury.␊␊ +``` + +###### Out + +```text +1:9: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line +``` + ##### `empty-document.md` ###### Out No messages. -##### `not-ok.md` +##### `block-quote.md` + +###### In + +```markdown +> Mercury. + +Venus. + +> +> Earth. +> +``` + +###### Out + +```text +6:3: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line +6:9: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line +``` + +##### `directive.md` ###### In +> 👉 **Note**: this example uses +> directives ([`remark-directive`][github-remark-directive]). + ```markdown -Foo…␊␊␊…Bar␊␊␊ +:::mercury +Venus. + + +Earth. +::: ``` ###### Out ```text -4:1: Remove 1 line before node -4:5: Remove 2 lines after node +5:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line +``` + +##### `footnote.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +[^x]: + Mercury. + +Venus. + +[^y]: + + Earth. + + + Mars. +``` + +###### Out + +```text +8:5: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line +11:5: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line +``` + +##### `jsx.md` + +###### In + +> 👉 **Note**: this example uses +> MDX ([`remark-mdx`][github-remark-mdx]). + +```mdx + + Venus. + + + Earth. + +``` + +###### Out + +```text +5:3: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line +``` + +##### `list.md` + +###### In + +```markdown +* Mercury. +* Venus. + +*** + +* Mercury. + +* Venus. + +*** + +* Mercury. + + +* Venus. +``` + +###### Out + +```text +15:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line +``` + +##### `list-item.md` + +###### In + +```markdown +* Mercury. + Venus. + +*** + +* Mercury. + + Venus. + +*** + +* Mercury. + + + Venus. + +*** + +* + Mercury. +``` + +###### Out + +```text +15:3: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line +20:3: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line +``` + +##### `deep-block-quote.md` + +###### In + +```markdown +* > * > # Venus␊␊ +``` + +###### Out + +```text +1:16: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line +``` + +##### `deep-list-item.md` + +###### In + +```markdown +> * > * # Venus␊␊ +``` + +###### Out + +```text +1:16: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line ``` ## Compatibility @@ -247,8 +481,14 @@ abide by its terms. [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c +[github-remark-directive]: https://github.com/remarkjs/remark-directive + +[github-remark-gfm]: https://github.com/remarkjs/remark-gfm + [github-remark-lint]: https://github.com/remarkjs/remark-lint +[github-remark-mdx]: https://mdxjs.com/packages/remark-mdx/ + [github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer diff --git a/packages/remark-lint-no-duplicate-defined-urls/index.js b/packages/remark-lint-no-duplicate-defined-urls/index.js index 53667389..70438c30 100644 --- a/packages/remark-lint-no-duplicate-defined-urls/index.js +++ b/packages/remark-lint-no-duplicate-defined-urls/index.js @@ -35,32 +35,33 @@ * @author Titus Wormer * @copyright 2020 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * [alpha]: alpha.com - * [bravo]: bravo.com + * [mercury]: https://example.com/mercury/ + * [venus]: https://example.com/venus/ * * @example - * {"name": "not-ok.md", "label": "input"} - * - * [alpha]: alpha.com - * [bravo]: alpha.com + * {"label": "input", "name": "not-ok.md"} * + * [mercury]: https://example.com/mercury/ + * [venus]: https://example.com/mercury/ * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 2:1-2:19: Do not use different definitions with the same URL (1:1) + * 2:1-2:38: Unexpected definition with an already defined URL (as `mercury`), expected unique URLs */ /** + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root */ +import {ok as assert} from 'devlop' import {lintRule} from 'unified-lint-rule' -import {pointStart, position} from 'unist-util-position' -import {stringifyPosition} from 'unist-util-stringify-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintNoDuplicateDefinedUrls = lintRule( { @@ -74,27 +75,39 @@ const remarkLintNoDuplicateDefinedUrls = lintRule( * Nothing. */ function (tree, file) { - /** @type {Map} */ + /** @type {Map>} */ const map = new Map() - visit(tree, 'definition', function (node) { - const place = position(node) - const start = pointStart(node) + visitParents(tree, 'definition', function (node, parents) { + const ancestors = [...parents, node] + + if (node.position && node.url) { + const urlNormal = String(node.url).toUpperCase() + const duplicateAncestors = map.get(urlNormal) - if (place && start && node.url) { - const url = String(node.url).toUpperCase() - const duplicate = map.get(url) + if (duplicateAncestors) { + const duplicate = duplicateAncestors.at(-1) + assert(duplicate) // Always defined. + assert(duplicate.type === 'definition') // Always tail. - if (duplicate) { file.message( - 'Do not use different definitions with the same URL (' + - duplicate + - ')', - place + 'Unexpected definition with an already defined URL (as `' + + duplicate.identifier + + '`), expected unique URLs', + { + ancestors, + cause: new VFileMessage('URL already defined here', { + ancestors: duplicateAncestors, + place: duplicate.position, + source: 'remark-lint', + ruleId: 'no-duplicate-defined-urls' + }), + place: node.position + } ) } - map.set(url, stringifyPosition(start)) + map.set(urlNormal, ancestors) } }) } diff --git a/packages/remark-lint-no-duplicate-defined-urls/package.json b/packages/remark-lint-no-duplicate-defined-urls/package.json index 2a88b465..d5aea60d 100644 --- a/packages/remark-lint-no-duplicate-defined-urls/package.json +++ b/packages/remark-lint-no-duplicate-defined-urls/package.json @@ -33,10 +33,10 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-duplicate-defined-urls/readme.md b/packages/remark-lint-no-duplicate-defined-urls/readme.md index d6f17a4f..f78b57ce 100644 --- a/packages/remark-lint-no-duplicate-defined-urls/readme.md +++ b/packages/remark-lint-no-duplicate-defined-urls/readme.md @@ -140,8 +140,8 @@ identifiers. ###### In ```markdown -[alpha]: alpha.com -[bravo]: bravo.com +[mercury]: https://example.com/mercury/ +[venus]: https://example.com/venus/ ``` ###### Out @@ -153,14 +153,14 @@ No messages. ###### In ```markdown -[alpha]: alpha.com -[bravo]: alpha.com +[mercury]: https://example.com/mercury/ +[venus]: https://example.com/mercury/ ``` ###### Out ```text -2:1-2:19: Do not use different definitions with the same URL (1:1) +2:1-2:38: Unexpected definition with an already defined URL (as `mercury`), expected unique URLs ``` ## Compatibility diff --git a/packages/remark-lint-no-duplicate-definitions/index.js b/packages/remark-lint-no-duplicate-definitions/index.js index 8966e72e..510f394a 100644 --- a/packages/remark-lint-no-duplicate-definitions/index.js +++ b/packages/remark-lint-no-duplicate-definitions/index.js @@ -34,45 +34,50 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * [foo]: bar - * [baz]: qux + * [mercury]: https://example.com/mercury/ + * [venus]: https://example.com/venus/ * * @example - * {"name": "not-ok.md", "label": "input"} - * - * [foo]: bar - * [foo]: qux + * {"label": "input", "name": "not-ok.md"} * + * [mercury]: https://example.com/mercury/ + * [mercury]: https://example.com/venus/ * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 2:1-2:11: Do not use definitions with the same identifier (1:1) + * 2:1-2:38: Unexpected definition with an already defined identifier (`mercury`), expected unique identifiers * * @example * {"gfm": true, "label": "input", "name": "gfm.md"} * - * GFM footnote definitions are checked too[^a]. + * Mercury[^mercury]. * - * [^a]: alpha - * [^a]: bravo + * [^mercury]: + * Mercury is the first planet from the Sun and the smallest in the Solar + * System. + * + * [^mercury]: + * Venus is the second planet from the Sun. * * @example * {"gfm": true, "label": "output", "name": "gfm.md"} * - * 4:1-4:12: Do not use footnote definitions with the same identifier (3:1) + * 7:1-7:12: Unexpected footnote definition with an already defined identifier (`mercury`), expected unique identifiers */ /** + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root */ +import {ok as assert} from 'devlop' import {lintRule} from 'unified-lint-rule' -import {pointStart, position} from 'unist-util-position' -import {stringifyPosition} from 'unist-util-stringify-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' /** @type {ReadonlyArray} */ const empty = [] @@ -89,14 +94,12 @@ const remarkLintNoDuplicateDefinitions = lintRule( * Nothing. */ function (tree, file) { - /** @type {Map} */ + /** @type {Map>} */ const definitions = new Map() - /** @type {Map} */ + /** @type {Map>} */ const footnoteDefinitions = new Map() - visit(tree, function (node) { - const place = position(node) - const start = pointStart(node) + visitParents(tree, function (node, parents) { const [map, identifier] = node.type === 'definition' ? [definitions, node.identifier] @@ -104,21 +107,34 @@ const remarkLintNoDuplicateDefinitions = lintRule( ? [footnoteDefinitions, node.identifier] : empty - if (map && identifier && place && start) { - const duplicate = map.get(identifier) + if (map && identifier && node.position) { + const ancestors = [...parents, node] + const duplicateAncestors = map.get(identifier) + + if (duplicateAncestors) { + const duplicate = duplicateAncestors.at(-1) + assert(duplicate) // Always defined. - if (duplicate) { file.message( - 'Do not use' + - (node.type === 'footnoteDefinition' ? ' footnote' : '') + - ' definitions with the same identifier (' + - duplicate + - ')', - place + 'Unexpected ' + + (node.type === 'footnoteDefinition' ? 'footnote ' : '') + + 'definition with an already defined identifier (`' + + identifier + + '`), expected unique identifiers', + { + ancestors, + cause: new VFileMessage('Identifier already defined here', { + ancestors: duplicateAncestors, + place: duplicate.position, + source: 'remark-lint', + ruleId: 'no-duplicate-definitions' + }), + place: node.position + } ) } - map.set(identifier, stringifyPosition(start)) + map.set(identifier, ancestors) } }) } diff --git a/packages/remark-lint-no-duplicate-definitions/package.json b/packages/remark-lint-no-duplicate-definitions/package.json index 9bcc715d..68124ad4 100644 --- a/packages/remark-lint-no-duplicate-definitions/package.json +++ b/packages/remark-lint-no-duplicate-definitions/package.json @@ -32,10 +32,10 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-duplicate-definitions/readme.md b/packages/remark-lint-no-duplicate-definitions/readme.md index 2917bf68..a10bc72e 100644 --- a/packages/remark-lint-no-duplicate-definitions/readme.md +++ b/packages/remark-lint-no-duplicate-definitions/readme.md @@ -143,8 +143,8 @@ It’s a mistake when the same identifier is defined multiple times. ###### In ```markdown -[foo]: bar -[baz]: qux +[mercury]: https://example.com/mercury/ +[venus]: https://example.com/venus/ ``` ###### Out @@ -156,14 +156,14 @@ No messages. ###### In ```markdown -[foo]: bar -[foo]: qux +[mercury]: https://example.com/mercury/ +[mercury]: https://example.com/venus/ ``` ###### Out ```text -2:1-2:11: Do not use definitions with the same identifier (1:1) +2:1-2:38: Unexpected definition with an already defined identifier (`mercury`), expected unique identifiers ``` ##### `gfm.md` @@ -174,16 +174,20 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -GFM footnote definitions are checked too[^a]. +Mercury[^mercury]. -[^a]: alpha -[^a]: bravo +[^mercury]: + Mercury is the first planet from the Sun and the smallest in the Solar + System. + +[^mercury]: + Venus is the second planet from the Sun. ``` ###### Out ```text -4:1-4:12: Do not use footnote definitions with the same identifier (3:1) +7:1-7:12: Unexpected footnote definition with an already defined identifier (`mercury`), expected unique identifiers ``` ## Compatibility diff --git a/packages/remark-lint-no-duplicate-headings-in-section/index.js b/packages/remark-lint-no-duplicate-headings-in-section/index.js index 2c5a5f45..7346e5f6 100644 --- a/packages/remark-lint-no-duplicate-headings-in-section/index.js +++ b/packages/remark-lint-no-duplicate-headings-in-section/index.js @@ -39,80 +39,83 @@ * @example * {"name": "ok.md"} * - * ## Alpha + * # Planets * - * ### Bravo + * ## Venus * - * ## Charlie + * ### Discovery * - * ### Bravo + * ## Mars * - * ### Delta + * ### Discovery * - * #### Bravo + * ### Phobos * - * #### Echo - * - * ##### Bravo + * #### Discovery * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} + * + * # Planets * - * ## Foxtrot + * ## Mars * - * ### Golf + * ### Discovery * - * ### Golf + * ### Discovery * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 5:1-5:9: Do not use headings with similar content per section (3:1) + * 7:1-7:14: Unexpected heading with equivalent text in section, expected unique headings * * @example - * {"name": "not-ok-tolerant-heading-increment.md", "label": "input"} + * {"label": "input", "name": "tolerant-heading-increment.md"} * - * # Alpha + * # Planets * - * #### Bravo + * #### Discovery * - * ###### Charlie + * ###### Phobos * - * #### Bravo + * #### Discovery * - * ###### Delta + * ###### Deimos * * @example - * {"name": "not-ok-tolerant-heading-increment.md", "label": "output"} + * {"label": "output", "name": "tolerant-heading-increment.md"} * - * 7:1-7:11: Do not use headings with similar content per section (3:1) + * 7:1-7:15: Unexpected heading with equivalent text in section, expected unique headings * * @example * {"label": "input", "mdx": true, "name": "mdx.mdx"} * * MDX is supported too. * - *

Alpha

- *

Alpha

+ *

Planets

+ *

Mars

+ *

Discovery

+ *

Discovery

* * @example * {"label": "output", "mdx": true, "name": "mdx.mdx"} * - * 4:1-4:15: Do not use headings with similar content per section (3:1) + * 6:1-6:19: Unexpected heading with equivalent text in section, expected unique headings */ /** * @typedef {import('mdast').Heading} Heading + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root */ /// +import {ok as assert} from 'devlop' import {toString} from 'mdast-util-to-string' import {lintRule} from 'unified-lint-rule' -import {pointStart, position} from 'unist-util-position' -import {stringifyPosition} from 'unist-util-stringify-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const jsxNameRe = /^h([1-6])$/ @@ -128,10 +131,10 @@ const remarkLintNoDuplicateHeadingsInSection = lintRule( * Nothing. */ function (tree, file) { - /** @type {Array>} */ + /** @type {Array>>} */ const stack = [] - visit(tree, function (node) { + visitParents(tree, function (node, parents) { /** @type {Heading['depth'] | undefined} */ let rank @@ -149,23 +152,32 @@ const remarkLintNoDuplicateHeadingsInSection = lintRule( } if (rank) { + const ancestors = [...parents, node] const value = toString(node).toLowerCase() const index = rank - 1 - const scope = stack[index] || (stack[index] = new Map()) - const duplicate = scope.get(value) - const place = position(node) - const start = pointStart(node) + const map = stack[index] || (stack[index] = new Map()) + const duplicateAncestors = map.get(value) + + if (node.position && duplicateAncestors) { + const duplicate = duplicateAncestors.at(-1) + assert(duplicate) // Always defined. - if (place && duplicate) { file.message( - 'Do not use headings with similar content per section (' + - duplicate + - ')', - place + 'Unexpected heading with equivalent text in section, expected unique headings', + { + ancestors, + cause: new VFileMessage('Equivalent heading text defined here', { + ancestors: duplicateAncestors, + place: duplicate.position, + source: 'remark-lint', + ruleId: 'no-duplicate-headings-in-section' + }), + place: node.position + } ) } - scope.set(value, stringifyPosition(start)) + map.set(value, ancestors) // Drop things after it. stack.length = rank } diff --git a/packages/remark-lint-no-duplicate-headings-in-section/package.json b/packages/remark-lint-no-duplicate-headings-in-section/package.json index 9b8399c9..beebad99 100644 --- a/packages/remark-lint-no-duplicate-headings-in-section/package.json +++ b/packages/remark-lint-no-duplicate-headings-in-section/package.json @@ -33,12 +33,12 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-duplicate-headings-in-section/readme.md b/packages/remark-lint-no-duplicate-headings-in-section/readme.md index 751697ab..c14cb258 100644 --- a/packages/remark-lint-no-duplicate-headings-in-section/readme.md +++ b/packages/remark-lint-no-duplicate-headings-in-section/readme.md @@ -141,21 +141,19 @@ section. ###### In ```markdown -## Alpha +# Planets -### Bravo +## Venus -## Charlie +### Discovery -### Bravo +## Mars -### Delta +### Discovery -#### Bravo +### Phobos -#### Echo - -##### Bravo +#### Discovery ``` ###### Out @@ -167,39 +165,41 @@ No messages. ###### In ```markdown -## Foxtrot +# Planets + +## Mars -### Golf +### Discovery -### Golf +### Discovery ``` ###### Out ```text -5:1-5:9: Do not use headings with similar content per section (3:1) +7:1-7:14: Unexpected heading with equivalent text in section, expected unique headings ``` -##### `not-ok-tolerant-heading-increment.md` +##### `tolerant-heading-increment.md` ###### In ```markdown -# Alpha +# Planets -#### Bravo +#### Discovery -###### Charlie +###### Phobos -#### Bravo +#### Discovery -###### Delta +###### Deimos ``` ###### Out ```text -7:1-7:11: Do not use headings with similar content per section (3:1) +7:1-7:15: Unexpected heading with equivalent text in section, expected unique headings ``` ##### `mdx.mdx` @@ -212,14 +212,16 @@ No messages. ```mdx MDX is supported too. -

Alpha

-

Alpha

+

Planets

+

Mars

+

Discovery

+

Discovery

``` ###### Out ```text -4:1-4:15: Do not use headings with similar content per section (3:1) +6:1-6:19: Unexpected heading with equivalent text in section, expected unique headings ``` ## Compatibility diff --git a/packages/remark-lint-no-duplicate-headings/index.js b/packages/remark-lint-no-duplicate-headings/index.js index f54e2c63..36c64092 100644 --- a/packages/remark-lint-no-duplicate-headings/index.js +++ b/packages/remark-lint-no-duplicate-headings/index.js @@ -42,53 +42,51 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * # Foo + * # Mercury * - * ## Bar + * ## Venus * * @example * {"label": "input", "name": "not-ok.md"} * - * # Foo - * - * ## Foo + * # Mercury * - * ## [Foo](http://foo.com/bar) + * ## Mercury * + * ## [Mercury](http://example.com/mercury/) * @example * {"label": "output", "name": "not-ok.md"} * - * 3:1-3:7: Do not use headings with similar content (1:1) - * 5:1-5:29: Do not use headings with similar content (3:1) + * 3:1-3:11: Unexpected heading with equivalent text, expected unique headings + * 5:1-5:42: Unexpected heading with equivalent text, expected unique headings * * @example * {"label": "input", "mdx": true, "name": "mdx.mdx"} * - * MDX is supported too. - * - *

Alpha

- *

Alpha

- * + *

Mercury

+ *

Mercury

* @example * {"label": "output", "mdx": true, "name": "mdx.mdx"} * - * 4:1-4:15: Do not use headings with similar content (3:1) + * 2:1-2:17: Unexpected heading with equivalent text, expected unique headings */ /** + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root */ /// +import {ok as assert} from 'devlop' import {toString} from 'mdast-util-to-string' import {lintRule} from 'unified-lint-rule' -import {pointStart, position} from 'unist-util-position' -import {stringifyPosition} from 'unist-util-stringify-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const jsxNameRe = /^h([1-6])$/ @@ -104,10 +102,10 @@ const remarkLintNoDuplicateHeadings = lintRule( * Nothing. */ function (tree, file) { - /** @type {Map} */ + /** @type {Map>} */ const map = new Map() - visit(tree, function (node) { + visitParents(tree, function (node, parents) { if ( node.type === 'heading' || ((node.type === 'mdxJsxFlowElement' || @@ -115,22 +113,30 @@ const remarkLintNoDuplicateHeadings = lintRule( node.name && jsxNameRe.test(node.name)) ) { - const place = position(node) - const start = pointStart(node) - - if (place && start) { - const value = toString(node).toLowerCase() - const duplicate = map.get(value) + const ancestors = [...parents, node] + const value = toString(node).toLowerCase() + const duplicateAncestors = map.get(value) - if (duplicate) { - file.message( - 'Do not use headings with similar content (' + duplicate + ')', - node - ) - } + if (node.position && duplicateAncestors) { + const duplicate = duplicateAncestors.at(-1) + assert(duplicate) // Always defined. - map.set(value, stringifyPosition(start)) + file.message( + 'Unexpected heading with equivalent text, expected unique headings', + { + ancestors, + cause: new VFileMessage('Equivalent heading text defined here', { + ancestors: duplicateAncestors, + place: duplicate.position, + source: 'remark-lint', + ruleId: 'no-duplicate-headings' + }), + place: node.position + } + ) } + + map.set(value, ancestors) } }) } diff --git a/packages/remark-lint-no-duplicate-headings/package.json b/packages/remark-lint-no-duplicate-headings/package.json index ccf97af7..40ee249e 100644 --- a/packages/remark-lint-no-duplicate-headings/package.json +++ b/packages/remark-lint-no-duplicate-headings/package.json @@ -32,12 +32,12 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-duplicate-headings/readme.md b/packages/remark-lint-no-duplicate-headings/readme.md index 29c6eb97..15edf412 100644 --- a/packages/remark-lint-no-duplicate-headings/readme.md +++ b/packages/remark-lint-no-duplicate-headings/readme.md @@ -151,9 +151,9 @@ which makes linking to them prone to changes. ###### In ```markdown -# Foo +# Mercury -## Bar +## Venus ``` ###### Out @@ -165,18 +165,18 @@ No messages. ###### In ```markdown -# Foo +# Mercury -## Foo +## Mercury -## [Foo](http://foo.com/bar) +## [Mercury](http://example.com/mercury/) ``` ###### Out ```text -3:1-3:7: Do not use headings with similar content (1:1) -5:1-5:29: Do not use headings with similar content (3:1) +3:1-3:11: Unexpected heading with equivalent text, expected unique headings +5:1-5:42: Unexpected heading with equivalent text, expected unique headings ``` ##### `mdx.mdx` @@ -187,16 +187,14 @@ No messages. > MDX ([`remark-mdx`][github-remark-mdx]). ```mdx -MDX is supported too. - -

Alpha

-

Alpha

+

Mercury

+

Mercury

``` ###### Out ```text -4:1-4:15: Do not use headings with similar content (3:1) +2:1-2:17: Unexpected heading with equivalent text, expected unique headings ``` ## Compatibility diff --git a/packages/remark-lint-no-emphasis-as-heading/index.js b/packages/remark-lint-no-emphasis-as-heading/index.js index 02f8292e..a5668b39 100644 --- a/packages/remark-lint-no-emphasis-as-heading/index.js +++ b/packages/remark-lint-no-emphasis-as-heading/index.js @@ -38,38 +38,40 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * # Foo + * # Mercury * - * Bar. + * **Mercury** is the first planet from the Sun and the smallest in the Solar + * System. * * @example - * {"name": "not-ok.md", "label": "input"} - * - * *Foo* + * {"label": "input", "name": "not-ok.md"} * - * Bar. + * **Mercury** * - * __Qux__ + * **Mercury** is the first planet from the Sun and the smallest in the Solar + * System. * - * Quux. + * *Venus* * + * **Venus** is the second planet from the Sun. * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:1-1:6: Don’t use emphasis to introduce a section, use a heading - * 5:1-5:8: Don’t use emphasis to introduce a section, use a heading + * 1:1-1:12: Unexpected strong introducing a section, expected a heading instead + * 6:1-6:8: Unexpected emphasis introducing a section, expected a heading instead */ /** * @typedef {import('mdast').Root} Root + * @typedef {import('mdast').RootContent} RootContent */ import {lintRule} from 'unified-lint-rule' -import {visit} from 'unist-util-visit' -import {position} from 'unist-util-position' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoEmphasisAsHeading = lintRule( { @@ -83,31 +85,37 @@ const remarkLintNoEmphasisAsHeading = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'paragraph', function (node, index, parent) { + visitParents(tree, 'paragraph', function (node, parents) { + const parent = parents.at(-1) + + if (!node.position || !parent) { + return + } + + // Next sibling needs to be a paragraph. + const siblings = /** @type {Array} */ (parent.children) + const next = parent.children[siblings.indexOf(node) + 1] + + if (!next || next.type !== 'paragraph') { + return + } + + // Only child is emphasis/strong. const head = node.children[0] - const place = position(node) if ( - place && - parent && - typeof index === 'number' && - node.children.length === 1 && - (head.type === 'emphasis' || head.type === 'strong') + node.children.length !== 1 || + (head.type !== 'emphasis' && head.type !== 'strong') ) { - const previous = parent.children[index - 1] - const next = parent.children[index + 1] - - if ( - (!previous || previous.type !== 'heading') && - next && - next.type === 'paragraph' - ) { - file.message( - 'Don’t use emphasis to introduce a section, use a heading', - place - ) - } + return } + + file.message( + 'Unexpected ' + + head.type + + ' introducing a section, expected a heading instead', + {ancestors: [...parents, node, head], place: node.position} + ) }) } ) diff --git a/packages/remark-lint-no-emphasis-as-heading/package.json b/packages/remark-lint-no-emphasis-as-heading/package.json index 01051066..f251a605 100644 --- a/packages/remark-lint-no-emphasis-as-heading/package.json +++ b/packages/remark-lint-no-emphasis-as-heading/package.json @@ -33,8 +33,7 @@ "dependencies": { "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-emphasis-as-heading/readme.md b/packages/remark-lint-no-emphasis-as-heading/readme.md index 9d3a2d79..a8b35a5d 100644 --- a/packages/remark-lint-no-emphasis-as-heading/readme.md +++ b/packages/remark-lint-no-emphasis-as-heading/readme.md @@ -147,9 +147,10 @@ It’s recommended to use actual headings instead. ###### In ```markdown -# Foo +# Mercury -Bar. +**Mercury** is the first planet from the Sun and the smallest in the Solar +System. ``` ###### Out @@ -161,20 +162,21 @@ No messages. ###### In ```markdown -*Foo* +**Mercury** -Bar. +**Mercury** is the first planet from the Sun and the smallest in the Solar +System. -__Qux__ +*Venus* -Quux. +**Venus** is the second planet from the Sun. ``` ###### Out ```text -1:1-1:6: Don’t use emphasis to introduce a section, use a heading -5:1-5:8: Don’t use emphasis to introduce a section, use a heading +1:1-1:12: Unexpected strong introducing a section, expected a heading instead +6:1-6:8: Unexpected emphasis introducing a section, expected a heading instead ``` ## Compatibility diff --git a/packages/remark-lint-no-empty-url/index.js b/packages/remark-lint-no-empty-url/index.js index 941d8b59..7dc7c214 100644 --- a/packages/remark-lint-no-empty-url/index.js +++ b/packages/remark-lint-no-empty-url/index.js @@ -38,32 +38,31 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * [alpha](http://bravo.com). - * - * ![charlie](http://delta.com/echo.png "foxtrot"). + * [Mercury](http://example.com/mercury/). * - * [golf][hotel]. + * ![Venus](http://example.com/venus/ "Go to Venus"). * - * [india]: http://juliett.com + * [earth]: http://example.com/earth/ * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * [alpha](). + * [Mercury](). * - * ![bravo](#). + * ![Venus](#). * - * [charlie]: <> + * [earth]: <> * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:1-1:10: Don’t use links without URL - * 3:1-3:12: Don’t use images without URL - * 5:1-5:14: Don’t use definitions without URL + * 1:1-1:12: Unexpected empty link URL referencing the current document, expected URL + * 3:1-3:12: Unexpected empty image URL referencing the current document, expected URL + * 5:1-5:12: Unexpected empty definition URL referencing the current document, expected URL */ /** @@ -71,8 +70,7 @@ */ import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoEmptyUrl = lintRule( { @@ -86,17 +84,20 @@ const remarkLintNoEmptyUrl = lintRule( * Nothing. */ function (tree, file) { - visit(tree, function (node) { - const place = position(node) - + visitParents(tree, function (node, parents) { if ( (node.type === 'definition' || node.type === 'image' || node.type === 'link') && - place && + node.position && (!node.url || node.url === '#' || node.url === '?') ) { - file.message('Don’t use ' + node.type + 's without URL', place) + file.message( + 'Unexpected empty ' + + node.type + + ' URL referencing the current document, expected URL', + {ancestors: [...parents, node], place: node.position} + ) } }) } diff --git a/packages/remark-lint-no-empty-url/package.json b/packages/remark-lint-no-empty-url/package.json index 36a6beae..4d7a8643 100644 --- a/packages/remark-lint-no-empty-url/package.json +++ b/packages/remark-lint-no-empty-url/package.json @@ -35,8 +35,7 @@ "dependencies": { "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-empty-url/readme.md b/packages/remark-lint-no-empty-url/readme.md index 52c96f8f..30c48bc2 100644 --- a/packages/remark-lint-no-empty-url/readme.md +++ b/packages/remark-lint-no-empty-url/readme.md @@ -143,13 +143,11 @@ It’s recommended to fill them out. ###### In ```markdown -[alpha](http://bravo.com). +[Mercury](http://example.com/mercury/). -![charlie](http://delta.com/echo.png "foxtrot"). +![Venus](http://example.com/venus/ "Go to Venus"). -[golf][hotel]. - -[india]: http://juliett.com +[earth]: http://example.com/earth/ ``` ###### Out @@ -161,19 +159,19 @@ No messages. ###### In ```markdown -[alpha](). +[Mercury](). -![bravo](#). +![Venus](#). -[charlie]: <> +[earth]: <> ``` ###### Out ```text -1:1-1:10: Don’t use links without URL -3:1-3:12: Don’t use images without URL -5:1-5:14: Don’t use definitions without URL +1:1-1:12: Unexpected empty link URL referencing the current document, expected URL +3:1-3:12: Unexpected empty image URL referencing the current document, expected URL +5:1-5:12: Unexpected empty definition URL referencing the current document, expected URL ``` ## Compatibility diff --git a/packages/remark-lint-no-file-name-articles/index.js b/packages/remark-lint-no-file-name-articles/index.js index 18302b83..4fa8448a 100644 --- a/packages/remark-lint-no-file-name-articles/index.js +++ b/packages/remark-lint-no-file-name-articles/index.js @@ -30,28 +30,24 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT - * @example - * {"name": "title.md"} * * @example - * {"name": "a-title.md", "label": "output", "positionless": true} - * - * 1:1: Do not start file names with `a` + * {"name": "title.md"} * * @example - * {"name": "the-title.md", "label": "output", "positionless": true} + * {"label": "output", "name": "a-title.md", "positionless": true} * - * 1:1: Do not start file names with `the` + * 1:1: Unexpected file name starting with `a`, remove it * * @example - * {"name": "teh-title.md", "label": "output", "positionless": true} + * {"label": "output", "name": "the-title.md", "positionless": true} * - * 1:1: Do not start file names with `teh` + * 1:1: Unexpected file name starting with `the`, remove it * * @example - * {"name": "an-article.md", "label": "output", "positionless": true} + * {"label": "output", "name": "an-article.md", "positionless": true} * - * 1:1: Do not start file names with `an` + * 1:1: Unexpected file name starting with `an`, remove it */ /** @@ -72,10 +68,12 @@ const remarkLintNoFileNameArticles = lintRule( * Nothing. */ function (_, file) { - const match = file.stem && file.stem.match(/^(the|teh|an?)\b/i) + const match = file.stem && file.stem.match(/^(?:the|teh|an?)\b/i) if (match) { - file.message('Do not start file names with `' + match[0] + '`') + file.message( + 'Unexpected file name starting with `' + match[0] + '`, remove it' + ) } } ) diff --git a/packages/remark-lint-no-file-name-articles/readme.md b/packages/remark-lint-no-file-name-articles/readme.md index cddb546d..88fff6ce 100644 --- a/packages/remark-lint-no-file-name-articles/readme.md +++ b/packages/remark-lint-no-file-name-articles/readme.md @@ -144,7 +144,7 @@ No messages. ###### Out ```text -1:1: Do not start file names with `a` +1:1: Unexpected file name starting with `a`, remove it ``` ##### `the-title.md` @@ -152,15 +152,7 @@ No messages. ###### Out ```text -1:1: Do not start file names with `the` -``` - -##### `teh-title.md` - -###### Out - -```text -1:1: Do not start file names with `teh` +1:1: Unexpected file name starting with `the`, remove it ``` ##### `an-article.md` @@ -168,7 +160,7 @@ No messages. ###### Out ```text -1:1: Do not start file names with `an` +1:1: Unexpected file name starting with `an`, remove it ``` ## Compatibility diff --git a/packages/remark-lint-no-file-name-consecutive-dashes/index.js b/packages/remark-lint-no-file-name-consecutive-dashes/index.js index 39067d5c..c40aebdf 100644 --- a/packages/remark-lint-no-file-name-consecutive-dashes/index.js +++ b/packages/remark-lint-no-file-name-consecutive-dashes/index.js @@ -30,13 +30,14 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "plug-ins.md"} * * @example * {"name": "plug--ins.md", "label": "output", "positionless": true} * - * 1:1: Do not use consecutive dashes in a file name + * 1:1: Unexpected consecutive dashes in a file name, expected `-` */ /** @@ -58,7 +59,7 @@ const remarkLintNoFileNameConsecutiveDashes = lintRule( */ function (_, file) { if (file.stem && /-{2,}/.test(file.stem)) { - file.message('Do not use consecutive dashes in a file name') + file.message('Unexpected consecutive dashes in a file name, expected `-`') } } ) diff --git a/packages/remark-lint-no-file-name-consecutive-dashes/readme.md b/packages/remark-lint-no-file-name-consecutive-dashes/readme.md index 89d3bd1b..1c30ec54 100644 --- a/packages/remark-lint-no-file-name-consecutive-dashes/readme.md +++ b/packages/remark-lint-no-file-name-consecutive-dashes/readme.md @@ -144,7 +144,7 @@ No messages. ###### Out ```text -1:1: Do not use consecutive dashes in a file name +1:1: Unexpected consecutive dashes in a file name, expected `-` ``` ## Compatibility diff --git a/packages/remark-lint-no-file-name-irregular-characters/index.js b/packages/remark-lint-no-file-name-irregular-characters/index.js index dc42679e..11c8d78c 100644 --- a/packages/remark-lint-no-file-name-irregular-characters/index.js +++ b/packages/remark-lint-no-file-name-irregular-characters/index.js @@ -33,26 +33,37 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * + * @example + * {"name": "mercury-and-venus.md"} + * * @example - * {"name": "plug-ins.md"} + * {"name": "mercury.md"} * * @example - * {"name": "plugins.md"} + * {"label": "output", "name": "mercury_and_venus.md", "positionless": true} + * + * 1:1: Unexpected character `_` in file name + * + * @example + * {"config": "\\.a-z0-9", "label": "output", "name": "Readme.md", "positionless": true} + * + * 1:1: Unexpected character `R` in file name * * @example - * {"name": "plug_ins.md", "label": "output", "positionless": true} + * {"config": {"source": "[^\\.a-z0-9]"}, "label": "output", "name": "mercury_and_venus.md", "positionless": true} * - * 1:1: Do not use `_` in a file name + * 1:1: Unexpected character `_` in file name * * @example - * {"name": "README.md", "label": "output", "config": "\\.a-z0-9", "positionless": true} + * {"label": "output", "name": "mercury and venus.md", "positionless": true} * - * 1:1: Do not use `R` in a file name + * 1:1: Unexpected character ` ` in file name * * @example - * {"name": "plug ins.md", "label": "output", "positionless": true} + * {"config": 1, "label": "output", "name": "not-ok-options.md", "positionless": true} * - * 1:1: Do not use ` ` in a file name + * 1:1: Unexpected value `1` for `options`, expected `RegExp` or `string` */ /** @@ -61,7 +72,7 @@ import {lintRule} from 'unified-lint-rule' -const expression = /[^-.\dA-Za-z]/ +const defaultExpression = /[^-.\dA-Za-z]/ const remarkLintNoFileNameIrregularCharacters = lintRule( { @@ -73,22 +84,32 @@ const remarkLintNoFileNameIrregularCharacters = lintRule( * Tree. * @param {RegExp | string | null | undefined} [options] * Configuration (default: `/[^-.\dA-Za-z]/`), - * when string wrapped in `new RegExp('[^' + x + ']')` so make sure to + * when string wrapped in `new RegExp('[^' + x + ']', 'u')` so make sure to * double escape regexp characters * @returns {undefined} * Nothing. */ function (_, file, options) { - let preferred = options || expression + let expected = defaultExpression - if (typeof preferred === 'string') { - preferred = new RegExp('[^' + preferred + ']') + if (options === null || options === undefined) { + // Empty. + } else if (typeof options === 'string') { + expected = new RegExp('[^' + options + ']', 'u') + } else if (typeof options === 'object' && 'source' in options) { + expected = new RegExp(options.source, options.flags ?? 'u') + } else { + file.fail( + 'Unexpected value `' + + options + + '` for `options`, expected `RegExp` or `string`' + ) } - const match = file.stem && file.stem.match(preferred) + const match = file.stem && file.stem.match(expected) if (match) { - file.message('Do not use `' + match[0] + '` in a file name') + file.message('Unexpected character `' + match[0] + '` in file name') } } ) diff --git a/packages/remark-lint-no-file-name-irregular-characters/readme.md b/packages/remark-lint-no-file-name-irregular-characters/readme.md index 00015814..ecc22976 100644 --- a/packages/remark-lint-no-file-name-irregular-characters/readme.md +++ b/packages/remark-lint-no-file-name-irregular-characters/readme.md @@ -136,42 +136,62 @@ Transform ([`Transformer` from `unified`][github-unified-transformer]). ## Examples -##### `plug-ins.md` +##### `mercury-and-venus.md` ###### Out No messages. -##### `plugins.md` +##### `mercury.md` ###### Out No messages. -##### `plug_ins.md` +##### `mercury_and_venus.md` ###### Out ```text -1:1: Do not use `_` in a file name +1:1: Unexpected character `_` in file name ``` -##### `README.md` +##### `Readme.md` When configured with `'\\.a-z0-9'`. ###### Out ```text -1:1: Do not use `R` in a file name +1:1: Unexpected character `R` in file name ``` -##### `plug ins.md` +##### `mercury_and_venus.md` + +When configured with `{ source: '[^\\.a-z0-9]' }`. + +###### Out + +```text +1:1: Unexpected character `_` in file name +``` + +##### `mercury and venus.md` + +###### Out + +```text +1:1: Unexpected character ` ` in file name +``` + +##### `not-ok-options.md` + +When configured with `1`. ###### Out ```text -1:1: Do not use ` ` in a file name +1:1: Unexpected value `1` for `options`, expected `RegExp` or `string` ``` ## Compatibility diff --git a/packages/remark-lint-no-file-name-mixed-case/index.js b/packages/remark-lint-no-file-name-mixed-case/index.js index 024bbab4..58c46493 100644 --- a/packages/remark-lint-no-file-name-mixed-case/index.js +++ b/packages/remark-lint-no-file-name-mixed-case/index.js @@ -31,16 +31,17 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "README.md"} + * {"name": "MERCURY.md"} * * @example - * {"name": "readme.md"} + * {"name": "mercury.md"} * * @example - * {"name": "Readme.md", "label": "output", "positionless": true} + * {"label": "output", "name": "Mercury.md", "positionless": true} * - * 1:1: Do not mix casing in file names + * 1:1: Unexpected mixed case in file name, expected either lowercase or uppercase */ /** @@ -64,7 +65,9 @@ const remarkLintNofileNameMixedCase = lintRule( const name = file.stem if (name && !(name === name.toLowerCase() || name === name.toUpperCase())) { - file.message('Do not mix casing in file names') + file.message( + 'Unexpected mixed case in file name, expected either lowercase or uppercase' + ) } } ) diff --git a/packages/remark-lint-no-file-name-mixed-case/readme.md b/packages/remark-lint-no-file-name-mixed-case/readme.md index 0d6ac870..8c8d85f5 100644 --- a/packages/remark-lint-no-file-name-mixed-case/readme.md +++ b/packages/remark-lint-no-file-name-mixed-case/readme.md @@ -134,24 +134,24 @@ Transform ([`Transformer` from `unified`][github-unified-transformer]). ## Examples -##### `README.md` +##### `MERCURY.md` ###### Out No messages. -##### `readme.md` +##### `mercury.md` ###### Out No messages. -##### `Readme.md` +##### `Mercury.md` ###### Out ```text -1:1: Do not mix casing in file names +1:1: Unexpected mixed case in file name, expected either lowercase or uppercase ``` ## Compatibility diff --git a/packages/remark-lint-no-file-name-outer-dashes/index.js b/packages/remark-lint-no-file-name-outer-dashes/index.js index 392ad06b..121e9a24 100644 --- a/packages/remark-lint-no-file-name-outer-dashes/index.js +++ b/packages/remark-lint-no-file-name-outer-dashes/index.js @@ -30,18 +30,19 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "readme.md"} + * {"name": "mercury-and-venus.md"} * * @example - * {"name": "-readme.md", "label": "output", "positionless": true} + * {"label": "output", "name": "-mercury.md", "positionless": true} * - * 1:1: Do not use initial or final dashes in a file name + * 1:1: Unexpected initial or final dashes in file name, expected dashes to join words * * @example - * {"name": "readme-.md", "label": "output", "positionless": true} + * {"label": "output", "name": "venus-.md", "positionless": true} * - * 1:1: Do not use initial or final dashes in a file name + * 1:1: Unexpected initial or final dashes in file name, expected dashes to join words */ /** @@ -63,7 +64,9 @@ const remarkLintNofileNameOuterDashes = lintRule( */ function (_, file) { if (file.stem && /^-|-$/.test(file.stem)) { - file.message('Do not use initial or final dashes in a file name') + file.message( + 'Unexpected initial or final dashes in file name, expected dashes to join words' + ) } } ) diff --git a/packages/remark-lint-no-file-name-outer-dashes/readme.md b/packages/remark-lint-no-file-name-outer-dashes/readme.md index 33fe34f6..3f360b1d 100644 --- a/packages/remark-lint-no-file-name-outer-dashes/readme.md +++ b/packages/remark-lint-no-file-name-outer-dashes/readme.md @@ -133,26 +133,26 @@ Transform ([`Transformer` from `unified`][github-unified-transformer]). ## Examples -##### `readme.md` +##### `mercury-and-venus.md` ###### Out No messages. -##### `-readme.md` +##### `-mercury.md` ###### Out ```text -1:1: Do not use initial or final dashes in a file name +1:1: Unexpected initial or final dashes in file name, expected dashes to join words ``` -##### `readme-.md` +##### `venus-.md` ###### Out ```text -1:1: Do not use initial or final dashes in a file name +1:1: Unexpected initial or final dashes in file name, expected dashes to join words ``` ## Compatibility diff --git a/packages/remark-lint-no-heading-content-indent/index.js b/packages/remark-lint-no-heading-content-indent/index.js index 6404f02b..6bbdc9ba 100644 --- a/packages/remark-lint-no-heading-content-indent/index.js +++ b/packages/remark-lint-no-heading-content-indent/index.js @@ -41,51 +41,57 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * #␠Foo + * #␠Mercury * - * ## Bar␠## + * ##␠Venus␠## * - * ##␠Baz + * ␠␠##␠Earth * - * Setext headings are not affected. + * Setext headings are not affected: * - * Baz - * === + * ␠Mars + * ===== * - * @example - * {"name": "not-ok.md", "label": "input"} + * ␠Jupiter + * -------- * - * #␠␠Foo + * @example + * {"label": "input", "name": "not-ok.md"} * - * ## Bar␠␠## + * #␠␠Mercury * - * ##␠␠Baz + * ##␠Venus␠␠## * + * ␠␠##␠␠␠Earth * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:4: Remove 1 space before this heading’s content - * 3:7: Remove 1 space after this heading’s content - * 5:7: Remove 1 space before this heading’s content + * 1:4: Unexpected `2` spaces between hashes and content, expected `1` space, remove `1` space + * 3:11: Unexpected `2` spaces between content and hashes, expected `1` space, remove `1` space + * 5:8: Unexpected `3` spaces between hashes and content, expected `1` space, remove `2` spaces * * @example - * {"name": "empty-heading.md"} + * {"label": "input", "name": "empty-heading.md"} * * #␠␠ + * @example + * {"label": "output", "name": "empty-heading.md"} + * + * 1:4: Unexpected `2` spaces between hashes and content, expected `1` space, remove `1` space */ /** * @typedef {import('mdast').Root} Root */ -import {headingStyle} from 'mdast-util-heading-style' -import plural from 'pluralize' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoHeadingContentIndent = lintRule( { @@ -99,55 +105,116 @@ const remarkLintNoHeadingContentIndent = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'heading', function (node) { + const value = String(file) + + visitParents(tree, 'heading', function (node, parents) { const start = pointStart(node) - const type = headingStyle(node, 'atx') - - if (!start) return - - if (type === 'atx' || type === 'atx-closed') { - const headStart = pointStart(node.children[0]) - - // Ignore empty headings. - if (!headStart) { - return - } - - const diff = headStart.column - start.column - 1 - node.depth - - if (diff) { - file.message( - 'Remove ' + - Math.abs(diff) + - ' ' + - plural('space', Math.abs(diff)) + - ' before this heading’s content', - pointStart(node.children[0]) - ) - } + const end = pointEnd(node) + + if ( + !end || + !start || + typeof end.offset !== 'number' || + typeof start.offset !== 'number' + ) { + return } - // Closed ATX headings always must have a space between their content and - // the final hashes, thus, there is no `add x spaces`. - if (type === 'atx-closed') { - const final = pointEnd(node.children[node.children.length - 1]) - const end = pointEnd(node) - - /* c8 ignore next -- we get here if we have offsets. */ - if (!final || !end) return - - const diff = end.column - final.column - 1 - node.depth - - if (diff) { - file.message( - 'Remove ' + - diff + - ' ' + - plural('space', diff) + - ' after this heading’s content', - final - ) - } + let index = start.offset + let code = value.charCodeAt(index) + // Node positional info starts after whitespace, + // so we don’t need to walk past it. + let found = false + + while (value.charCodeAt(index) === 35 /* `#` */) { + index++ + found = true + continue + } + + const from = index + + code = value.charCodeAt(index) + + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + code = value.charCodeAt(++index) + continue + } + + const size = index - from + + // Not ATX / fine. + if (found && size > 1) { + file.message( + 'Unexpected `' + + size + + '` ' + + pluralize('space', size) + + ' between hashes and content, expected `1` space, remove `' + + (size - 1) + + '` ' + + pluralize('space', size - 1), + { + ancestors: [...parents, node], + place: { + line: start.line, + column: start.column + (index - start.offset), + offset: start.offset + (index - start.offset) + } + } + ) + } + + const contentStart = index + + index = end.offset + code = value.charCodeAt(index - 1) + + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + index-- + code = value.charCodeAt(index - 1) + continue + } + + let endFound = false + + while (value.charCodeAt(index - 1) === 35 /* `#` */) { + index-- + endFound = true + continue + } + + const endFrom = index + + code = value.charCodeAt(index - 1) + + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + index-- + code = value.charCodeAt(index - 1) + continue + } + + const endSize = endFrom - index + + if (endFound && index > contentStart && endSize > 1) { + file.message( + 'Unexpected `' + + endSize + + '` ' + + pluralize('space', endSize) + + ' between content and hashes, expected `1` space, remove `' + + (endSize - 1) + + '` ' + + pluralize('space', endSize - 1), + { + ancestors: [...parents, node], + place: { + line: end.line, + column: end.column - (end.offset - endFrom), + offset: end.offset - (end.offset - endFrom) + } + } + ) } }) } diff --git a/packages/remark-lint-no-heading-content-indent/package.json b/packages/remark-lint-no-heading-content-indent/package.json index 792fdcf9..9b594f4c 100644 --- a/packages/remark-lint-no-heading-content-indent/package.json +++ b/packages/remark-lint-no-heading-content-indent/package.json @@ -33,11 +33,10 @@ ], "dependencies": { "@types/mdast": "^4.0.0", - "mdast-util-heading-style": "^3.0.0", "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -50,7 +49,8 @@ "prettier": true, "rules": { "capitalized-comments": "off", - "unicorn/prefer-at": "off" + "unicorn/prefer-at": "off", + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-no-heading-content-indent/readme.md b/packages/remark-lint-no-heading-content-indent/readme.md index 807a0eeb..34f153a2 100644 --- a/packages/remark-lint-no-heading-content-indent/readme.md +++ b/packages/remark-lint-no-heading-content-indent/readme.md @@ -150,16 +150,19 @@ Due to this, it’s recommended to turn this rule on. ###### In ```markdown -#␠Foo +#␠Mercury -## Bar␠## +##␠Venus␠## - ##␠Baz +␠␠##␠Earth -Setext headings are not affected. +Setext headings are not affected: -Baz -=== +␠Mars +===== + +␠Jupiter +-------- ``` ###### Out @@ -171,19 +174,19 @@ No messages. ###### In ```markdown -#␠␠Foo +#␠␠Mercury -## Bar␠␠## +##␠Venus␠␠## - ##␠␠Baz +␠␠##␠␠␠Earth ``` ###### Out ```text -1:4: Remove 1 space before this heading’s content -3:7: Remove 1 space after this heading’s content -5:7: Remove 1 space before this heading’s content +1:4: Unexpected `2` spaces between hashes and content, expected `1` space, remove `1` space +3:11: Unexpected `2` spaces between content and hashes, expected `1` space, remove `1` space +5:8: Unexpected `3` spaces between hashes and content, expected `1` space, remove `2` spaces ``` ##### `empty-heading.md` @@ -196,7 +199,9 @@ No messages. ###### Out -No messages. +```text +1:4: Unexpected `2` spaces between hashes and content, expected `1` space, remove `1` space +``` ## Compatibility diff --git a/packages/remark-lint-no-heading-indent/index.js b/packages/remark-lint-no-heading-indent/index.js index 70e88f12..578cd67b 100644 --- a/packages/remark-lint-no-heading-indent/index.js +++ b/packages/remark-lint-no-heading-indent/index.js @@ -3,11 +3,11 @@ * * ## What is this? * - * This package checks the indent of headings. + * This package checks the spaces before headings. * * ## When should I use this? * - * You can use this package to check that headings are consistent. + * You can use this rule to check markdown code style. * * ## API * @@ -30,15 +30,17 @@ * While it is possible to use an indent to headings on their text: * * ```markdown - * # One - * ## Two - * ### Three - * #### Four + * # Mercury + * ## Venus + * ### Earth + * #### Mars * ``` * - * …such style is uncommon, a bit hard to maintain, and it’s impossible to add a - * heading with a rank of 5 as it would form indented code instead. - * Hence, it’s recommended to not indent headings and to turn this rule on. + * …such style is uncommon, + * a bit hard to maintain, + * and it’s impossible to add a heading with a rank of 5 as it would form + * indented code instead. + * So it’s recommended to not indent headings and to turn this rule on. * * ## Fix * @@ -52,49 +54,49 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * #␠Hello world + * #␠Mercury * - * Foo + * Venus * ----- * - * #␠Hello world␠# + * #␠Earth␠# * - * Bar - * ===== + * Mars + * ==== * * @example - * {"name": "not-ok.md", "label": "input"} - * - * ␠␠␠# Hello world + * {"label": "input", "name": "not-ok.md"} * - * ␠Foo - * ----- + * ␠␠␠# Mercury * - * ␠# Hello world # + * ␠Venus + * ------ * - * ␠␠␠Bar - * ===== + * ␠# Earth # * + * ␠␠␠Mars + * ====== * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:4: Remove 3 spaces before this heading - * 3:2: Remove 1 space before this heading - * 6:2: Remove 1 space before this heading - * 8:4: Remove 3 spaces before this heading + * 1:4: Unexpected `3` spaces before heading, expected `0` spaces, remove `3` spaces + * 3:2: Unexpected `1` space before heading, expected `0` spaces, remove `1` space + * 6:2: Unexpected `1` space before heading, expected `0` spaces, remove `1` space + * 8:4: Unexpected `3` spaces before heading, expected `0` spaces, remove `3` spaces */ /** * @typedef {import('mdast').Root} Root */ -import plural from 'pluralize' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoHeadingIndent = lintRule( { @@ -108,25 +110,30 @@ const remarkLintNoHeadingIndent = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'heading', function (node, _, parent) { + visitParents(tree, 'heading', function (node, parents) { + const parent = parents[parents.length - 1] const start = pointStart(node) // Note: it’s rather complex to detect what the expected indent is in block // quotes and lists, so let’s only do directly in root for now. - if (!start || (parent && parent.type !== 'root')) { + if (!start || !parent || parent.type !== 'root') { return } - const diff = start.column - 1 + const actual = start.column - 1 - if (diff) { + if (actual) { file.message( - 'Remove ' + - diff + - ' ' + - plural('space', diff) + - ' before this heading', - start + 'Unexpected `' + + actual + + '` ' + + pluralize('space', actual) + + ' before heading, expected `0` spaces, remove' + + ' `' + + actual + + '` ' + + pluralize('space', actual), + {ancestors: [...parents, node], place: start} ) } }) diff --git a/packages/remark-lint-no-heading-indent/package.json b/packages/remark-lint-no-heading-indent/package.json index d2b74846..8db3af9d 100644 --- a/packages/remark-lint-no-heading-indent/package.json +++ b/packages/remark-lint-no-heading-indent/package.json @@ -35,7 +35,7 @@ "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -47,7 +47,8 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "unicorn/prefer-at": "off" } } } diff --git a/packages/remark-lint-no-heading-indent/readme.md b/packages/remark-lint-no-heading-indent/readme.md index e40178b2..c8c1e994 100644 --- a/packages/remark-lint-no-heading-indent/readme.md +++ b/packages/remark-lint-no-heading-indent/readme.md @@ -30,11 +30,11 @@ ## What is this? -This package checks the indent of headings. +This package checks the spaces before headings. ## When should I use this? -You can use this package to check that headings are consistent. +You can use this rule to check markdown code style. ## Presets @@ -136,15 +136,17 @@ markdown. While it is possible to use an indent to headings on their text: ```markdown - # One - ## Two - ### Three -#### Four + # Mercury + ## Venus + ### Earth +#### Mars ``` -…such style is uncommon, a bit hard to maintain, and it’s impossible to add a -heading with a rank of 5 as it would form indented code instead. -Hence, it’s recommended to not indent headings and to turn this rule on. +…such style is uncommon, +a bit hard to maintain, +and it’s impossible to add a heading with a rank of 5 as it would form +indented code instead. +So it’s recommended to not indent headings and to turn this rule on. ## Fix @@ -157,15 +159,15 @@ Hence, it’s recommended to not indent headings and to turn this rule on. ###### In ```markdown -#␠Hello world +#␠Mercury -Foo +Venus ----- -#␠Hello world␠# +#␠Earth␠# -Bar -===== +Mars +==== ``` ###### Out @@ -177,24 +179,24 @@ No messages. ###### In ```markdown -␠␠␠# Hello world +␠␠␠# Mercury -␠Foo ------ +␠Venus +------ -␠# Hello world # +␠# Earth # -␠␠␠Bar -===== +␠␠␠Mars +====== ``` ###### Out ```text -1:4: Remove 3 spaces before this heading -3:2: Remove 1 space before this heading -6:2: Remove 1 space before this heading -8:4: Remove 3 spaces before this heading +1:4: Unexpected `3` spaces before heading, expected `0` spaces, remove `3` spaces +3:2: Unexpected `1` space before heading, expected `0` spaces, remove `1` space +6:2: Unexpected `1` space before heading, expected `0` spaces, remove `1` space +8:4: Unexpected `3` spaces before heading, expected `0` spaces, remove `3` spaces ``` ## Compatibility diff --git a/packages/remark-lint-no-heading-like-paragraph/index.js b/packages/remark-lint-no-heading-like-paragraph/index.js index d4b36409..8f8232a3 100644 --- a/packages/remark-lint-no-heading-like-paragraph/index.js +++ b/packages/remark-lint-no-heading-like-paragraph/index.js @@ -7,8 +7,7 @@ * * ## When should I use this? * - * You can use this package to ensure that no broken headings are user, which - * instead will result in paragraphs with the `#` characters shown. + * You can use this rule to check markdown code style. * * ## API * @@ -31,35 +30,41 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * ###### Alpha + * ###### Venus * - * Bravo. + * Mercury. * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} + * + * ####### Venus * - * ####### Charlie + * Mercury. * - * Delta. + * ######## Earth * + * Mars. * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:1-1:16: This looks like a heading but has too many hashes + * 1:8: Unexpected `7` hashes starting paragraph looking like a heading, expected up to `6` hashes, remove `1` hash + * 5:9: Unexpected `8` hashes starting paragraph looking like a heading, expected up to `6` hashes, remove `2` hashes */ /** * @typedef {import('mdast').Root} Root */ +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {pointStart} from 'unist-util-position' +import {visitParents} from 'unist-util-visit-parents' -const fence = '#######' +const max = 6 const remarkLintNoHeadingLikeParagraph = lintRule( { @@ -73,20 +78,37 @@ const remarkLintNoHeadingLikeParagraph = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'paragraph', function (node) { - const place = position(node) + visitParents(tree, 'paragraph', function (node, parents) { + const head = node.children[0] + + if (head && head.type === 'text') { + const start = pointStart(node) + let size = 0 + + while (head.value.charCodeAt(size) === 35 /* `#` */) { + size++ + } - if (place) { - const head = node.children[0] + if (start && typeof start.offset === 'number' && size > max) { + const extra = size - max - if ( - head && - head.type === 'text' && - head.value.slice(0, fence.length) === fence - ) { file.message( - 'This looks like a heading but has too many hashes', - place + 'Unexpected `' + + size + + '` hashes starting paragraph looking like a heading, expected up to `' + + max + + '` hashes, remove `' + + extra + + '` ' + + pluralize('hash', extra), + { + ancestors: [...parents, node, head], + place: { + line: start.line, + column: start.column + size, + offset: start.offset + size + } + } ) } } diff --git a/packages/remark-lint-no-heading-like-paragraph/package.json b/packages/remark-lint-no-heading-like-paragraph/package.json index c3345b6a..164f414b 100644 --- a/packages/remark-lint-no-heading-like-paragraph/package.json +++ b/packages/remark-lint-no-heading-like-paragraph/package.json @@ -32,9 +32,10 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -46,7 +47,8 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-no-heading-like-paragraph/readme.md b/packages/remark-lint-no-heading-like-paragraph/readme.md index 00f9b513..c8b717fc 100644 --- a/packages/remark-lint-no-heading-like-paragraph/readme.md +++ b/packages/remark-lint-no-heading-like-paragraph/readme.md @@ -32,8 +32,7 @@ This package checks for broken headings. ## When should I use this? -You can use this package to ensure that no broken headings are user, which -instead will result in paragraphs with the `#` characters shown. +You can use this rule to check markdown code style. ## Presets @@ -135,9 +134,9 @@ Transform ([`Transformer` from `unified`][github-unified-transformer]). ###### In ```markdown -###### Alpha +###### Venus -Bravo. +Mercury. ``` ###### Out @@ -149,15 +148,20 @@ No messages. ###### In ```markdown -####### Charlie +####### Venus -Delta. +Mercury. + +######## Earth + +Mars. ``` ###### Out ```text -1:1-1:16: This looks like a heading but has too many hashes +1:8: Unexpected `7` hashes starting paragraph looking like a heading, expected up to `6` hashes, remove `1` hash +5:9: Unexpected `8` hashes starting paragraph looking like a heading, expected up to `6` hashes, remove `2` hashes ``` ## Compatibility diff --git a/packages/remark-lint-no-heading-punctuation/index.js b/packages/remark-lint-no-heading-punctuation/index.js index c744b427..3fdb5228 100644 --- a/packages/remark-lint-no-heading-punctuation/index.js +++ b/packages/remark-lint-no-heading-punctuation/index.js @@ -1,9 +1,9 @@ /** - * remark-lint rule to warn when headings end in punctuation. + * remark-lint rule to warn when headings end in irregular characters. * * ## What is this? * - * This package checks the style of hedings. + * This package checks heading text. * * ## When should I use this? * @@ -13,14 +13,14 @@ * * ### `unified().use(remarkLintNoHeadingPunctuation[, options])` * - * Warn when headings end in punctuation. + * Warn when headings end in irregular characters. * * ###### Parameters * - * * `options` (`string`, default: `'!,.:;?'`) + * * `options` (`RegExp` or `string`, default: `/[!,.:;?]/u`) * — configuration, - * wrapped in `new RegExp('[' + x + ']', 'u')` so make sure to escape regexp - * characters + * when string wrapped in `new RegExp('[' + x + ']', 'u')` so make sure to + * escape regexp characters * * ###### Returns * @@ -33,49 +33,60 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * # Hello + * # Mercury * * @example - * {"name": "ok.md", "config": ",;:!?"} + * {"label": "input", "name": "not-ok.md"} * - * # Hello… + * # Mercury: * - * @example - * {"name": "not-ok.md", "label": "input"} + * # Venus? + * + * # Earth! * - * # Hello: + * # Mars, + * + * # Jupiter; + * @example + * {"label": "output", "name": "not-ok.md"} * - * # Hello? + * 1:1-1:11: Unexpected character `:` at end of heading, remove it + * 3:1-3:9: Unexpected character `?` at end of heading, remove it + * 5:1-5:9: Unexpected character `!` at end of heading, remove it + * 7:1-7:8: Unexpected character `,` at end of heading, remove it + * 9:1-9:11: Unexpected character `;` at end of heading, remove it * - * # Hello! + * @example + * {"config": ",;:!?", "name": "ok.md"} * - * # Hello, + * # Mercury… * - * # Hello; + * @example + * {"config": {"source": "[^A-Za-z0-9]"}, "label": "input", "name": "regex.md"} * + * # Mercury! * @example - * {"name": "not-ok.md", "label": "output"} + * {"config": {"source": "[^A-Za-z0-9]"}, "label": "output", "name": "regex.md"} * - * 1:1-1:9: Don’t add a trailing `:` to headings - * 3:1-3:9: Don’t add a trailing `?` to headings - * 5:1-5:9: Don’t add a trailing `!` to headings - * 7:1-7:9: Don’t add a trailing `,` to headings - * 9:1-9:9: Don’t add a trailing `;` to headings + * 1:1-1:11: Unexpected character `!` at end of heading, remove it * * @example - * {"label": "input", "mdx": true, "name": "mdx.mdx"} + * {"label": "input", "mdx": true, "name": "example.mdx"} * - * MDX is supported too. + *

Mercury?

+ * @example + * {"label": "output", "mdx": true, "name": "example.mdx"} * - *

Hi?

+ * 1:1-1:18: Unexpected character `?` at end of heading, remove it * * @example - * {"label": "output", "mdx": true, "name": "mdx.mdx"} + * {"config": 1, "label": "output", "name": "not-ok-options.md", "positionless": true} * - * 3:1-3:13: Don’t add a trailing `?` to headings + * 1:1: Unexpected value `1` for `options`, expected `RegExp` or `string` */ /** @@ -86,10 +97,10 @@ import {toString} from 'mdast-util-to-string' import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const jsxNameRe = /^h([1-6])$/ +const defaultExpression = /[!,.:;?]/u const remarkLintNoHeadingPunctuation = lintRule( { @@ -99,35 +110,47 @@ const remarkLintNoHeadingPunctuation = lintRule( /** * @param {Root} tree * Tree. - * @param {string | null | undefined} [options] - * Configuration (default: `'!,.:;?'`), + * @param {RegExp | string | null | undefined} [options] + * Configuration (default: `/[!,.:;?]/u`), * wrapped in `new RegExp('[' + x + ']', 'u')` so make sure to double escape * regexp characters. * @returns {undefined} * Nothing. */ function (tree, file, options) { - const expression = new RegExp('[' + (options || '!,.:;?') + ']', 'u') + let expected = defaultExpression - visit(tree, function (node) { + if (options === null || options === undefined) { + // Empty. + } else if (typeof options === 'string') { + expected = new RegExp('[' + options + ']', 'u') + } else if (typeof options === 'object' && 'source' in options) { + expected = new RegExp(options.source, options.flags ?? 'u') + } else { + file.fail( + 'Unexpected value `' + + options + + '` for `options`, expected `RegExp` or `string`' + ) + } + + visitParents(tree, function (node, parents) { if ( - node.type === 'heading' || - ((node.type === 'mdxJsxFlowElement' || - node.type === 'mdxJsxTextElement') && - node.name && - jsxNameRe.test(node.name)) + node.position && // Plain markdown. + (node.type === 'heading' || + // MDX JSX. + ((node.type === 'mdxJsxFlowElement' || + node.type === 'mdxJsxTextElement') && + node.name && + jsxNameRe.test(node.name))) ) { - const place = position(node) - - if (place) { - const tail = Array.from(toString(node)).at(-1) + const tail = Array.from(toString(node)).at(-1) - if (tail && expression.test(tail)) { - file.message( - 'Don’t add a trailing `' + tail + '` to headings', - place - ) - } + if (tail && expected.test(tail)) { + file.message( + 'Unexpected character `' + tail + '` at end of heading, remove it', + {ancestors: [...parents, node], place: node.position} + ) } } }) diff --git a/packages/remark-lint-no-heading-punctuation/package.json b/packages/remark-lint-no-heading-punctuation/package.json index ba2a84b4..8fd9011f 100644 --- a/packages/remark-lint-no-heading-punctuation/package.json +++ b/packages/remark-lint-no-heading-punctuation/package.json @@ -35,8 +35,7 @@ "mdast-util-mdx": "^3.0.0", "mdast-util-to-string": "^4.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^66.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-heading-punctuation/readme.md b/packages/remark-lint-no-heading-punctuation/readme.md index f291285c..328a1d8c 100644 --- a/packages/remark-lint-no-heading-punctuation/readme.md +++ b/packages/remark-lint-no-heading-punctuation/readme.md @@ -10,7 +10,7 @@ [![Backers][badge-funding-backers-image]][badge-funding-url] [![Chat][badge-chat-image]][badge-chat-url] -[`remark-lint`][github-remark-lint] rule to warn when headings end in punctuation. +[`remark-lint`][github-remark-lint] rule to warn when headings end in irregular characters. ## Contents @@ -28,7 +28,7 @@ ## What is this? -This package checks the style of hedings. +This package checks heading text. ## When should I use this? @@ -121,14 +121,14 @@ The default export is ### `unified().use(remarkLintNoHeadingPunctuation[, options])` -Warn when headings end in punctuation. +Warn when headings end in irregular characters. ###### Parameters -* `options` (`string`, default: `'!,.:;?'`) +* `options` (`RegExp` or `string`, default: `/[!,.:;?]/u`) — configuration, - wrapped in `new RegExp('[' + x + ']', 'u')` so make sure to escape regexp - characters + when string wrapped in `new RegExp('[' + x + ']', 'u')` so make sure to + escape regexp characters ###### Returns @@ -141,54 +141,70 @@ Transform ([`Transformer` from `unified`][github-unified-transformer]). ###### In ```markdown -# Hello +# Mercury ``` ###### Out No messages. -##### `ok.md` - -When configured with `',;:!?'`. +##### `not-ok.md` ###### In ```markdown -# Hello… +# Mercury: + +# Venus? + +# Earth! + +# Mars, + +# Jupiter; ``` ###### Out -No messages. +```text +1:1-1:11: Unexpected character `:` at end of heading, remove it +3:1-3:9: Unexpected character `?` at end of heading, remove it +5:1-5:9: Unexpected character `!` at end of heading, remove it +7:1-7:8: Unexpected character `,` at end of heading, remove it +9:1-9:11: Unexpected character `;` at end of heading, remove it +``` -##### `not-ok.md` +##### `ok.md` + +When configured with `',;:!?'`. ###### In ```markdown -# Hello: +# Mercury… +``` -# Hello? +###### Out -# Hello! +No messages. -# Hello, +##### `regex.md` + +When configured with `{ source: '[^A-Za-z0-9]' }`. + +###### In -# Hello; +```markdown +# Mercury! ``` ###### Out ```text -1:1-1:9: Don’t add a trailing `:` to headings -3:1-3:9: Don’t add a trailing `?` to headings -5:1-5:9: Don’t add a trailing `!` to headings -7:1-7:9: Don’t add a trailing `,` to headings -9:1-9:9: Don’t add a trailing `;` to headings +1:1-1:11: Unexpected character `!` at end of heading, remove it ``` -##### `mdx.mdx` +##### `example.mdx` ###### In @@ -196,15 +212,23 @@ No messages. > MDX ([`remark-mdx`][github-remark-mdx]). ```mdx -MDX is supported too. +

Mercury?

+``` + +###### Out -

Hi?

+```text +1:1-1:18: Unexpected character `?` at end of heading, remove it ``` +##### `not-ok-options.md` + +When configured with `1`. + ###### Out ```text -3:1-3:13: Don’t add a trailing `?` to headings +1:1: Unexpected value `1` for `options`, expected `RegExp` or `string` ``` ## Compatibility diff --git a/packages/remark-lint-no-html/index.js b/packages/remark-lint-no-html/index.js index 1e49386e..ddb650d6 100644 --- a/packages/remark-lint-no-html/index.js +++ b/packages/remark-lint-no-html/index.js @@ -11,19 +11,30 @@ * * ## API * - * ### `unified().use(remarkLintNoHtml)` + * ### `unified().use(remarkLintNoHtml[, options])` * * Warn when HTML is used. * * ###### Parameters * - * There are no options. + * * `options` ([`Options`][api-options], optional) + * — configuration * * ###### Returns * * Transform ([`Transformer` from `unified`][github-unified-transformer]). * - * [api-remark-lint-no-html]: #unifieduseremarklintnohtml + * ### `Options` + * + * Configuration (TypeScript type). + * + * ###### Fields + * + * * `allowComments` (`boolean`, default: `true`) + * — allow comments or not + * + * [api-options]: #options + * [api-remark-lint-no-html]: #unifieduseremarklintnohtml-options * [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer * * @module no-html @@ -34,28 +45,42 @@ * @example * {"name": "ok.md"} * - * # Hello + * # Mercury * - * + * * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - *

Hello

+ *

Mercury

+ * @example + * {"label": "output", "name": "not-ok.md"} + * + * 1:1-1:17: Unexpected HTML, use markdown instead + * + * @example + * {"config": {"allowComments": false}, "label": "input", "name": "not-ok.md"} * + * * @example - * {"name": "not-ok.md", "label": "output"} + * {"config": {"allowComments": false}, "label": "output", "name": "not-ok.md"} * - * 1:1-1:15: Do not use HTML in markdown + * 1:1-1:15: Unexpected HTML, use markdown instead */ /** * @typedef {import('mdast').Root} Root */ +/** + * @typedef Options + * Configuration. + * @property {boolean | null | undefined} [allowComments=true] + * Allow comments or not (default: `true`). + */ + import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoHtml = lintRule( { @@ -65,15 +90,27 @@ const remarkLintNoHtml = lintRule( /** * @param {Root} tree * Tree. + * @param {Readonly | null | undefined} [options] + * Configuration (optional). * @returns {undefined} * Nothing. */ - function (tree, file) { - visit(tree, 'html', function (node) { - const place = position(node) - if (place && !/^\s* + ``` ###### Out @@ -148,13 +160,29 @@ No messages. ###### In ```markdown -

Hello

+

Mercury

+``` + +###### Out + +```text +1:1-1:17: Unexpected HTML, use markdown instead +``` + +##### `not-ok.md` + +When configured with `{ allowComments: false }`. + +###### In + +```markdown + ``` ###### Out ```text -1:1-1:15: Do not use HTML in markdown +1:1-1:15: Unexpected HTML, use markdown instead ``` ## Compatibility @@ -182,7 +210,9 @@ abide by its terms. [MIT][file-license] © [Titus Wormer][author] -[api-remark-lint-no-html]: #unifieduseremarklintnohtml +[api-options]: #options + +[api-remark-lint-no-html]: #unifieduseremarklintnohtml-options [author]: https://wooorm.com diff --git a/packages/remark-lint-no-literal-urls/index.js b/packages/remark-lint-no-literal-urls/index.js index aefd2b55..fd71c75a 100644 --- a/packages/remark-lint-no-literal-urls/index.js +++ b/packages/remark-lint-no-literal-urls/index.js @@ -4,10 +4,12 @@ * ## What is this? * * This package checks that regular autolinks or full links are used. + * Literal autolinks is a GFM feature enabled with + * [`remark-gfm`][github-remark-gfm]. * * ## When should I use this? * - * You can use this package to check links. + * You can use this package to check that links are consistent. * * ## API * @@ -38,6 +40,7 @@ * It always generates regular autolinks or full links. * * [api-remark-lint-no-literal-urls]: #unifieduseremarklintnoliteralurls + * [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 * @@ -45,20 +48,29 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md"} + * {"name": "ok.md", "gfm": true} + * + * * - * + * ![Venus](http://example.com/venus/). * * @example * {"name": "not-ok.md", "label": "input", "gfm": true} * - * http://foo.bar/baz + * https://example.com/mercury/ + * + * www.example.com/venus/ + * + * earth@mars.planets * * @example * {"name": "not-ok.md", "label": "output", "gfm": true} * - * 1:1-1:19: Don’t use literal URLs without angle brackets + * 1:1-1:29: Unexpected GFM autolink literal, expected regular autolink, add `<` before and `>` after + * 3:1-3:23: Unexpected GFM autolink literal, expected regular autolink, add `` after + * 5:1-5:19: Unexpected GFM autolink literal, expected regular autolink, add `` after */ /** @@ -66,9 +78,13 @@ */ import {toString} from 'mdast-util-to-string' +import {asciiPunctuation} from 'micromark-util-character' import {lintRule} from 'unified-lint-rule' -import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {pointStart} from 'unist-util-position' +import {visitParents} from 'unist-util-visit-parents' + +const defaultHttp = 'http://' +const defaultMailto = 'mailto:' const remarkLintNoLiteralUrls = lintRule( { @@ -82,23 +98,39 @@ const remarkLintNoLiteralUrls = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'link', function (node) { - const value = toString(node) - const end = pointEnd(node) + const value = String(file) + + visitParents(tree, 'link', function (node, parents) { const start = pointStart(node) - const headStart = pointStart(node.children[0]) - const tailEnd = pointEnd(node.children[node.children.length - 1]) + + if (!start || typeof start.offset !== 'number') return + + const raw = toString(node) + + /** @type {string | undefined} */ + let protocol + let otherwiseFine = false + + if (raw === node.url) { + otherwiseFine = true + } else if (defaultHttp + raw === node.url) { + protocol = defaultHttp + } else if (defaultMailto + raw === node.url) { + protocol = defaultMailto + } if ( - end && - start && - headStart && - tailEnd && - end.column === tailEnd.column && - start.column === headStart.column && - (node.url === 'mailto:' + value || node.url === value) + // If the url is the same as the content… + (protocol || otherwiseFine) && + // …and it doesn’t start with a marker. + !asciiPunctuation(value.charCodeAt(start.offset)) ) { - file.message('Don’t use literal URLs without angle brackets', node) + file.message( + 'Unexpected GFM autolink literal, expected regular autolink, add ' + + (protocol ? '`<' + protocol + '`' : '`<`') + + ' before and `>` after', + {ancestors: [...parents, node], place: node.position} + ) } }) } diff --git a/packages/remark-lint-no-literal-urls/package.json b/packages/remark-lint-no-literal-urls/package.json index 821efb52..3adddc91 100644 --- a/packages/remark-lint-no-literal-urls/package.json +++ b/packages/remark-lint-no-literal-urls/package.json @@ -33,9 +33,10 @@ "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-string": "^4.0.0", + "micromark-util-character": "^2.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -48,7 +49,9 @@ "prettier": true, "rules": { "capitalized-comments": "off", - "unicorn/prefer-at": "off" + "unicorn/prefer-at": "off", + "unicorn/prefer-code-point": "off", + "unicorn/prefer-switch": "off" } } } diff --git a/packages/remark-lint-no-literal-urls/readme.md b/packages/remark-lint-no-literal-urls/readme.md index c42aaa9e..e334621b 100644 --- a/packages/remark-lint-no-literal-urls/readme.md +++ b/packages/remark-lint-no-literal-urls/readme.md @@ -31,10 +31,12 @@ ## What is this? This package checks that regular autolinks or full links are used. +Literal autolinks is a GFM feature enabled with +[`remark-gfm`][github-remark-gfm]. ## When should I use this? -You can use this package to check links. +You can use this package to check that links are consistent. ## Presets @@ -154,8 +156,13 @@ It always generates regular autolinks or full links. ###### In +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + ```markdown - + + +![Venus](http://example.com/venus/). ``` ###### Out @@ -170,13 +177,19 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -http://foo.bar/baz +https://example.com/mercury/ + +www.example.com/venus/ + +earth@mars.planets ``` ###### Out ```text -1:1-1:19: Don’t use literal URLs without angle brackets +1:1-1:29: Unexpected GFM autolink literal, expected regular autolink, add `<` before and `>` after +3:1-3:23: Unexpected GFM autolink literal, expected regular autolink, add `` after +5:1-5:19: Unexpected GFM autolink literal, expected regular autolink, add `` after ``` ## Compatibility diff --git a/packages/remark-lint-no-missing-blank-lines/index.js b/packages/remark-lint-no-missing-blank-lines/index.js index 8e95f5db..82a4aa9a 100644 --- a/packages/remark-lint-no-missing-blank-lines/index.js +++ b/packages/remark-lint-no-missing-blank-lines/index.js @@ -1,5 +1,5 @@ /** - * remark-lint rule to warn when there are no blank lines between blocks. + * remark-lint rule to warn when blank lines are missing. * * ## What is this? * @@ -13,7 +13,7 @@ * * ### `unified().use(remarkLintNoMissingBlankLines[, options])` * - * Warn when there are no blank lines between blocks. + * Warn when blank lines are missing. * * ###### Parameters * @@ -53,134 +53,122 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * # Foo + * # Mercury * - * ## Bar + * ## Venus * - * - Paragraph + * * Earth. * - * + List. + * * Mars. * - * Paragraph. + * > # Jupiter + * > + * > Saturn. * * @example - * {"name": "not-ok.md", "label": "input"} - * - * # Foo - * ## Bar + * {"label": "input", "name": "not-ok.md"} * - * - Paragraph - * + List. + * # Mercury + * ## Venus * - * Paragraph. + * * Earth + * * Mars. * + * > # Jupiter + * > Saturn. * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 2:1-2:7: Missing blank line before block node - * 5:3-5:10: Missing blank line before block node + * 2:1-2:9: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line + * 5:3-5:10: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line + * 8:3-8:10: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line * * @example - * {"name": "tight.md", "config": {"exceptTightLists": true}, "label": "input"} + * {"config": {"exceptTightLists": true}, "name": "tight.md"} * - * # Foo - * ## Bar + * * Venus. * - * - Paragraph - * + List. - * - * Paragraph. + * * Mars. * * @example - * {"name": "tight.md", "config": {"exceptTightLists": true}, "label": "output"} - * - * 2:1-2:7: Missing blank line before block node + * {"label": "input", "name": "containers.md"} * - * @example - * {"name": "containers.md", "label": "input"} - * - * > # Alpha + * > # Venus * > - * > Bravo. + * > Mercury. * - * - charlie. - * - delta. + * - earth. + * - mars. * - * + # Echo - * Foxtrot. + * * # Jupiter + * Saturn. * @example - * {"name": "containers.md", "label": "output"} + * {"label": "output", "name": "containers.md"} * - * 9:3-9:11: Missing blank line before block node + * 9:3-9:10: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line * * @example * {"gfm": true, "label": "input", "name": "gfm.md"} * - * GFM tables and footnotes are also checked[^e] - * - * | Alpha | Bravo | - * | ------- | ----- | - * | Charlie | Delta | - * - * [^e]: Echo - * [^f]: Foxtrot. + * | Planet | Diameter | + * | ------- | -------- | + * | Mercury | 4 880 km | * + * [^Mercury]: + * **Mercury** is the first planet from the Sun and the smallest + * in the Solar System. + * [^Venus]: + * **Venus** is the second planet from the Sun. * @example * {"gfm": true, "label": "output", "name": "gfm.md"} * - * 8:1-8:15: Missing blank line before block node + * 8:1-9:49: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line * * @example * {"label": "input", "mdx": true, "name": "mdx.mdx"} * - * MDX JSX flow elements and expressions are also checked. - * * - * # Alpha - * Bravo. + * # Venus + * Mars. * * {Math.PI} - * * @example * {"label": "output", "mdx": true, "name": "mdx.mdx"} * - * 5:3-5:9: Missing blank line before block node - * 7:1-7:10: Missing blank line before block node + * 3:3-3:8: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line + * 5:1-5:10: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line * * @example * {"label": "input", "math": true, "name": "math.md"} * - * Math is also checked. - * * $$ * \frac{1}{2} * $$ * $$ * \frac{2}{3} * $$ - * * @example * {"label": "output", "math": true, "name": "math.md"} * - * 6:1-8:3: Missing blank line before block node + * 4:1-6:3: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line * * @example * {"directive": true, "label": "input", "name": "directive.md"} * * Directives are also checked. * - * ::video{#123} - * :::tip - * Tip! + * ::video{#mercury} + * :::planet + * Venus. * ::: - * * @example * {"directive": true, "label": "output", "name": "directive.md"} * - * 4:1-6:4: Missing blank line before block node + * 4:1-6:4: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line */ /** @@ -199,9 +187,10 @@ /// /// +import {phrasing} from 'mdast-util-phrasing' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {SKIP, visitParents} from 'unist-util-visit-parents' /** @type {ReadonlyArray} */ // eslint-disable-next-line unicorn/prefer-set-has @@ -242,24 +231,39 @@ const remarkLintNoMissingBlankLines = lintRule( function (tree, file, options) { const exceptTightLists = options ? options.exceptTightLists : false - visit(tree, function (node, index, parent) { + visitParents(tree, function (node, parents) { + const parent = parents[parents.length - 1] + + // Ignore phrasing nodes and non-parents. + if (phrasing(node)) return SKIP + if (!parent) return + if ( - parent && - typeof index === 'number' && - types.includes(node.type) && - (parent.type !== 'listItem' || !exceptTightLists) + // Children of list items are normally checked. + (!exceptTightLists || parent.type !== 'listItem') && + // Known block: + types.includes(node.type) ) { const start = pointStart(node) - const previous = parent.children[index - 1] + const siblings = /** @type {Array} */ (parent.children) + const previous = siblings[siblings.indexOf(node) - 1] const previousEnd = pointEnd(previous) if ( - start && - previousEnd && - types.includes(previous.type) && - start.line === previousEnd.line + 1 + !previous || + !previousEnd || + !start || + // Other known block: + !types.includes(previous.type) ) { - file.message('Missing blank line before block node', node) + return + } + + if (previousEnd.line + 1 === start.line) { + file.message( + 'Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line', + {ancestors: [...parents, node], place: node.position} + ) } } }) diff --git a/packages/remark-lint-no-missing-blank-lines/package.json b/packages/remark-lint-no-missing-blank-lines/package.json index 6f29520b..ffbc1fde 100644 --- a/packages/remark-lint-no-missing-blank-lines/package.json +++ b/packages/remark-lint-no-missing-blank-lines/package.json @@ -35,9 +35,10 @@ "mdast-util-directive": "^3.0.0", "mdast-util-math": "^3.0.0", "mdast-util-mdx": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -49,7 +50,8 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "unicorn/prefer-at": "off" } } } diff --git a/packages/remark-lint-no-missing-blank-lines/readme.md b/packages/remark-lint-no-missing-blank-lines/readme.md index 252d828c..45e9e2a6 100644 --- a/packages/remark-lint-no-missing-blank-lines/readme.md +++ b/packages/remark-lint-no-missing-blank-lines/readme.md @@ -10,7 +10,7 @@ [![Backers][badge-funding-backers-image]][badge-funding-url] [![Chat][badge-chat-image]][badge-chat-url] -[`remark-lint`][github-remark-lint] rule to warn when there are no blank lines between blocks. +[`remark-lint`][github-remark-lint] rule to warn when blank lines are missing. ## Contents @@ -121,7 +121,7 @@ The default export is ### `unified().use(remarkLintNoMissingBlankLines[, options])` -Warn when there are no blank lines between blocks. +Warn when blank lines are missing. ###### Parameters @@ -159,15 +159,17 @@ It has a `join` function to customize such behavior. ###### In ```markdown -# Foo +# Mercury -## Bar +## Venus -- Paragraph +* Earth. - + List. + * Mars. -Paragraph. +> # Jupiter +> +> Saturn. ``` ###### Out @@ -179,20 +181,22 @@ No messages. ###### In ```markdown -# Foo -## Bar +# Mercury +## Venus -- Paragraph - + List. +* Earth + * Mars. -Paragraph. +> # Jupiter +> Saturn. ``` ###### Out ```text -2:1-2:7: Missing blank line before block node -5:3-5:10: Missing blank line before block node +2:1-2:9: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line +5:3-5:10: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line +8:3-8:10: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line ``` ##### `tight.md` @@ -202,41 +206,35 @@ When configured with `{ exceptTightLists: true }`. ###### In ```markdown -# Foo -## Bar - -- Paragraph - + List. +* Venus. -Paragraph. + * Mars. ``` ###### Out -```text -2:1-2:7: Missing blank line before block node -``` +No messages. ##### `containers.md` ###### In ```markdown -> # Alpha +> # Venus > -> Bravo. +> Mercury. -- charlie. -- delta. +- earth. +- mars. -+ # Echo - Foxtrot. +* # Jupiter + Saturn. ``` ###### Out ```text -9:3-9:11: Missing blank line before block node +9:3-9:10: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line ``` ##### `gfm.md` @@ -247,20 +245,21 @@ Paragraph. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -GFM tables and footnotes are also checked[^e] - -| Alpha | Bravo | -| ------- | ----- | -| Charlie | Delta | - -[^e]: Echo -[^f]: Foxtrot. +| Planet | Diameter | +| ------- | -------- | +| Mercury | 4 880 km | + +[^Mercury]: + **Mercury** is the first planet from the Sun and the smallest + in the Solar System. +[^Venus]: + **Venus** is the second planet from the Sun. ``` ###### Out ```text -8:1-8:15: Missing blank line before block node +8:1-9:49: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line ``` ##### `mdx.mdx` @@ -271,11 +270,9 @@ GFM tables and footnotes are also checked[^e] > MDX ([`remark-mdx`][github-remark-mdx]). ```mdx -MDX JSX flow elements and expressions are also checked. - - # Alpha - Bravo. + # Venus + Mars. {Math.PI} ``` @@ -283,8 +280,8 @@ MDX JSX flow elements and expressions are also checked. ###### Out ```text -5:3-5:9: Missing blank line before block node -7:1-7:10: Missing blank line before block node +3:3-3:8: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line +5:1-5:10: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line ``` ##### `math.md` @@ -295,8 +292,6 @@ MDX JSX flow elements and expressions are also checked. > math ([`remark-math`][github-remark-math]). ```markdown -Math is also checked. - $$ \frac{1}{2} $$ @@ -308,7 +303,7 @@ $$ ###### Out ```text -6:1-8:3: Missing blank line before block node +4:1-6:3: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line ``` ##### `directive.md` @@ -321,16 +316,16 @@ $$ ```markdown Directives are also checked. -::video{#123} -:::tip -Tip! +::video{#mercury} +:::planet +Venus. ::: ``` ###### Out ```text -4:1-6:4: Missing blank line before block node +4:1-6:4: Unexpected `0` blank lines between nodes, expected `1` or more blank lines, add `1` blank line ``` ## Compatibility diff --git a/packages/remark-lint-no-multiple-toplevel-headings/index.js b/packages/remark-lint-no-multiple-toplevel-headings/index.js index aff1004e..13789875 100644 --- a/packages/remark-lint-no-multiple-toplevel-headings/index.js +++ b/packages/remark-lint-no-multiple-toplevel-headings/index.js @@ -1,9 +1,9 @@ /** - * remark-lint rule to warn when multiple top-level headings are used. + * remark-lint rule to warn when top-level headings are used multiple times. * * ## What is this? * - * This package checks that no more than one top level heading is used. + * This package checks that top-level headings are unique. * * ## When should I use this? * @@ -13,7 +13,7 @@ * * ### `unified().use(remarkLintNoMultipleToplevelHeadings[, options])` * - * Warn when multiple top-level headings are used. + * Warn when top-level headings are used multiple times. * * ###### Parameters * @@ -59,54 +59,66 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md", "config": 1} + * {"name": "ok.md"} * - * # Foo + * # Mercury * - * ## Bar + * ## Venus * * @example - * {"name": "not-ok.md", "config": 1, "label": "input"} + * {"label": "input", "name": "not-ok.md"} + * + * # Venus * - * # Foo + * # Mercury + * @example + * {"label": "output", "name": "not-ok.md"} * - * # Bar + * 3:1-3:10: Unexpected duplicate toplevel heading, exected a single heading with rank `1` * * @example - * {"name": "not-ok.md", "config": 1, "label": "output"} + * {"config": 2, "label": "input", "name": "not-ok.md"} * - * 3:1-3:6: Don’t use multiple top level headings (1:1) + * ## Venus + * + * ## Mercury + * @example + * {"config": 2, "label": "output", "name": "not-ok.md"} + * + * 3:1-3:11: Unexpected duplicate toplevel heading, exected a single heading with rank `2` * * @example * {"label": "input", "name": "html.md"} * - * In markdown, HTML is supported. + * Venus and mercury. * - *

First

+ *

Earth

* - *

Second

+ *

Mars

* @example * {"label": "output", "name": "html.md"} * - * 5:1-5:16: Don’t use multiple top level headings (3:1) + * 5:1-5:14: Unexpected duplicate toplevel heading, exected a single heading with rank `1` * * @example * {"label": "input", "mdx": true, "name": "mdx.mdx"} * - * In MDX, JSX is supported. + * Venus and mercury. * - *

First

- *

Second

+ *

Earth

+ *

Mars

* @example * {"label": "output", "mdx": true, "name": "mdx.mdx"} * - * 4:1-4:16: Don’t use multiple top level headings (3:1) + * 4:1-4:14: Unexpected duplicate toplevel heading, exected a single heading with rank `1` */ /** - * @typedef {import('mdast').Root} Root * @typedef {import('mdast').Heading} Heading + * @typedef {import('mdast').Nodes} Nodes + * @typedef {import('mdast').Root} Root */ /** @@ -119,10 +131,10 @@ /// +import {ok as assert} from 'devlop' import {lintRule} from 'unified-lint-rule' -import {pointStart, position} from 'unist-util-position' -import {stringifyPosition} from 'unist-util-stringify-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const htmlRe = / | undefined} */ + let duplicateAncestors - visit(tree, function (node) { + visitParents(tree, function (node, parents) { /** @type {Depth | undefined} */ let rank @@ -164,17 +176,33 @@ const remarkLintNoMultipleToplevelHeadings = lintRule( } if (rank) { - const start = pointStart(node) - const place = position(node) + const ancestors = [...parents, node] + + if (node.position && rank === option) { + if (duplicateAncestors) { + const duplicate = duplicateAncestors.at(-1) + assert(duplicate) // Always defined. - if (start && place && rank === option) { - if (duplicate) { file.message( - 'Don’t use multiple top level headings (' + duplicate + ')', - place + 'Unexpected duplicate toplevel heading, exected a single heading with rank `' + + rank + + '`', + { + ancestors, + cause: new VFileMessage( + 'Toplevel heading already defined here', + { + ancestors: duplicateAncestors, + place: duplicate.position, + source: 'remark-lint', + ruleId: 'no-multiple-toplevel-headings' + } + ), + place: node.position + } ) } else { - duplicate = stringifyPosition(start) + duplicateAncestors = ancestors } } } diff --git a/packages/remark-lint-no-multiple-toplevel-headings/package.json b/packages/remark-lint-no-multiple-toplevel-headings/package.json index a1241fc8..be2d76eb 100644 --- a/packages/remark-lint-no-multiple-toplevel-headings/package.json +++ b/packages/remark-lint-no-multiple-toplevel-headings/package.json @@ -32,11 +32,11 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "mdast-util-mdx": "^3.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^5.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-multiple-toplevel-headings/readme.md b/packages/remark-lint-no-multiple-toplevel-headings/readme.md index 2543b478..c35bfca9 100644 --- a/packages/remark-lint-no-multiple-toplevel-headings/readme.md +++ b/packages/remark-lint-no-multiple-toplevel-headings/readme.md @@ -10,7 +10,7 @@ [![Backers][badge-funding-backers-image]][badge-funding-url] [![Chat][badge-chat-image]][badge-chat-url] -[`remark-lint`][github-remark-lint] rule to warn when multiple top-level headings are used. +[`remark-lint`][github-remark-lint] rule to warn when top-level headings are used multiple times. ## Contents @@ -31,7 +31,7 @@ ## What is this? -This package checks that no more than one top level heading is used. +This package checks that top-level headings are unique. ## When should I use this? @@ -126,7 +126,7 @@ The default export is ### `unified().use(remarkLintNoMultipleToplevelHeadings[, options])` -Warn when multiple top-level headings are used. +Warn when top-level headings are used multiple times. ###### Parameters @@ -166,14 +166,12 @@ which is typically a heading with a rank of `1`. ##### `ok.md` -When configured with `1`. - ###### In ```markdown -# Foo +# Mercury -## Bar +## Venus ``` ###### Out @@ -182,20 +180,36 @@ No messages. ##### `not-ok.md` -When configured with `1`. +###### In + +```markdown +# Venus + +# Mercury +``` + +###### Out + +```text +3:1-3:10: Unexpected duplicate toplevel heading, exected a single heading with rank `1` +``` + +##### `not-ok.md` + +When configured with `2`. ###### In ```markdown -# Foo +## Venus -# Bar +## Mercury ``` ###### Out ```text -3:1-3:6: Don’t use multiple top level headings (1:1) +3:1-3:11: Unexpected duplicate toplevel heading, exected a single heading with rank `2` ``` ##### `html.md` @@ -203,17 +217,17 @@ When configured with `1`. ###### In ```markdown -In markdown, HTML is supported. +Venus and mercury. -

First

+

Earth

-

Second

+

Mars

``` ###### Out ```text -5:1-5:16: Don’t use multiple top level headings (3:1) +5:1-5:14: Unexpected duplicate toplevel heading, exected a single heading with rank `1` ``` ##### `mdx.mdx` @@ -224,16 +238,16 @@ In markdown, HTML is supported. > MDX ([`remark-mdx`][github-remark-mdx]). ```mdx -In MDX, JSX is supported. +Venus and mercury. -

First

-

Second

+

Earth

+

Mars

``` ###### Out ```text -4:1-4:16: Don’t use multiple top level headings (3:1) +4:1-4:14: Unexpected duplicate toplevel heading, exected a single heading with rank `1` ``` ## Compatibility diff --git a/packages/remark-lint-no-paragraph-content-indent/index.js b/packages/remark-lint-no-paragraph-content-indent/index.js index 62c9425f..c46e310c 100644 --- a/packages/remark-lint-no-paragraph-content-indent/index.js +++ b/packages/remark-lint-no-paragraph-content-indent/index.js @@ -35,81 +35,63 @@ * @author Titus Wormer * @copyright 2017 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * Alpha - * - * Bravo - * Charlie. - * **Delta**. - * - * * Echo - * Foxtrot. - * - * > Golf - * > Hotel. - * - * `india()` - * juliett. - * - * - `kilo()` - * lima. + * Mercury. * - * - `mike()` - november. + * Venus and + * **Earth**. * - * ![image]() text + * * Mars and + * Jupiter. * - * ![image reference][] text - * - * [![][text]][text] + * > Saturn and + * > Uranus. * * @example - * {"name": "not-ok.md", "label": "input"} - * - * ␠Alpha - * - * Bravo - * ␠Charlie. + * {"label": "input", "name": "not-ok.md"} * - * * Delta - * ␠Echo. + * ␠Mercury. * - * > Foxtrot - * > ␠Golf. + * Venus and + * ␠␠**Earth**. * - * `hotel()` - * ␠india. + * * Mars and + * ␠␠Jupiter. * - * - `juliett()` - * ␠kilo. + * > Saturn and + * > ␠Uranus. * - * ␠![lima]() mike + * * Neptune + * and + * ␠␠Pluto. * - * * november - * oscar - * ␠papa. + * > Ceres + * and + * > ␠Makemake. * * @example - * {"name": "not-ok.md", "label": "output"} - * - * 1:2: Expected no indentation in paragraph content - * 4:2: Expected no indentation in paragraph content - * 7:6: Expected no indentation in paragraph content - * 10:4: Expected no indentation in paragraph content - * 13:2: Expected no indentation in paragraph content - * 16:6: Expected no indentation in paragraph content - * 18:2: Expected no indentation in paragraph content - * 22:4: Expected no indentation in paragraph content + * {"label": "output", "name": "not-ok.md"} + * + * 1:2: Unexpected `1` extra space before content line, remove `1` space + * 4:3: Unexpected `2` extra spaces before content line, remove `2` spaces + * 7:5: Unexpected `2` extra spaces before content line, remove `2` spaces + * 10:4: Unexpected `1` extra space before content line, remove `1` space + * 14:5: Unexpected `2` extra spaces before content line, remove `2` spaces + * 18:4: Unexpected `1` extra space before content line, remove `1` space */ /** * @typedef {import('mdast').Root} Root */ +import {ok as assert} from 'devlop' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {SKIP, visit} from 'unist-util-visit' +import {SKIP, visitParents} from 'unist-util-visit-parents' import {location} from 'vfile-location' const remarkLintNoParagraphContentIndent = lintRule( @@ -125,60 +107,100 @@ const remarkLintNoParagraphContentIndent = lintRule( */ function (tree, file) { const value = String(file) - const loc = location(value) + const locations = location(value) + + // Note: this code is very similar to `remark-lint-no-table-indentation`. + visitParents(tree, 'paragraph', function (node, parents) { + const parent = parents.at(-1) + const end = pointEnd(node) + const start = pointStart(node) + + if (!parent || !end || !start) return - visit(tree, 'paragraph', function (node, _, parent) { - const end = pointEnd(node)?.line - let line = pointStart(node)?.line + const parentHead = parent.children[0] + // Always defined if we have a parent. + assert(parentHead) + let line = start.line /** @type {number | undefined} */ let column - if (parent && parent.type === 'root') { + if (parent.type === 'root') { column = 1 - } else if (parent && parent.type === 'blockquote') { + } else if (parent.type === 'blockquote') { const parentStart = pointStart(parent) - if (parentStart) column = parentStart.column + 2 - } else if (parent && parent.type === 'listItem') { - column = pointStart(parent.children[0])?.column - // Skip past the first line if we’re the first child of a list item. - if (line && parent.children[0] === node) { - line++ + if (parentStart) { + column = parentStart.column + 2 } - } + } else if (parent.type === 'listItem') { + const headStart = pointStart(parentHead) + + if (headStart) { + column = headStart.column - // In a parent we don’t know, exit. - if (!column || !line || !end) { - return + // Skip past the first line if we’re the first child of a list item. + if (parentHead === node) { + line++ + } + } } - while (line <= end) { - let offset = loc.toOffset({line, column}) - const lineColumn = offset + /* c8 ignore next -- unknown parent. */ + if (!column) return - /* c8 ignore next 3 -- we get here if we have offsets. */ - if (typeof lineColumn !== 'number' || typeof offset !== 'number') { - continue - } + while (line <= end.line) { + let index = locations.toOffset({line, column}) - while (/[ \t]/.test(value.charAt(offset - 1))) { - offset-- + /* c8 ignore next -- out of range somehow. */ + if (typeof index !== 'number') continue + + const expected = index + + // Check that we only have whitespace / block quote marker before. + // We expect a line ending or a block quote marker. + // Otherwise (weird ancestor or lazy line) we stop. + let code = value.charCodeAt(index - 1) + + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + index-- + code = value.charCodeAt(index - 1) } - // Exit if we find some other content before this line. - // This might be because the paragraph line is lazy, which isn’t this - // rule. - if (!offset || /[\r\n>]/.test(value.charAt(offset - 1))) { - offset = lineColumn + if ( + code === 10 /* `\n` */ || + code === 13 /* `\r` */ || + code === 62 /* `>` */ || + Number.isNaN(code) + ) { + // Now check superfluous indent. + let actual = expected - while (/[ \t]/.test(value.charAt(offset))) { - offset++ + code = value.charCodeAt(actual) + + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + code = value.charCodeAt(++actual) } - if (lineColumn !== offset) { + const difference = actual - expected + + if (difference !== 0) { file.message( - 'Expected no indentation in paragraph content', - loc.toPoint(offset) + 'Unexpected `' + + difference + + '` extra ' + + pluralize('space', difference) + + ' before content line, remove `' + + difference + + '` ' + + pluralize('space', difference), + { + ancestors: [...parents, node], + place: { + line, + column: column + difference, + offset: actual + } + } ) } } diff --git a/packages/remark-lint-no-paragraph-content-indent/package.json b/packages/remark-lint-no-paragraph-content-indent/package.json index bf10d9ee..8b69226c 100644 --- a/packages/remark-lint-no-paragraph-content-indent/package.json +++ b/packages/remark-lint-no-paragraph-content-indent/package.json @@ -33,9 +33,11 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", "vfile-location": "^5.0.0" }, "scripts": {}, @@ -49,7 +51,9 @@ "prettier": true, "rules": { "capitalized-comments": "off", - "complexity": "off" + "complexity": "off", + "unicorn/prefer-code-point": "off", + "unicorn/prefer-switch": "off" } } } diff --git a/packages/remark-lint-no-paragraph-content-indent/readme.md b/packages/remark-lint-no-paragraph-content-indent/readme.md index a1a4970f..c33db6c7 100644 --- a/packages/remark-lint-no-paragraph-content-indent/readme.md +++ b/packages/remark-lint-no-paragraph-content-indent/readme.md @@ -140,31 +140,16 @@ So it’s recommended to turn this rule on. ###### In ```markdown -Alpha +Mercury. -Bravo -Charlie. -**Delta**. +Venus and +**Earth**. -* Echo - Foxtrot. +* Mars and + Jupiter. -> Golf -> Hotel. - -`india()` -juliett. - -- `kilo()` - lima. - -- `mike()` - november. - -![image]() text - -![image reference][] text - -[![][text]][text] +> Saturn and +> Uranus. ``` ###### Out @@ -176,41 +161,35 @@ No messages. ###### In ```markdown -␠Alpha - -Bravo -␠Charlie. - -* Delta - ␠Echo. +␠Mercury. -> Foxtrot -> ␠Golf. +Venus and +␠␠**Earth**. -`hotel()` -␠india. +* Mars and + ␠␠Jupiter. -- `juliett()` - ␠kilo. +> Saturn and +> ␠Uranus. -␠![lima]() mike +* Neptune +and + ␠␠Pluto. -* november -oscar - ␠papa. +> Ceres +and +> ␠Makemake. ``` ###### Out ```text -1:2: Expected no indentation in paragraph content -4:2: Expected no indentation in paragraph content -7:6: Expected no indentation in paragraph content -10:4: Expected no indentation in paragraph content -13:2: Expected no indentation in paragraph content -16:6: Expected no indentation in paragraph content -18:2: Expected no indentation in paragraph content -22:4: Expected no indentation in paragraph content +1:2: Unexpected `1` extra space before content line, remove `1` space +4:3: Unexpected `2` extra spaces before content line, remove `2` spaces +7:5: Unexpected `2` extra spaces before content line, remove `2` spaces +10:4: Unexpected `1` extra space before content line, remove `1` space +14:5: Unexpected `2` extra spaces before content line, remove `2` spaces +18:4: Unexpected `1` extra space before content line, remove `1` space ``` ## Compatibility diff --git a/packages/remark-lint-no-reference-like-url/index.js b/packages/remark-lint-no-reference-like-url/index.js index 08c4d4c9..024cd569 100644 --- a/packages/remark-lint-no-reference-like-url/index.js +++ b/packages/remark-lint-no-reference-like-url/index.js @@ -40,33 +40,46 @@ * @author Titus Wormer * @copyright 2016 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * [Alpha](http://example.com). + * [**Mercury**][mercury] is the first planet from the sun. * - * [bravo]: https://example.com + * [mercury]: https://example.com/mercury/ * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} + * + * [**Mercury**](mercury) is the first planet from the sun. * - * [Charlie](delta). + * [mercury]: https://example.com/mercury/ + * @example + * {"label": "output", "name": "not-ok.md"} * - * [delta]: https://example.com + * 1:1-1:23: Unexpected resource link (`[text](url)`) with URL that matches a definition identifier (as `mercury`), expected reference (`[text][id]`) * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "input", "name": "image.md"} * - * 1:1-1:17: Did you mean to use `[delta]` instead of `(delta)`, a reference? + * ![**Mercury** is a planet](mercury). + * + * [mercury]: https://example.com/mercury.jpg + * @example + * {"label": "output", "name": "image.md"} + * + * 1:1-1:36: Unexpected resource image (`![text](url)`) with URL that matches a definition identifier (as `mercury`), expected reference (`![text][id]`) */ /** + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root */ +import {ok as assert} from 'devlop' import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintNoReferenceLikeUrl = lintRule( { @@ -80,32 +93,55 @@ const remarkLintNoReferenceLikeUrl = lintRule( * Nothing. */ function (tree, file) { - /** @type {Set} */ - const identifiers = new Set() + /** @type {Map>} */ + const definitions = new Map() + /** @type {Array>} */ + const references = [] - visit(tree, 'definition', function (node) { - identifiers.add(node.identifier.toLowerCase()) + visitParents(tree, function (node, ancestors) { + if (node.type === 'definition') { + definitions.set(node.identifier.toLowerCase(), [...ancestors, node]) + } else if (node.type === 'image' || node.type === 'link') { + references.push([...ancestors, node]) + } }) - visit(tree, function (node) { - const place = position(node) + for (const ancestors of references) { + const node = ancestors.at(-1) + assert(node) // Always defined. + assert(node.type === 'image' || node.type === 'link') // Always media. + const maybeIdentifier = node.url.toLowerCase() + const definitionAncestors = definitions.get(maybeIdentifier) + + if (node.position && definitionAncestors) { + const definition = definitionAncestors.at(-1) + assert(definition) // Always defined. + assert(definition.type === 'definition') // Always definition. + const prefix = node.type === 'image' ? '!' : '' - if ( - place && - (node.type === 'image' || node.type === 'link') && - identifiers.has(node.url.toLowerCase()) - ) { file.message( - 'Did you mean to use `[' + - node.url + - ']` instead of ' + - '`(' + - node.url + - ')`, a reference?', - place + 'Unexpected resource ' + + node.type + + ' (`' + + prefix + + '[text](url)`) with URL that matches a definition identifier (as `' + + definition.identifier + + '`), expected reference (`' + + prefix + + '[text][id]`)', + { + ancestors, + cause: new VFileMessage('Definition defined here', { + ancestors: definitionAncestors, + place: definition.position, + source: 'remark-lint', + ruleId: 'no-reference-like-url' + }), + place: node.position + } ) } - }) + } } ) diff --git a/packages/remark-lint-no-reference-like-url/package.json b/packages/remark-lint-no-reference-like-url/package.json index 714852d7..42005cb6 100644 --- a/packages/remark-lint-no-reference-like-url/package.json +++ b/packages/remark-lint-no-reference-like-url/package.json @@ -33,9 +33,11 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^5.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-reference-like-url/readme.md b/packages/remark-lint-no-reference-like-url/readme.md index 3b6d03c3..1a8fcd52 100644 --- a/packages/remark-lint-no-reference-like-url/readme.md +++ b/packages/remark-lint-no-reference-like-url/readme.md @@ -145,9 +145,9 @@ then a link `[text](alpha)` should instead have been `[text][alpha]`. ###### In ```markdown -[Alpha](http://example.com). +[**Mercury**][mercury] is the first planet from the sun. -[bravo]: https://example.com +[mercury]: https://example.com/mercury/ ``` ###### Out @@ -159,15 +159,31 @@ No messages. ###### In ```markdown -[Charlie](delta). +[**Mercury**](mercury) is the first planet from the sun. -[delta]: https://example.com +[mercury]: https://example.com/mercury/ ``` ###### Out ```text -1:1-1:17: Did you mean to use `[delta]` instead of `(delta)`, a reference? +1:1-1:23: Unexpected resource link (`[text](url)`) with URL that matches a definition identifier (as `mercury`), expected reference (`[text][id]`) +``` + +##### `image.md` + +###### In + +```markdown +![**Mercury** is a planet](mercury). + +[mercury]: https://example.com/mercury.jpg +``` + +###### Out + +```text +1:1-1:36: Unexpected resource image (`![text](url)`) with URL that matches a definition identifier (as `mercury`), expected reference (`![text][id]`) ``` ## Compatibility diff --git a/packages/remark-lint-no-shell-dollars/index.js b/packages/remark-lint-no-shell-dollars/index.js index 4dcc3ab6..8e558b8f 100644 --- a/packages/remark-lint-no-shell-dollars/index.js +++ b/packages/remark-lint-no-shell-dollars/index.js @@ -38,52 +38,49 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * * ```bash - * echo a + * echo "Mercury and Venus" * ``` * * ```sh - * echo a - * echo a > file + * echo "Mercury and Venus" + * echo "Earth and Mars" > file * ``` * * ```zsh - * $ echo a - * a - * $ echo a > file + * $ echo "Mercury and Venus" + * Mercury and Venus + * $ echo "Earth and Mars" > file * ``` * - * Some empty code: - * * ```command * ``` * - * It’s fine to use dollars in non-shell code. - * * ```js * $('div').remove() * ``` * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * * ```sh - * $ echo a + * $ echo "Mercury and Venus" * ``` * * ```bash - * $ echo a - * $ echo a > file + * $ echo "Mercury and Venus" + * $ echo "Earth and Mars" > file * ``` * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:1-3:4: Do not use dollar signs before shell commands - * 5:1-8:4: Do not use dollar signs before shell commands + * 1:1-3:4: Unexpected shell code with every line prefixed by `$`, expected different code for input and output + * 5:1-8:4: Unexpected shell code with every line prefixed by `$`, expected different code for input and output */ /** @@ -92,8 +89,7 @@ import {collapseWhiteSpace} from 'collapse-white-space' import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' // See: const flags = new Set([ @@ -136,33 +132,38 @@ const remarkLintNoShellDollars = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'code', function (node) { - const place = position(node) - - // Check known shell code. - if (place && node.lang && flags.has(node.lang)) { + visitParents(tree, 'code', function (node, parents) { + if ( + node.position && + // Check known shell code. + node.lang && + flags.has(node.lang) + ) { const lines = node.value.split('\n') let index = -1 - let hasLines = false + let some = false while (++index < lines.length) { - const line = collapseWhiteSpace(lines[index], {style: 'html'}) + const line = collapseWhiteSpace(lines[index], { + style: 'html', + trim: true + }) if (!line) continue - hasLines = true + // Unprefixed line is fine. + if (line.charCodeAt(0) !== 36 /* `$` */) return - if (!/^\$/.test(line)) { - return - } + some = true } - if (!hasLines) { - return - } + if (!some) return - file.message('Do not use dollar signs before shell commands', place) + file.message( + 'Unexpected shell code with every line prefixed by `$`, expected different code for input and output', + {ancestors: [...parents, node], place: node.position} + ) } }) } diff --git a/packages/remark-lint-no-shell-dollars/package.json b/packages/remark-lint-no-shell-dollars/package.json index 616b17b7..8a7bd4cf 100644 --- a/packages/remark-lint-no-shell-dollars/package.json +++ b/packages/remark-lint-no-shell-dollars/package.json @@ -34,8 +34,7 @@ "@types/mdast": "^4.0.0", "collapse-white-space": "^2.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -47,7 +46,8 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-no-shell-dollars/readme.md b/packages/remark-lint-no-shell-dollars/readme.md index 05ffc96c..0512237c 100644 --- a/packages/remark-lint-no-shell-dollars/readme.md +++ b/packages/remark-lint-no-shell-dollars/readme.md @@ -148,27 +148,23 @@ or use different code blocks for commands and output. ````markdown ```bash -echo a +echo "Mercury and Venus" ``` ```sh -echo a -echo a > file +echo "Mercury and Venus" +echo "Earth and Mars" > file ``` ```zsh -$ echo a -a -$ echo a > file +$ echo "Mercury and Venus" +Mercury and Venus +$ echo "Earth and Mars" > file ``` -Some empty code: - ```command ``` -It’s fine to use dollars in non-shell code. - ```js $('div').remove() ``` @@ -184,20 +180,20 @@ No messages. ````markdown ```sh -$ echo a +$ echo "Mercury and Venus" ``` ```bash -$ echo a -$ echo a > file +$ echo "Mercury and Venus" +$ echo "Earth and Mars" > file ``` ```` ###### Out ```text -1:1-3:4: Do not use dollar signs before shell commands -5:1-8:4: Do not use dollar signs before shell commands +1:1-3:4: Unexpected shell code with every line prefixed by `$`, expected different code for input and output +5:1-8:4: Unexpected shell code with every line prefixed by `$`, expected different code for input and output ``` ## Compatibility diff --git a/packages/remark-lint-no-shortcut-reference-image/index.js b/packages/remark-lint-no-shortcut-reference-image/index.js index 9abafe9d..b3915cd6 100644 --- a/packages/remark-lint-no-shortcut-reference-image/index.js +++ b/packages/remark-lint-no-shortcut-reference-image/index.js @@ -38,24 +38,24 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * ![foo][] + * ![Mercury][] * - * [foo]: http://foo.bar/baz.png + * [mercury]: /mercury.png * * @example - * {"name": "not-ok.md", "label": "input"} - * - * ![foo] + * {"label": "input", "name": "not-ok.md"} * - * [foo]: http://foo.bar/baz.png + * ![Mercury] * + * [mercury]: /mercury.png * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:1-1:7: Use the trailing [] on reference images + * 1:1-1:11: Unexpected shortcut reference image (`![text]`), expected collapsed reference (`![text][]`) */ /** @@ -63,8 +63,7 @@ */ import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoShortcutReferenceImage = lintRule( { @@ -78,10 +77,12 @@ const remarkLintNoShortcutReferenceImage = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'imageReference', function (node) { - const place = position(node) - if (place && node.referenceType === 'shortcut') { - file.message('Use the trailing [] on reference images', place) + visitParents(tree, 'imageReference', function (node, parents) { + if (node.position && node.referenceType === 'shortcut') { + file.message( + 'Unexpected shortcut reference image (`![text]`), expected collapsed reference (`![text][]`)', + {ancestors: [...parents, node], place: node.position} + ) } }) } diff --git a/packages/remark-lint-no-shortcut-reference-image/package.json b/packages/remark-lint-no-shortcut-reference-image/package.json index 72181e99..dae7694b 100644 --- a/packages/remark-lint-no-shortcut-reference-image/package.json +++ b/packages/remark-lint-no-shortcut-reference-image/package.json @@ -34,8 +34,7 @@ "dependencies": { "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-shortcut-reference-image/readme.md b/packages/remark-lint-no-shortcut-reference-image/readme.md index fd989485..db044a3f 100644 --- a/packages/remark-lint-no-shortcut-reference-image/readme.md +++ b/packages/remark-lint-no-shortcut-reference-image/readme.md @@ -148,9 +148,9 @@ So it’s recommended to use collapsed or full references instead. ###### In ```markdown -![foo][] +![Mercury][] -[foo]: http://foo.bar/baz.png +[mercury]: /mercury.png ``` ###### Out @@ -162,15 +162,15 @@ No messages. ###### In ```markdown -![foo] +![Mercury] -[foo]: http://foo.bar/baz.png +[mercury]: /mercury.png ``` ###### Out ```text -1:1-1:7: Use the trailing [] on reference images +1:1-1:11: Unexpected shortcut reference image (`![text]`), expected collapsed reference (`![text][]`) ``` ## Compatibility diff --git a/packages/remark-lint-no-shortcut-reference-link/index.js b/packages/remark-lint-no-shortcut-reference-link/index.js index 6fd1b48b..65d91e8c 100644 --- a/packages/remark-lint-no-shortcut-reference-link/index.js +++ b/packages/remark-lint-no-shortcut-reference-link/index.js @@ -38,24 +38,24 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * [foo][] + * [Mercury][] * - * [foo]: http://foo.bar/baz + * [mercury]: http://example.com/mercury/ * * @example - * {"name": "not-ok.md", "label": "input"} - * - * [foo] + * {"label": "input", "name": "not-ok.md"} * - * [foo]: http://foo.bar/baz + * [Mercury] * + * [mercury]: http://example.com/mercury/ * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:1-1:6: Use the trailing `[]` on reference links + * 1:1-1:10: Unexpected shortcut reference link (`[text]`), expected collapsed reference (`[text][]`) */ /** @@ -63,8 +63,7 @@ */ import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoShortcutReferenceLink = lintRule( { @@ -78,10 +77,12 @@ const remarkLintNoShortcutReferenceLink = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'linkReference', function (node) { - const place = position(node) - if (place && node.referenceType === 'shortcut') { - file.message('Use the trailing `[]` on reference links', place) + visitParents(tree, 'linkReference', function (node, parents) { + if (node.position && node.referenceType === 'shortcut') { + file.message( + 'Unexpected shortcut reference link (`[text]`), expected collapsed reference (`[text][]`)', + {ancestors: [...parents, node], place: node.position} + ) } }) } diff --git a/packages/remark-lint-no-shortcut-reference-link/package.json b/packages/remark-lint-no-shortcut-reference-link/package.json index 8cb5f4d0..714680ad 100644 --- a/packages/remark-lint-no-shortcut-reference-link/package.json +++ b/packages/remark-lint-no-shortcut-reference-link/package.json @@ -34,8 +34,7 @@ "dependencies": { "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-shortcut-reference-link/readme.md b/packages/remark-lint-no-shortcut-reference-link/readme.md index 7490836f..7dc92a12 100644 --- a/packages/remark-lint-no-shortcut-reference-link/readme.md +++ b/packages/remark-lint-no-shortcut-reference-link/readme.md @@ -148,9 +148,9 @@ So it’s recommended to use collapsed or full references instead. ###### In ```markdown -[foo][] +[Mercury][] -[foo]: http://foo.bar/baz +[mercury]: http://example.com/mercury/ ``` ###### Out @@ -162,15 +162,15 @@ No messages. ###### In ```markdown -[foo] +[Mercury] -[foo]: http://foo.bar/baz +[mercury]: http://example.com/mercury/ ``` ###### Out ```text -1:1-1:6: Use the trailing `[]` on reference links +1:1-1:10: Unexpected shortcut reference link (`[text]`), expected collapsed reference (`[text][]`) ``` ## Compatibility diff --git a/packages/remark-lint-no-table-indentation/index.js b/packages/remark-lint-no-table-indentation/index.js index 603cab06..48d7a4ca 100644 --- a/packages/remark-lint-no-table-indentation/index.js +++ b/packages/remark-lint-no-table-indentation/index.js @@ -45,63 +45,60 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md", "gfm": true} * - * Paragraph. - * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | + * | Planet | Mean anomaly (°) | + * | ------- | ---------------: | + * | Mercury | 174 796 | * * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} - * - * Paragraph. + * {"gfm": true, "label": "input", "name": "not-ok.md"} * - * ␠␠␠| A | B | - * ␠␠␠| ----- | ----- | - * ␠␠␠| Alpha | Bravo | + * ␠| Planet | Mean anomaly (°) | + * ␠␠| ------- | ---------------: | + * ␠␠␠| Mercury | 174 796 | * * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} + * {"gfm": true, "label": "output", "name": "not-ok.md"} * - * 3:4: Do not indent table rows - * 4:4: Do not indent table rows - * 5:4: Do not indent table rows + * 1:2: Unexpected `1` extra space before table row, remove `1` space + * 2:3: Unexpected `2` extra spaces before table row, remove `2` spaces + * 3:4: Unexpected `3` extra spaces before table row, remove `3` spaces * * @example - * {"name": "not-ok-blockquote.md", "label": "input", "gfm": true} + * {"gfm": true, "label": "input", "name": "blockquote.md"} * - * >␠␠| A | - * >␠| - | + * >␠| Planet | + * >␠␠| ------- | * * @example - * {"name": "not-ok-blockquote.md", "label": "output", "gfm": true} + * {"gfm": true, "label": "output", "name": "blockquote.md"} * - * 1:4: Do not indent table rows + * 2:4: Unexpected `1` extra space before table row, remove `1` space * * @example - * {"name": "not-ok-list.md", "label": "input", "gfm": true} + * {"gfm": true, "label": "input", "name": "list.md"} * - * -␠␠␠paragraph - * - * ␠␠␠␠␠| A | - * ␠␠␠␠| - | + * *␠| Planet | + * ␠␠␠| ------- | * * @example - * {"name": "not-ok-list.md", "label": "output", "gfm": true} + * {"gfm": true, "label": "output", "name": "list.md"} * - * 3:6: Do not indent table rows + * 2:4: Unexpected `1` extra space before table row, remove `1` space */ /** * @typedef {import('mdast').Root} Root */ +import {ok as assert} from 'devlop' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {SKIP, visit} from 'unist-util-visit' +import {SKIP, visitParents} from 'unist-util-visit-parents' import {location} from 'vfile-location' const remarkLintNoTableIndentation = lintRule( @@ -117,59 +114,101 @@ const remarkLintNoTableIndentation = lintRule( */ function (tree, file) { const value = String(file) - const loc = location(value) + const locations = location(value) - visit(tree, 'table', function (node, _, parent) { - const parentStart = pointStart(parent) + // Note: this code is very similar to `remark-lint-no-paragraph-content-indent`. + visitParents(tree, 'table', function (node, parents) { + const parent = parents.at(-1) const end = pointEnd(node) const start = pointStart(node) - /** @type {number | undefined} */ - let column - if (!start || !end) return + if (!parent || !end || !start) return + const parentHead = parent.children[0] + // Always defined if we have a parent. + assert(parentHead) let line = start.line + /** @type {number | undefined} */ + let column - if (parent && parent.type === 'root') { + if (parent.type === 'root') { column = 1 - } else if (parent && parent.type === 'blockquote') { - if (parentStart) column = parentStart.column + 2 - } else if (parent && parent.type === 'listItem') { - const head = parent.children[0] - column = pointStart(head)?.column - - /* c8 ignore next 4 -- skip past the first line if we’re the first - * child of a list item. */ - if (typeof line === 'number' && head === node) { - line++ + } else if (parent.type === 'blockquote') { + const parentStart = pointStart(parent) + + if (parentStart) { + column = parentStart.column + 2 + } + } else if (parent.type === 'listItem') { + const headStart = pointStart(parentHead) + + if (headStart) { + column = headStart.column + + // Skip past the first line if we’re the first child of a list item. + if (parentHead === node) { + line++ + } } } - /* c8 ignore next -- in a parent we don’t know, exit */ + /* c8 ignore next -- unknown parent. */ if (!column) return while (line <= end.line) { - let offset = loc.toOffset({line, column}) + let index = locations.toOffset({line, column}) - /* c8 ignore next 3 -- we get here if we have offsets. */ - if (typeof offset !== 'number') { - continue - } + /* c8 ignore next -- out of range somehow. */ + if (typeof index !== 'number') continue + + const expected = index - const lineColumn = offset - while (/[ \t]/.test(value.charAt(offset - 1))) { - offset-- + // Check that we only have whitespace / block quote marker before. + // We expect a line ending or a block quote marker. + // Otherwise (weird ancestor or lazy line) we stop. + let code = value.charCodeAt(index - 1) + + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + index-- + code = value.charCodeAt(index - 1) } - if (!offset || /[\r\n>]/.test(value.charAt(offset - 1))) { - offset = lineColumn + if ( + code === 10 /* `\n` */ || + code === 13 /* `\r` */ || + code === 62 /* `>` */ || + Number.isNaN(code) + ) { + // Now check superfluous indent. + let actual = expected + + code = value.charCodeAt(actual) - while (/[ \t]/.test(value.charAt(offset))) { - offset++ + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + code = value.charCodeAt(++actual) } - if (lineColumn !== offset) { - file.message('Do not indent table rows', loc.toPoint(offset)) + const difference = actual - expected + + if (difference !== 0) { + file.message( + 'Unexpected `' + + difference + + '` extra ' + + pluralize('space', difference) + + ' before table row, remove `' + + difference + + '` ' + + pluralize('space', difference), + { + ancestors: [...parents, node], + place: { + line, + column: column + difference, + offset: actual + } + } + ) } } diff --git a/packages/remark-lint-no-table-indentation/package.json b/packages/remark-lint-no-table-indentation/package.json index 6d5c8be7..bbfd20fc 100644 --- a/packages/remark-lint-no-table-indentation/package.json +++ b/packages/remark-lint-no-table-indentation/package.json @@ -32,9 +32,11 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", "vfile-location": "^5.0.0" }, "scripts": {}, @@ -47,7 +49,10 @@ "xo": { "prettier": true, "rules": { - "capitalized-comments": "off" + "capitalized-comments": "off", + "complexity": "off", + "unicorn/prefer-code-point": "off", + "unicorn/prefer-switch": "off" } } } diff --git a/packages/remark-lint-no-table-indentation/readme.md b/packages/remark-lint-no-table-indentation/readme.md index 4d389927..2feb35d4 100644 --- a/packages/remark-lint-no-table-indentation/readme.md +++ b/packages/remark-lint-no-table-indentation/readme.md @@ -156,11 +156,9 @@ So it’s recommended to not indent tables and to turn this rule on. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -Paragraph. - -| A | B | -| ----- | ----- | -| Alpha | Bravo | +| Planet | Mean anomaly (°) | +| ------- | ---------------: | +| Mercury | 174 796 | ``` ###### Out @@ -175,22 +173,20 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -Paragraph. - -␠␠␠| A | B | -␠␠␠| ----- | ----- | -␠␠␠| Alpha | Bravo | +␠| Planet | Mean anomaly (°) | +␠␠| ------- | ---------------: | +␠␠␠| Mercury | 174 796 | ``` ###### Out ```text -3:4: Do not indent table rows -4:4: Do not indent table rows -5:4: Do not indent table rows +1:2: Unexpected `1` extra space before table row, remove `1` space +2:3: Unexpected `2` extra spaces before table row, remove `2` spaces +3:4: Unexpected `3` extra spaces before table row, remove `3` spaces ``` -##### `not-ok-blockquote.md` +##### `blockquote.md` ###### In @@ -198,17 +194,17 @@ Paragraph. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown ->␠␠| A | ->␠| - | +>␠| Planet | +>␠␠| ------- | ``` ###### Out ```text -1:4: Do not indent table rows +2:4: Unexpected `1` extra space before table row, remove `1` space ``` -##### `not-ok-list.md` +##### `list.md` ###### In @@ -216,16 +212,14 @@ Paragraph. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown --␠␠␠paragraph - -␠␠␠␠␠| A | -␠␠␠␠| - | +*␠| Planet | +␠␠␠| ------- | ``` ###### Out ```text -3:6: Do not indent table rows +2:4: Unexpected `1` extra space before table row, remove `1` space ``` ## Compatibility diff --git a/packages/remark-lint-no-tabs/index.js b/packages/remark-lint-no-tabs/index.js index c2f2d16c..f2d3f7ba 100644 --- a/packages/remark-lint-no-tabs/index.js +++ b/packages/remark-lint-no-tabs/index.js @@ -77,42 +77,23 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * Foo Bar - * - * ␠␠␠␠Foo + * ␠␠␠␠mercury() * * @example - * {"name": "not-ok.md", "label": "input", "positionless": true} - * - * ␉Here's one before a code block. - * - * Here's a tab:␉, and here is another:␉. - * - * And this is in `inline␉code`. + * {"label": "input", "name": "not-ok.md", "positionless": true} * - * >␉This is in a block quote. - * - * *␉And… - * - * ␉1.␉in a list. - * - * And this is a tab as the last character.␉ + * ␉mercury() * + * Venus␉and Earth. * @example - * {"name": "not-ok.md", "label": "output"} - * - * 1:1: Use spaces instead of tabs - * 3:14: Use spaces instead of tabs - * 3:37: Use spaces instead of tabs - * 5:23: Use spaces instead of tabs - * 7:2: Use spaces instead of tabs - * 9:2: Use spaces instead of tabs - * 11:1: Use spaces instead of tabs - * 11:4: Use spaces instead of tabs - * 13:41: Use spaces instead of tabs + * {"label": "output", "name": "not-ok.md"} + * + * 1:1: Unexpected tab (`\t`), expected spaces + * 3:6: Unexpected tab (`\t`), expected spaces */ /** @@ -139,7 +120,10 @@ const remarkLintNoTabs = lintRule( let index = value.indexOf('\t') while (index !== -1) { - file.message('Use spaces instead of tabs', toPoint(index)) + file.message('Unexpected tab (`\\t`), expected spaces', { + place: toPoint(index) + }) + index = value.indexOf('\t', index + 1) } } diff --git a/packages/remark-lint-no-tabs/readme.md b/packages/remark-lint-no-tabs/readme.md index 4c7c9b7d..ed795a5b 100644 --- a/packages/remark-lint-no-tabs/readme.md +++ b/packages/remark-lint-no-tabs/readme.md @@ -183,9 +183,7 @@ uses spaces exclusively for indentation. ###### In ```markdown -Foo Bar - -␠␠␠␠Foo +␠␠␠␠mercury() ``` ###### Out @@ -197,33 +195,16 @@ No messages. ###### In ```markdown -␉Here's one before a code block. - -Here's a tab:␉, and here is another:␉. - -And this is in `inline␉code`. - ->␉This is in a block quote. - -*␉And… - -␉1.␉in a list. +␉mercury() -And this is a tab as the last character.␉ +Venus␉and Earth. ``` ###### Out ```text -1:1: Use spaces instead of tabs -3:14: Use spaces instead of tabs -3:37: Use spaces instead of tabs -5:23: Use spaces instead of tabs -7:2: Use spaces instead of tabs -9:2: Use spaces instead of tabs -11:1: Use spaces instead of tabs -11:4: Use spaces instead of tabs -13:41: Use spaces instead of tabs +1:1: Unexpected tab (`\t`), expected spaces +3:6: Unexpected tab (`\t`), expected spaces ``` ## Compatibility diff --git a/packages/remark-lint-no-undefined-references/index.js b/packages/remark-lint-no-undefined-references/index.js index da0a1037..3d43bd05 100644 --- a/packages/remark-lint-no-undefined-references/index.js +++ b/packages/remark-lint-no-undefined-references/index.js @@ -32,6 +32,8 @@ * * * `allow` (`Array`, optional) * — list of values to allow between `[` and `]` + * * `allowShortcutLink` (`boolean`, default: `false`) + * — allow shortcut references, which are just brackets such as `[text]` * * ## Recommendation * @@ -49,7 +51,7 @@ * but it might become one when an author later adds a definition: * * ```markdown - * Some new text […][] + * Some new text […][]. * * […]: #read-more * ``` @@ -65,116 +67,133 @@ * @author Titus Wormer * @copyright 2016 Titus Wormer * @license MIT - * @example - * {"name": "ok.md"} - * - * [foo][] - * - * Just a [ bracket. - * - * Typically, you’d want to use escapes (with a backslash: \\) to escape what - * could turn into a \[reference otherwise]. - * - * Just two braces can’t link: []. - * - * [foo]: https://example.com * * @example - * {"config": {"allow": ["…"]}, "name": "ok-allow.md"} + * {"name": "ok.md"} * - * > Eliding a portion of a quoted passage […] is acceptable. + * [Mercury][] is the first planet from the Sun and the smallest in the Solar + * System. * - * @example - * {"config": {"allow": ["a", {"source": "^b\\."}]}, "name": "ok-allow-source.md"} + * Venus is the second planet from the [Sun. * - * [foo][b.c] + * Earth is the third planet from the \[Sun] and the only astronomical object + * known to harbor life\. * - * [bar][a] + * Mars is the fourth planet from the Sun: []. * - * Matching is case-insensitive: [bar][B.C] + * [mercury]: https://example.com/mercury/ * * @example * {"label": "input", "name": "not-ok.md"} * - * [bar] + * [Mercury] is the first planet from the Sun and the smallest in the Solar + * System. * - * [baz][] + * [Venus][] is the second planet from the Sun. * - * [text][qux] + * [Earth][earth] is the third planet from the Sun and the only astronomical + * object known to harbor life. * - * Spread [over - * lines][] + * ![Mars] is the fourth planet from the Sun in the [Solar + * System]. * - * > in [a - * > block quote][] + * > Jupiter is the fifth planet from the Sun and the largest in the [Solar + * > System][]. * - * [asd][a + * [Saturn][ is the sixth planet from the Sun and the second-largest + * in the Solar System, after Jupiter. * - * Can include [*emphasis*]. + * [*Uranus*][] is the seventh planet from the Sun. * - * Multiple pairs: [a][b][c]. + * [Neptune][neptune][more] is the eighth and farthest planet from the Sun. * @example * {"label": "output", "name": "not-ok.md"} * - * 1:1-1:6: Found reference to undefined definition - * 3:1-3:8: Found reference to undefined definition - * 5:1-5:12: Found reference to undefined definition - * 7:8-8:9: Found reference to undefined definition - * 10:6-11:17: Found reference to undefined definition - * 13:1-13:6: Found reference to undefined definition - * 15:13-15:25: Found reference to undefined definition - * 17:17-17:23: Found reference to undefined definition - * 17:23-17:26: Found reference to undefined definition + * 1:1-1:10: Unexpected reference to undefined definition, expected corresponding definition (`mercury`) for a link or escaped opening bracket (`\[`) for regular text + * 4:1-4:10: Unexpected reference to undefined definition, expected corresponding definition (`venus`) for a link or escaped opening bracket (`\[`) for regular text + * 6:1-6:15: Unexpected reference to undefined definition, expected corresponding definition (`earth`) for a link or escaped opening bracket (`\[`) for regular text + * 9:2-9:8: Unexpected reference to undefined definition, expected corresponding definition (`mars`) for an image or escaped opening bracket (`\[`) for regular text + * 9:50-10:8: Unexpected reference to undefined definition, expected corresponding definition (`solar system`) for a link or escaped opening bracket (`\[`) for regular text + * 12:67-13:12: Unexpected reference to undefined definition, expected corresponding definition (`solar > system`) for a link or escaped opening bracket (`\[`) for regular text + * 15:1-15:9: Unexpected reference to undefined definition, expected corresponding definition (`saturn`) for a link or escaped opening bracket (`\[`) for regular text + * 18:1-18:13: Unexpected reference to undefined definition, expected corresponding definition (`*uranus*`) for a link or escaped opening bracket (`\[`) for regular text + * 20:1-20:19: Unexpected reference to undefined definition, expected corresponding definition (`neptune`) for a link or escaped opening bracket (`\[`) for regular text + * 20:19-20:25: Unexpected reference to undefined definition, expected corresponding definition (`more`) for a link or escaped opening bracket (`\[`) for regular text * * @example - * {"config": {"allow": ["a", {"source": "^b\\."}]}, "label": "input", "name": "not-ok-source.md"} + * {"config": {"allow": ["…"]}, "name": "ok-allow.md"} * - * [foo][a.c] + * Mercury is the first planet from the Sun and the smallest in the Solar + * System. […] * - * [bar][b] * @example - * {"config": {"allow": ["a", {"source": "^b\\."}]}, "label": "output", "name": "not-ok-source.md"} + * {"config": {"allow": [{"source": "^mer"}, "venus"]}, "name": "source.md"} + * + * [Mercury][] is the first planet from the Sun and the smallest in the Solar + * System. * - * 1:1-1:11: Found reference to undefined definition - * 3:1-3:9: Found reference to undefined definition + * [Venus][] is the second planet from the Sun. * * @example * {"gfm": true, "label": "input", "name": "gfm.md"} * - * GFM footnote calls are supported too. + * Mercury[^mercury] is the first planet from the Sun and the smallest in the + * Solar System. * - * Alpha[^a] + * [^venus]: + * **Venus** is the second planet from the Sun. * @example * {"gfm": true, "label": "output", "name": "gfm.md"} * - * 3:6-3:10: Found reference to undefined definition + * 1:8-1:18: Unexpected reference to undefined definition, expected corresponding definition (`mercury`) for a footnote or escaped opening bracket (`\[`) for regular text + * + * @example + * {"config": {"allowShortcutLink": true}, "label": "input", "name": "allow-shortcut-link.md"} + * + * [Mercury] is the first planet from the Sun and the smallest in the Solar + * System. + * + * [Venus][] is the second planet from the Sun. + * + * [Earth][earth] is the third planet from the Sun and the only astronomical object + * known to harbor life. + * @example + * {"config": {"allowShortcutLink": true}, "label": "output", "name": "allow-shortcut-link.md"} + * + * 4:1-4:10: Unexpected reference to undefined definition, expected corresponding definition (`venus`) for a link or escaped opening bracket (`\[`) for regular text + * 6:1-6:15: Unexpected reference to undefined definition, expected corresponding definition (`earth`) for a link or escaped opening bracket (`\[`) for regular text */ /** - * @typedef {import('mdast').Heading} Heading - * @typedef {import('mdast').Paragraph} Paragraph + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root */ /** * @typedef Options * Configuration. - * @property {ReadonlyArray<{source: string} | RegExp | string> | null | undefined} [allow] - * Text or regexes to allow between `[` and `]` even though they’re not - * defined (optional). + * @property {ReadonlyArray | null | undefined} [allow] + * List of values to allow between `[` and `]` (optional) + * @property {boolean | null | undefined} [allowShortcutLink] + * Allow shortcut references, which are just brackets such as `[text]` + * (`boolean`, default: `false`) */ +import {collapseWhiteSpace} from 'collapse-white-space' +import {ok as assert} from 'devlop' import {normalizeIdentifier} from 'micromark-util-normalize-identifier' import {lintRule} from 'unified-lint-rule' -import {pointEnd, pointStart, position} from 'unist-util-position' -import {EXIT, SKIP, visit} from 'unist-util-visit' +import {pointEnd, pointStart} from 'unist-util-position' +import {visitParents} from 'unist-util-visit-parents' import {location} from 'vfile-location' /** @type {Readonly} */ const emptyOptions = {} -/** @type {ReadonlyArray<{source: string} | RegExp | string>} */ +/** @type {ReadonlyArray} */ const emptyAllow = [] +const lineEndingExpression = /(\r?\n|\r)[\t ]*(>[\t ]*)*/g + const remarkLintNoUndefinedReferences = lintRule( { origin: 'remark-lint:no-undefined-references', @@ -191,15 +210,19 @@ const remarkLintNoUndefinedReferences = lintRule( function (tree, file, options) { const settings = options || emptyOptions const allow = settings.allow || emptyAllow - const contents = String(file) - const loc = location(file) - const lineEnding = /(\r?\n|\r)[\t ]*(>[\t ]*)*/g + const allowShortcutLink = settings.allowShortcutLink || false + const value = String(file) + const toPoint = location(file).toPoint + /** @type {Set} */ + const definitionIdentifiers = new Set() /** @type {Set} */ - const defined = new Set() + const footnoteDefinitionIdentifiers = new Set() /** @type {Array} */ const regexes = [] /** @type {Set} */ const strings = new Set() + /** @type {Array>} */ + const phrasingStacks = [] let index = -1 @@ -208,215 +231,229 @@ const remarkLintNoUndefinedReferences = lintRule( if (typeof value === 'string') { strings.add(normalizeIdentifier(value)) - } else if (value instanceof RegExp) { - regexes.push(value) - } else { - regexes.push(new RegExp(value.source, 'i')) + } else if (typeof value === 'object' && 'source' in value) { + regexes.push(new RegExp(value.source, value.flags ?? 'i')) } } - visit(tree, function (node) { - if (node.type === 'definition' || node.type === 'footnoteDefinition') { - defined.add(normalizeIdentifier(node.identifier)) + visitParents(tree, function (node, parents) { + if (node.type === 'definition') { + definitionIdentifiers.add(normalizeIdentifier(node.identifier)) } - }) - visit(tree, function (node) { - const place = position(node) - - /* c8 ignore next 12 -- CM specifies that references only form when - * defined. - * Still, they could be added by plugins, so let’s keep it. */ - if ( - (node.type === 'imageReference' || - node.type === 'linkReference' || - node.type === 'footnoteReference') && - place && - !defined.has(normalizeIdentifier(node.identifier)) && - !allowed(node.identifier) - ) { - file.message('Found reference to undefined definition', place) + if (node.type === 'footnoteDefinition') { + footnoteDefinitionIdentifiers.add(normalizeIdentifier(node.identifier)) } if (node.type === 'heading' || node.type === 'paragraph') { - findInPhrasing(node) - return SKIP + phrasingStacks.push([...parents, node]) } }) + for (const ancestors of phrasingStacks) { + findInPhrasingContainer(ancestors) + } + /** - * @param {Heading | Paragraph} node - * Node. + * @param {Array} ancestors + * Ancestors, the last of which a parent of phrasing nodes. * @returns {undefined} * Nothing. */ - function findInPhrasing(node) { - /** @type {Array>} */ - let ranges = [] - - visit(node, function (child) { - // Ignore the node itself. - if (child === node) return - - // Can’t have links in links, so reset ranges. - if (child.type === 'link' || child.type === 'linkReference') { - ranges = [] - return SKIP + function findInPhrasingContainer(ancestors) { + /** @type {Array<[ancestors: Array, brackets: Array]>} */ + const bracketRanges = [] + const node = ancestors.at(-1) + assert(node) // Always defined. + assert('children' in node) // Always defined. + + for (const child of node.children) { + if (child.type === 'text') { + findRangesInText(bracketRanges, [...ancestors, child]) + } else if ('children' in child) { + findInPhrasingContainer([...ancestors, child]) } + } - // Enter non-text. - if (child.type !== 'text') return + // Remaining ranges. + for (const range of bracketRanges) { + handleRange(range) + } + } - const start = pointStart(child) - const end = pointEnd(child) + /** + * @param {Array<[ancestors: Array, brackets: Array]>} ranges + * @param {Array} ancestors + */ + function findRangesInText(ranges, ancestors) { + const node = ancestors.at(-1) + assert(node) // Always defined. + const end = pointEnd(node) + const start = pointStart(node) - // Bail if there’s no positional info. - if ( - !start || - !end || - typeof start.offset !== 'number' || - typeof end.offset !== 'number' - ) { - return EXIT - } + // Bail if there’s no positional info. + if ( + !end || + !start || + typeof start.offset !== 'number' || + typeof end.offset !== 'number' + ) { + return + } - const source = contents.slice(start.offset, end.offset) - /** @type {Array<[number, string]>} */ - const lines = [[start.offset, '']] - let last = 0 + const source = value.slice(start.offset, end.offset) + /** @type {Array<[number, string]>} */ + const lines = [[start.offset, '']] + let last = 0 - lineEnding.lastIndex = 0 - let match = lineEnding.exec(source) + lineEndingExpression.lastIndex = 0 + let match = lineEndingExpression.exec(source) - while (match) { - const index = match.index - lines[lines.length - 1][1] = source.slice(last, index) - last = index + match[0].length - lines.push([start.offset + last, '']) - match = lineEnding.exec(source) - } + while (match) { + const index = match.index + const lineTuple = lines.at(-1) + assert(lineTuple) // Always defined. + lineTuple[1] = source.slice(last, index) - lines[lines.length - 1][1] = source.slice(last) - let lineIndex = -1 + last = index + match[0].length + lines.push([start.offset + last, '']) + match = lineEndingExpression.exec(source) + } - while (++lineIndex < lines.length) { - const line = lines[lineIndex][1] - let index = 0 + const lineTuple = lines.at(-1) + assert(lineTuple) // Always defined. + lineTuple[1] = source.slice(last) - while (index < line.length) { - const code = line.charCodeAt(index) + for (const lineTuple of lines) { + const [lineStart, line] = lineTuple + let index = 0 - // Skip past escaped brackets. - if (code === 92) { - const next = line.charCodeAt(index + 1) - index++ + while (index < line.length) { + const code = line.charCodeAt(index) + + // Opening bracket. + if (code === 91 /* `[` */) { + ranges.push([ancestors, [lineStart + index]]) + index++ + } + // Skip escaped brackets. + else if (code === 92 /* `\` */) { + const next = line.charCodeAt(index + 1) + + index++ - if (next === 91 || next === 93) { - index++ - } + if (next === 91 /* `[` */ || next === 93 /* `]` */) { + index++ } - // Opening bracket. - else if (code === 91) { - ranges.push([lines[lineIndex][0] + index]) + } + // Close bracket. + else if (code === 93 /* `]` */) { + const bracketInfo = ranges.at(-1) + + // No opening, ignore. + if (!bracketInfo) { index++ } - // Close bracket. - else if (code === 93) { - // No opening. - if (ranges.length === 0) { - index++ - } else if (line.charCodeAt(index + 1) === 91) { - index++ - - // Collapsed or full. - let range = ranges.pop() - - // Range should always exist. - if (range) { - range.push(lines[lineIndex][0] + index) - - // This is the end of a reference already. - if (range.length === 4) { - handleRange(range) - range = [] - } - - range.push(lines[lineIndex][0] + index) - ranges.push(range) - index++ - } - } else { - index++ - - // Shortcut or typical end of a reference. - const range = ranges.pop() - - // Range should always exist. - if (range) { - range.push(lines[lineIndex][0] + index) - handleRange(range) - } - } + // `][`. + else if ( + line.charCodeAt(index + 1) === 91 /* `[` */ && + // That would be the end of a reference already. + bracketInfo[1].length !== 3 + ) { + index++ + bracketInfo[1].push(lineStart + index, lineStart + index) + index++ } - // Anything else. + // `]` with earlier `[`. else { index++ + bracketInfo[1].push(lineStart + index) + handleRange(bracketInfo) + ranges.pop() } } - } - }) - - let index = -1 - - while (++index < ranges.length) { - handleRange(ranges[index]) - } - - /** - * @param {Array} range - * Range. - * @returns {undefined} - * Nothing. - */ - function handleRange(range) { - if (range.length === 1) return - if (range.length === 3) range.length = 2 - - // No need to warn for just `[]`. - if (range.length === 2 && range[0] + 2 === range[1]) return - - const offset = range.length === 4 && range[2] + 2 !== range[3] ? 2 : 0 - const id = contents - .slice(range[0 + offset] + 1, range[1 + offset] - 1) - .replace(lineEnding, ' ') - const start = loc.toPoint(range[0]) - const end = loc.toPoint(range[range.length - 1]) - - if ( - start && - end && - !defined.has(normalizeIdentifier(id)) && - !allowed(id) - ) { - file.message('Found reference to undefined definition', {start, end}) + // Anything else. + else { + index++ + } } } } /** - * @param {string} id - * Identifier. - * @returns {boolean} - * Whether `id` is allowed. + * @param {[ancestors: Array, brackets: Array]} bracketRange + * Info. + * @returns {undefined} + * Nothing. */ - function allowed(id) { - const normalized = normalizeIdentifier(id) - return ( - strings.has(normalized) || + function handleRange(bracketRange) { + const [ancestors, range] = bracketRange + + // `[`. + if (range.length === 1) return + + // `[x][`. + if (range.length === 3) range.length = 2 + + // No need to warn for just `[]`. + if (range.length === 2 && range[0] + 2 === range[1]) return + + const label = + value.charCodeAt(range[0] - 1) === 33 /* `!` */ + ? 'image' + : value.charCodeAt(range[0] + 1) === 94 /* `^` */ + ? 'footnote' + : 'link' + + const offset = range.length === 4 && range[2] + 2 !== range[3] ? 2 : 0 + + let id = normalizeIdentifier( + collapseWhiteSpace( + value.slice(range[0 + offset] + 1, range[1 + offset] - 1), + {style: 'html', trim: true} + ) + ) + let defined = definitionIdentifiers + + if (label === 'footnote') { + // Footnotes can’t have spaces. + /* c8 ignore next -- bit superfluous to test. */ + if (id.includes(' ')) return + + defined = footnoteDefinitionIdentifiers + // Drop the `^`. + id = id.slice(1) + } + + if ( + (allowShortcutLink && range.length === 2) || + defined.has(id) || + strings.has(id) || regexes.some(function (regex) { - return regex.test(normalized) + return regex.test(id) }) - ) + ) { + return + } + + const start = toPoint(range[0]) + const end = toPoint(range[range.length - 1]) + + if (end && start) { + file.message( + 'Unexpected reference to undefined definition, expected corresponding definition (`' + + id.toLowerCase() + + '`) for ' + + (label === 'image' ? 'an' : 'a') + + ' ' + + label + + ' or escaped opening bracket (`\\[`) for regular text', + { + ancestors, + place: {start, end} + } + ) + } } } ) diff --git a/packages/remark-lint-no-undefined-references/package.json b/packages/remark-lint-no-undefined-references/package.json index dbd57696..3f4f8cd8 100644 --- a/packages/remark-lint-no-undefined-references/package.json +++ b/packages/remark-lint-no-undefined-references/package.json @@ -34,10 +34,12 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", "micromark-util-normalize-identifier": "^2.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.0", "vfile-location": "^5.0.0" }, "scripts": {}, diff --git a/packages/remark-lint-no-undefined-references/readme.md b/packages/remark-lint-no-undefined-references/readme.md index 5ed52d9d..e14a1c3c 100644 --- a/packages/remark-lint-no-undefined-references/readme.md +++ b/packages/remark-lint-no-undefined-references/readme.md @@ -143,6 +143,8 @@ Configuration (TypeScript type). * `allow` (`Array`, optional) — list of values to allow between `[` and `]` +* `allowShortcutLink` (`boolean`, default: `false`) + — allow shortcut references, which are just brackets such as `[text]` ## Recommendation @@ -160,7 +162,7 @@ This isn’t a problem, but it might become one when an author later adds a definition: ```markdown -Some new text […][] +Some new text […][]. […]: #read-more ``` @@ -175,128 +177,139 @@ but their changes also result in a link for the text by the first author. ###### In ```markdown -[foo][] +[Mercury][] is the first planet from the Sun and the smallest in the Solar +System. -Just a [ bracket. +Venus is the second planet from the [Sun. -Typically, you’d want to use escapes (with a backslash: \\) to escape what -could turn into a \[reference otherwise]. +Earth is the third planet from the \[Sun] and the only astronomical object +known to harbor life\. -Just two braces can’t link: []. +Mars is the fourth planet from the Sun: []. -[foo]: https://example.com +[mercury]: https://example.com/mercury/ ``` ###### Out No messages. -##### `ok-allow.md` - -When configured with `{ allow: [ '…' ] }`. +##### `not-ok.md` ###### In ```markdown -> Eliding a portion of a quoted passage […] is acceptable. -``` +[Mercury] is the first planet from the Sun and the smallest in the Solar +System. -###### Out - -No messages. +[Venus][] is the second planet from the Sun. -##### `ok-allow-source.md` +[Earth][earth] is the third planet from the Sun and the only astronomical +object known to harbor life. -When configured with `{ allow: [ 'a', { source: '^b\\.' } ] }`. +![Mars] is the fourth planet from the Sun in the [Solar +System]. -###### In +> Jupiter is the fifth planet from the Sun and the largest in the [Solar +> System][]. -```markdown -[foo][b.c] +[Saturn][ is the sixth planet from the Sun and the second-largest +in the Solar System, after Jupiter. -[bar][a] +[*Uranus*][] is the seventh planet from the Sun. -Matching is case-insensitive: [bar][B.C] +[Neptune][neptune][more] is the eighth and farthest planet from the Sun. ``` ###### Out -No messages. +```text +1:1-1:10: Unexpected reference to undefined definition, expected corresponding definition (`mercury`) for a link or escaped opening bracket (`\[`) for regular text +4:1-4:10: Unexpected reference to undefined definition, expected corresponding definition (`venus`) for a link or escaped opening bracket (`\[`) for regular text +6:1-6:15: Unexpected reference to undefined definition, expected corresponding definition (`earth`) for a link or escaped opening bracket (`\[`) for regular text +9:2-9:8: Unexpected reference to undefined definition, expected corresponding definition (`mars`) for an image or escaped opening bracket (`\[`) for regular text +9:50-10:8: Unexpected reference to undefined definition, expected corresponding definition (`solar system`) for a link or escaped opening bracket (`\[`) for regular text +12:67-13:12: Unexpected reference to undefined definition, expected corresponding definition (`solar > system`) for a link or escaped opening bracket (`\[`) for regular text +15:1-15:9: Unexpected reference to undefined definition, expected corresponding definition (`saturn`) for a link or escaped opening bracket (`\[`) for regular text +18:1-18:13: Unexpected reference to undefined definition, expected corresponding definition (`*uranus*`) for a link or escaped opening bracket (`\[`) for regular text +20:1-20:19: Unexpected reference to undefined definition, expected corresponding definition (`neptune`) for a link or escaped opening bracket (`\[`) for regular text +20:19-20:25: Unexpected reference to undefined definition, expected corresponding definition (`more`) for a link or escaped opening bracket (`\[`) for regular text +``` -##### `not-ok.md` +##### `ok-allow.md` + +When configured with `{ allow: [ '…' ] }`. ###### In ```markdown -[bar] +Mercury is the first planet from the Sun and the smallest in the Solar +System. […] +``` -[baz][] +###### Out -[text][qux] +No messages. -Spread [over -lines][] +##### `source.md` -> in [a -> block quote][] +When configured with `{ allow: [ { source: '^mer' }, 'venus' ] }`. -[asd][a +###### In -Can include [*emphasis*]. +```markdown +[Mercury][] is the first planet from the Sun and the smallest in the Solar +System. -Multiple pairs: [a][b][c]. +[Venus][] is the second planet from the Sun. ``` ###### Out -```text -1:1-1:6: Found reference to undefined definition -3:1-3:8: Found reference to undefined definition -5:1-5:12: Found reference to undefined definition -7:8-8:9: Found reference to undefined definition -10:6-11:17: Found reference to undefined definition -13:1-13:6: Found reference to undefined definition -15:13-15:25: Found reference to undefined definition -17:17-17:23: Found reference to undefined definition -17:23-17:26: Found reference to undefined definition -``` - -##### `not-ok-source.md` +No messages. -When configured with `{ allow: [ 'a', { source: '^b\\.' } ] }`. +##### `gfm.md` ###### In +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + ```markdown -[foo][a.c] +Mercury[^mercury] is the first planet from the Sun and the smallest in the +Solar System. -[bar][b] +[^venus]: + **Venus** is the second planet from the Sun. ``` ###### Out ```text -1:1-1:11: Found reference to undefined definition -3:1-3:9: Found reference to undefined definition +1:8-1:18: Unexpected reference to undefined definition, expected corresponding definition (`mercury`) for a footnote or escaped opening bracket (`\[`) for regular text ``` -##### `gfm.md` +##### `allow-shortcut-link.md` -###### In +When configured with `{ allowShortcutLink: true }`. -> 👉 **Note**: this example uses -> GFM ([`remark-gfm`][github-remark-gfm]). +###### In ```markdown -GFM footnote calls are supported too. +[Mercury] is the first planet from the Sun and the smallest in the Solar +System. + +[Venus][] is the second planet from the Sun. -Alpha[^a] +[Earth][earth] is the third planet from the Sun and the only astronomical object +known to harbor life. ``` ###### Out ```text -3:6-3:10: Found reference to undefined definition +4:1-4:10: Unexpected reference to undefined definition, expected corresponding definition (`venus`) for a link or escaped opening bracket (`\[`) for regular text +6:1-6:15: Unexpected reference to undefined definition, expected corresponding definition (`earth`) for a link or escaped opening bracket (`\[`) for regular text ``` ## Compatibility diff --git a/packages/remark-lint-no-unneeded-full-reference-image/index.js b/packages/remark-lint-no-unneeded-full-reference-image/index.js index a12815b2..6a20b9e8 100644 --- a/packages/remark-lint-no-unneeded-full-reference-image/index.js +++ b/packages/remark-lint-no-unneeded-full-reference-image/index.js @@ -35,34 +35,53 @@ * @author Titus Wormer * @copyright 2019 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * ![alpha][] - * ![Bravo][] - * ![Charlie][delta] + * ![Mercury][] and ![Venus][venus-image]. * - * [alpha]: a - * [bravo]: b - * [delta]: d + * [mercury]: /mercury.png + * [venus-image]: /venus.png * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} + * + * ![Mercury][mercury]. * - * ![alpha][alpha] - * ![Bravo][bravo] - * ![charlie][Charlie] + * [mercury]: /mercury.png + * + * @example + * {"label": "output", "name": "not-ok.md"} * - * [alpha]: a - * [bravo]: b - * [charlie]: c + * 1:1-1:20: Unexpected full reference image (`![text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`![text][]`) * * @example - * {"name": "not-ok.md", "label": "output"} + * {"gfm": true, "label": "input", "name": "escape.md"} + * + * Matrix: + * + * | Kind | Text normal | Text escape | Text character reference | + * | ------------------------- | ----------- | ------------ | ------------------------ | + * | Label normal | ![&][&] | ![\&][&] | ![&][&] | + * | Label escape | ![&][\&] | ![\&][\&] | ![&][\&] | + * | Label character reference | ![&][&] | ![\&][&] | ![&][&] | + * + * When using the above matrix, the first row will go to `/a.png`, the second + * to `b`, third to `c`. + * Removing all labels, you’d instead get it per column: `/a.png`, `b`, `c`. + * That shows the label is not needed when it matches the text, and is otherwise. + * + * [&]: /a.png + * [\&]: /b.png + * [&]: /c.png * - * 1:1-1:16: Remove the image label as it matches the reference text - * 2:1-2:16: Remove the image label as it matches the reference text - * 3:1-3:20: Remove the image label as it matches the reference text + * @example + * {"label": "output", "name": "escape.md"} + * + * 5:31-5:38: Unexpected full reference image (`![text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`![text][]`) + * 6:45-6:54: Unexpected full reference image (`![text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`![text][]`) + * 7:60-7:75: Unexpected full reference image (`![text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`![text][]`) */ /** @@ -71,8 +90,8 @@ import {normalizeIdentifier} from 'micromark-util-normalize-identifier' import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {pointEnd, pointStart} from 'unist-util-position' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoUnneededFullReferenceImage = lintRule( { @@ -86,22 +105,39 @@ const remarkLintNoUnneededFullReferenceImage = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'imageReference', function (node) { - const place = position(node) + const value = String(file) + + visitParents(tree, 'imageReference', function (node, parents) { + const end = pointEnd(node) + const start = pointStart(node) if ( - !place || node.referenceType !== 'full' || - /* c8 ignore next -- generated AST can omit `alt`. */ - normalizeIdentifier(node.alt || '') !== node.identifier.toUpperCase() + !end || + !start || + typeof end.offset !== 'number' || + typeof start.offset !== 'number' ) { return } - file.message( - 'Remove the image label as it matches the reference text', - place - ) + const slice = value.slice(start.offset, end.offset) + // In a label, the `[` cannot occur unescaped. + const index = slice.lastIndexOf('][') + + /* c8 ignore next -- shouldn’t happen */ + if (index === -1) return + + // `2` for `![`. + const text = normalizeIdentifier(slice.slice(2, index)) + const label = node.identifier.toUpperCase() + + if (text === label) { + file.message( + 'Unexpected full reference image (`![text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`![text][]`)', + {ancestors: [...parents, node], place: node.position} + ) + } }) } ) diff --git a/packages/remark-lint-no-unneeded-full-reference-image/package.json b/packages/remark-lint-no-unneeded-full-reference-image/package.json index c15db82a..9c74fa01 100644 --- a/packages/remark-lint-no-unneeded-full-reference-image/package.json +++ b/packages/remark-lint-no-unneeded-full-reference-image/package.json @@ -37,7 +37,7 @@ "micromark-util-normalize-identifier": "^2.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-unneeded-full-reference-image/readme.md b/packages/remark-lint-no-unneeded-full-reference-image/readme.md index 2786f43d..6df8496b 100644 --- a/packages/remark-lint-no-unneeded-full-reference-image/readme.md +++ b/packages/remark-lint-no-unneeded-full-reference-image/readme.md @@ -140,13 +140,10 @@ the concise collapsed reference syntax (`![Alt][]`). ###### In ```markdown -![alpha][] -![Bravo][] -![Charlie][delta] +![Mercury][] and ![Venus][venus-image]. -[alpha]: a -[bravo]: b -[delta]: d +[mercury]: /mercury.png +[venus-image]: /venus.png ``` ###### Out @@ -158,21 +155,49 @@ No messages. ###### In ```markdown -![alpha][alpha] -![Bravo][bravo] -![charlie][Charlie] +![Mercury][mercury]. -[alpha]: a -[bravo]: b -[charlie]: c +[mercury]: /mercury.png ``` ###### Out ```text -1:1-1:16: Remove the image label as it matches the reference text -2:1-2:16: Remove the image label as it matches the reference text -3:1-3:20: Remove the image label as it matches the reference text +1:1-1:20: Unexpected full reference image (`![text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`![text][]`) +``` + +##### `escape.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Matrix: + +| Kind | Text normal | Text escape | Text character reference | +| ------------------------- | ----------- | ------------ | ------------------------ | +| Label normal | ![&][&] | ![\&][&] | ![&][&] | +| Label escape | ![&][\&] | ![\&][\&] | ![&][\&] | +| Label character reference | ![&][&] | ![\&][&] | ![&][&] | + +When using the above matrix, the first row will go to `/a.png`, the second +to `b`, third to `c`. +Removing all labels, you’d instead get it per column: `/a.png`, `b`, `c`. +That shows the label is not needed when it matches the text, and is otherwise. + +[&]: /a.png +[\&]: /b.png +[&]: /c.png +``` + +###### Out + +```text +5:31-5:38: Unexpected full reference image (`![text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`![text][]`) +6:45-6:54: Unexpected full reference image (`![text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`![text][]`) +7:60-7:75: Unexpected full reference image (`![text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`![text][]`) ``` ## Compatibility @@ -244,6 +269,8 @@ abide by its terms. [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c +[github-remark-gfm]: https://github.com/remarkjs/remark-gfm + [github-remark-lint]: https://github.com/remarkjs/remark-lint [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer diff --git a/packages/remark-lint-no-unneeded-full-reference-link/index.js b/packages/remark-lint-no-unneeded-full-reference-link/index.js index c4d324aa..5ad48081 100644 --- a/packages/remark-lint-no-unneeded-full-reference-link/index.js +++ b/packages/remark-lint-no-unneeded-full-reference-link/index.js @@ -35,40 +35,53 @@ * @author Titus Wormer * @copyright 2019 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * [alpha][] - * [Bravo][] - * [Charlie][delta] + * [Mercury][] and [Venus][venus-url]. + * + * [mercury]: https://example.com/mercury/ + * [venus-url]: https://example.com/venus/ + * + * @example + * {"label": "input", "name": "not-ok.md"} + * + * [Mercury][mercury]. + * + * [mercury]: https://example.com/mercury/ * - * This only works if the link text is a `text` node: - * [`echo`][] - * [*foxtrot*][] + * @example + * {"label": "output", "name": "not-ok.md"} * - * [alpha]: a - * [bravo]: b - * [delta]: d - * [`echo`]: e - * [*foxtrot*]: f + * 1:1-1:19: Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`) * * @example - * {"name": "not-ok.md", "label": "input"} + * {"gfm": true, "label": "input", "name": "escape.md"} + * + * Matrix: * - * [alpha][alpha] - * [Bravo][bravo] - * [charlie][Charlie] + * | Kind | Text normal | Text escape | Text character reference | + * | ------------------------- | ----------- | ------------ | ------------------------ | + * | Label normal | [&][&] | [\&][&] | [&][&] | + * | Label escape | [&][\&] | [\&][\&] | [&][\&] | + * | Label character reference | [&][&] | [\&][&] | [&][&] | * - * [alpha]: a - * [bravo]: b - * [charlie]: c + * When using the above matrix, the first row will go to `a`, the second + * to `b`, third to `c`. + * Removing all labels, you’d instead get it per column: `a`, `b`, `c`. + * That shows the label is not needed when it matches the text, and is otherwise. + * + * [&]: a + * [\&]: b + * [&]: c * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "escape.md"} * - * 1:1-1:15: Remove the link label as it matches the reference text - * 2:1-2:15: Remove the link label as it matches the reference text - * 3:1-3:19: Remove the link label as it matches the reference text + * 5:31-5:37: Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`) + * 6:45-6:53: Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`) + * 7:60-7:74: Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`) */ /** @@ -77,8 +90,8 @@ import {normalizeIdentifier} from 'micromark-util-normalize-identifier' import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {pointEnd, pointStart} from 'unist-util-position' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoUnneededFullReferenceLink = lintRule( { @@ -92,23 +105,39 @@ const remarkLintNoUnneededFullReferenceLink = lintRule( * Nothing. */ function (tree, file) { - visit(tree, 'linkReference', function (node) { - const place = position(node) + const value = String(file) + + visitParents(tree, 'linkReference', function (node, parents) { + const end = pointEnd(node) + const start = pointStart(node) + if ( - !place || node.referenceType !== 'full' || - node.children.length !== 1 || - node.children[0].type !== 'text' || - normalizeIdentifier(node.children[0].value) !== - node.identifier.toUpperCase() + !end || + !start || + typeof end.offset !== 'number' || + typeof start.offset !== 'number' ) { return } - file.message( - 'Remove the link label as it matches the reference text', - place - ) + const slice = value.slice(start.offset, end.offset) + // In a label, the `[` cannot occur unescaped. + const index = slice.lastIndexOf('][') + + /* c8 ignore next -- shouldn’t happen */ + if (index === -1) return + + // `1` for `[`. + const text = normalizeIdentifier(slice.slice(1, index)) + const label = node.identifier.toUpperCase() + + if (text === label) { + file.message( + 'Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`)', + {ancestors: [...parents, node], place: node.position} + ) + } }) } ) diff --git a/packages/remark-lint-no-unneeded-full-reference-link/package.json b/packages/remark-lint-no-unneeded-full-reference-link/package.json index 188cac7d..2b970dd2 100644 --- a/packages/remark-lint-no-unneeded-full-reference-link/package.json +++ b/packages/remark-lint-no-unneeded-full-reference-link/package.json @@ -37,7 +37,7 @@ "micromark-util-normalize-identifier": "^2.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-no-unneeded-full-reference-link/readme.md b/packages/remark-lint-no-unneeded-full-reference-link/readme.md index 9b67ec1c..a0c956d5 100644 --- a/packages/remark-lint-no-unneeded-full-reference-link/readme.md +++ b/packages/remark-lint-no-unneeded-full-reference-link/readme.md @@ -140,19 +140,10 @@ the concise collapsed reference syntax (`[Text][]`). ###### In ```markdown -[alpha][] -[Bravo][] -[Charlie][delta] - -This only works if the link text is a `text` node: -[`echo`][] -[*foxtrot*][] - -[alpha]: a -[bravo]: b -[delta]: d -[`echo`]: e -[*foxtrot*]: f +[Mercury][] and [Venus][venus-url]. + +[mercury]: https://example.com/mercury/ +[venus-url]: https://example.com/venus/ ``` ###### Out @@ -164,21 +155,49 @@ No messages. ###### In ```markdown -[alpha][alpha] -[Bravo][bravo] -[charlie][Charlie] +[Mercury][mercury]. + +[mercury]: https://example.com/mercury/ +``` + +###### Out + +```text +1:1-1:19: Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`) +``` + +##### `escape.md` + +###### In -[alpha]: a -[bravo]: b -[charlie]: c +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Matrix: + +| Kind | Text normal | Text escape | Text character reference | +| ------------------------- | ----------- | ------------ | ------------------------ | +| Label normal | [&][&] | [\&][&] | [&][&] | +| Label escape | [&][\&] | [\&][\&] | [&][\&] | +| Label character reference | [&][&] | [\&][&] | [&][&] | + +When using the above matrix, the first row will go to `a`, the second +to `b`, third to `c`. +Removing all labels, you’d instead get it per column: `a`, `b`, `c`. +That shows the label is not needed when it matches the text, and is otherwise. + +[&]: a +[\&]: b +[&]: c ``` ###### Out ```text -1:1-1:15: Remove the link label as it matches the reference text -2:1-2:15: Remove the link label as it matches the reference text -3:1-3:19: Remove the link label as it matches the reference text +5:31-5:37: Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`) +6:45-6:53: Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`) +7:60-7:74: Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`) ``` ## Compatibility @@ -250,6 +269,8 @@ abide by its terms. [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c +[github-remark-gfm]: https://github.com/remarkjs/remark-gfm + [github-remark-lint]: https://github.com/remarkjs/remark-lint [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer diff --git a/packages/remark-lint-no-unused-definitions/index.js b/packages/remark-lint-no-unused-definitions/index.js index 875b8e5c..6ed3ae22 100644 --- a/packages/remark-lint-no-unused-definitions/index.js +++ b/packages/remark-lint-no-unused-definitions/index.js @@ -37,43 +37,45 @@ * @example * {"name": "ok.md"} * - * [foo][] + * [Mercury][] * - * [foo]: https://example.com + * [mercury]: https://example.com/mercury/ * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * [bar]: https://example.com + * [mercury]: https://example.com/mercury/ * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 1:1-1:27: Found unused definition + * 1:1-1:40: Unexpected unused definition, expected no definition or one or more references to `mercury` * * @example * {"gfm": true, "label": "input", "name": "gfm.md"} * - * a[^x]. - * - * [^x]: ok - * [^y]: not ok + * Mercury[^mercury] is a planet. * + * [^Mercury]: + * **Mercury** is the first planet from the Sun and the smallest + * in the Solar System. + * [^Venus]: + * **Venus** is the second planet from + * the Sun. * @example * {"gfm": true, "label": "output", "name": "gfm.md"} * - * 4:1-4:13: Found unused footnote definition + * 6:1-8:13: Unexpected unused footnote definition, expected no definition or one or more footnote references to `venus` */ /** - * @typedef {import('mdast').Definition} Definition - * @typedef {import('mdast').FootnoteDefinition} FootnoteDefinition + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root */ +import {ok as assert} from 'devlop' import {lintRule} from 'unified-lint-rule' -import {position} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' const remarkLintNoUnusedDefinitions = lintRule( { @@ -87,28 +89,27 @@ const remarkLintNoUnusedDefinitions = lintRule( * Nothing. */ function (tree, file) { - /** @type {Map} */ + /** @type {Map | undefined, used: boolean}>} */ const footnoteDefinitions = new Map() - /** @type {Map} */ + /** @type {Map | undefined, used: boolean}>} */ const definitions = new Map() - visit(tree, function (node) { + visitParents(tree, function (node, parents) { if ('identifier' in node) { - const id = node.identifier.toLowerCase() const map = node.type === 'footnoteDefinition' || node.type === 'footnoteReference' ? footnoteDefinitions : definitions - let entry = map.get(id) + let entry = map.get(node.identifier) if (!entry) { - entry = {node: undefined, used: false} - map.set(id, entry) + entry = {ancestors: undefined, used: false} + map.set(node.identifier, entry) } if (node.type === 'definition' || node.type === 'footnoteDefinition') { - entry.node = node + entry.ancestors = [...parents, node] } else if ( node.type === 'imageReference' || node.type === 'linkReference' || @@ -119,19 +120,29 @@ const remarkLintNoUnusedDefinitions = lintRule( } }) - for (const entry of footnoteDefinitions.values()) { - const place = position(entry.node) + const entries = [...footnoteDefinitions.values(), ...definitions.values()] - if (place && !entry.used) { - file.message('Found unused footnote definition', place) - } - } + for (const entry of entries) { + if (!entry.used) { + assert(entry.ancestors) // Always defined if `used`. + const node = entry.ancestors.at(-1) + assert(node) // Always defined. + assert(node.type === 'footnoteDefinition' || node.type === 'definition') // Always definition. - for (const entry of definitions.values()) { - const place = position(entry.node) + if (node.position) { + const prefix = node.type === 'footnoteDefinition' ? 'footnote ' : '' - if (place && !entry.used) { - file.message('Found unused definition', place) + file.message( + 'Unexpected unused ' + + prefix + + 'definition, expected no definition or one or more ' + + prefix + + 'references to `' + + node.identifier + + '`', + {ancestors: entry.ancestors, place: node.position} + ) + } } } } diff --git a/packages/remark-lint-no-unused-definitions/package.json b/packages/remark-lint-no-unused-definitions/package.json index 13250582..b0ab1c37 100644 --- a/packages/remark-lint-no-unused-definitions/package.json +++ b/packages/remark-lint-no-unused-definitions/package.json @@ -31,8 +31,8 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", "unified-lint-rule": "^2.0.0", - "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0" }, "scripts": {}, diff --git a/packages/remark-lint-no-unused-definitions/readme.md b/packages/remark-lint-no-unused-definitions/readme.md index 8119f847..c0f9d09b 100644 --- a/packages/remark-lint-no-unused-definitions/readme.md +++ b/packages/remark-lint-no-unused-definitions/readme.md @@ -143,9 +143,9 @@ Unused definitions do not contribute anything, so they can be removed. ###### In ```markdown -[foo][] +[Mercury][] -[foo]: https://example.com +[mercury]: https://example.com/mercury/ ``` ###### Out @@ -157,13 +157,13 @@ No messages. ###### In ```markdown -[bar]: https://example.com +[mercury]: https://example.com/mercury/ ``` ###### Out ```text -1:1-1:27: Found unused definition +1:1-1:40: Unexpected unused definition, expected no definition or one or more references to `mercury` ``` ##### `gfm.md` @@ -174,16 +174,20 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -a[^x]. - -[^x]: ok -[^y]: not ok +Mercury[^mercury] is a planet. + +[^Mercury]: + **Mercury** is the first planet from the Sun and the smallest + in the Solar System. +[^Venus]: + **Venus** is the second planet from + the Sun. ``` ###### Out ```text -4:1-4:13: Found unused footnote definition +6:1-8:13: Unexpected unused footnote definition, expected no definition or one or more footnote references to `venus` ``` ## Compatibility diff --git a/packages/remark-lint-ordered-list-marker-style/index.js b/packages/remark-lint-ordered-list-marker-style/index.js index c063aeb7..eb4b05d7 100644 --- a/packages/remark-lint-ordered-list-marker-style/index.js +++ b/packages/remark-lint-ordered-list-marker-style/index.js @@ -25,24 +25,24 @@ * * Transform ([`Transformer` from `unified`][github-unified-transformer]). * - * ### `Marker` + * ### `Options` * - * Marker (TypeScript type). + * Configuration (TypeScript type). * * ###### Type * * ```ts - * type Marker = '.' | ')' + * type Options = Style | 'consistent' * ``` * - * ### `Options` + * ### `Style` * - * Configuration (TypeScript type). + * Style (TypeScript type). * * ###### Type * * ```ts - * type Options = Marker | 'consistent' + * type Style = '.' | ')' * ``` * * ## Recommendation @@ -58,7 +58,7 @@ * dots by default. * Pass `bulletOrdered: ')'` to always use parens. * - * [api-marker]: #marker + * [api-style]: #style * [api-options]: #options * [api-remark-lint-ordered-list-marker-style]: #unifieduseremarklintorderedlistmarkerstyle-options * [github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify @@ -68,48 +68,42 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * 1. Foo - * + * 1. Mercury * - * 1. Bar + * * Venus * - * Unordered lists are not affected by this rule. - * - * * Foo + * 1. Earth * * @example * {"name": "ok.md", "config": "."} * - * 1. Foo - * - * 2. Bar + * 1. Mercury * * @example * {"name": "ok.md", "config": ")"} * - * 1) Foo - * - * 2) Bar + * 1) Mercury * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * 1. Foo + * 1. Mercury * - * 2) Bar + * 1) Venus * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 3:1-3:8: Marker style should be `.` + * 3:2: Unexpected ordered list marker `)`, expected `.` * * @example - * {"name": "not-ok.md", "label": "output", "config": "💩", "positionless": true} + * {"name": "not-ok.md", "label": "output", "config": "🌍", "positionless": true} * - * 1:1: Incorrect ordered list item marker style `💩`: use either `'.'` or `')'` + * 1:1: Unexpected value `🌍` for `options`, expected `'.'`, `')'`, or `'consistent'` */ /** @@ -117,16 +111,18 @@ */ /** - * @typedef {Marker | 'consistent'} Options + * @typedef {Style | 'consistent'} Options * Configuration. * - * @typedef {'.' | ')'} Marker + * @typedef {'.' | ')'} Style * Style. */ +import {asciiDigit} from 'micromark-util-character' import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintOrderedListMarkerStyle = lintRule( { @@ -143,44 +139,75 @@ const remarkLintOrderedListMarkerStyle = lintRule( */ function (tree, file, options) { const value = String(file) - let option = options || 'consistent' + /** @type {Style | undefined} */ + let expected + /** @type {VFileMessage | undefined} */ + let cause - if (option !== 'consistent' && option !== '.' && option !== ')') { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (options === '.' || options === ')') { + expected = options + } else { file.fail( - 'Incorrect ordered list item marker style `' + - option + - "`: use either `'.'` or `')'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'.'`, `')'`, or `'consistent'`" ) } - visit(tree, 'list', function (node) { - let index = -1 + visitParents(tree, 'listItem', function (node, parents) { + const parent = parents.at(-1) - if (!node.ordered) return + if (!parent || parent.type !== 'list' || !parent.ordered) return - while (++index < node.children.length) { - const child = node.children[index] - const end = pointStart(child.children[0]) - const start = pointStart(child) + const start = pointStart(node) - if ( - end && - start && - typeof end.offset === 'number' && - typeof start.offset === 'number' - ) { - const marker = /** @type {Marker} */ ( - value - .slice(start.offset, end.offset) - .replace(/\s|\d/g, '') - .replace(/\[[x ]?]\s*$/i, '') - ) + if (start && typeof start.offset === 'number') { + let index = start.offset + let code = value.charCodeAt(index) + while (asciiDigit(code)) { + index++ + code = value.charCodeAt(index) + } + + /* c8 ignore next 2 -- weird ASTs. */ + const actual = + code === 41 /* `)` */ ? ')' : code === 46 /* `.` */ ? '.' : undefined - if (option === 'consistent') { - option = marker - } else if (marker !== option) { - file.message('Marker style should be `' + option + '`', child) + /* c8 ignore next -- weird ASTs. */ + if (!actual) return + + const place = { + line: start.line, + column: start.column + (index - start.offset), + offset: start.offset + (index - start.offset) + } + + if (expected) { + if (actual !== expected) { + file.message( + 'Unexpected ordered list marker `' + + actual + + '`, expected `' + + expected + + '`', + {ancestors: [...parents, node], cause, place} + ) } + } else { + expected = actual + cause = new VFileMessage( + 'Ordered list marker style `' + + expected + + "` first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place, + ruleId: 'ordered-list-marker-style', + source: 'remark-lint' + } + ) } } }) diff --git a/packages/remark-lint-ordered-list-marker-style/package.json b/packages/remark-lint-ordered-list-marker-style/package.json index 3c732a8e..e0ea1680 100644 --- a/packages/remark-lint-ordered-list-marker-style/package.json +++ b/packages/remark-lint-ordered-list-marker-style/package.json @@ -33,9 +33,11 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "micromark-util-character": "^2.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { @@ -48,6 +50,7 @@ "prettier": true, "rules": { "capitalized-comments": "off", + "unicorn/prefer-code-point": "off", "unicorn/prefer-string-replace-all": "off" } } diff --git a/packages/remark-lint-ordered-list-marker-style/readme.md b/packages/remark-lint-ordered-list-marker-style/readme.md index ee1a6437..1af0ace8 100644 --- a/packages/remark-lint-ordered-list-marker-style/readme.md +++ b/packages/remark-lint-ordered-list-marker-style/readme.md @@ -21,8 +21,8 @@ * [Use](#use) * [API](#api) * [`unified().use(remarkLintOrderedListMarkerStyle[, options])`](#unifieduseremarklintorderedlistmarkerstyle-options) - * [`Marker`](#marker) * [`Options`](#options) + * [`Style`](#style) * [Recommendation](#recommendation) * [Fix](#fix) * [Examples](#examples) @@ -122,8 +122,8 @@ On the CLI in a config file (here a `package.json`): This package exports no identifiers. It exports the [TypeScript][typescript] types -[`Marker`][api-marker] and -[`Options`][api-options]. +[`Options`][api-options] and +[`Style`][api-style]. The default export is [`remarkLintOrderedListMarkerStyle`][api-remark-lint-ordered-list-marker-style]. @@ -141,24 +141,24 @@ Warn when ordered list markers are inconsistent. Transform ([`Transformer` from `unified`][github-unified-transformer]). -### `Marker` +### `Options` -Marker (TypeScript type). +Configuration (TypeScript type). ###### Type ```ts -type Marker = '.' | ')' +type Options = Style | 'consistent' ``` -### `Options` +### `Style` -Configuration (TypeScript type). +Style (TypeScript type). ###### Type ```ts -type Options = Marker | 'consistent' +type Style = '.' | ')' ``` ## Recommendation @@ -181,14 +181,11 @@ Pass `bulletOrdered: ')'` to always use parens. ###### In ```markdown -1. Foo - +1. Mercury -1. Bar +* Venus -Unordered lists are not affected by this rule. - -* Foo +1. Earth ``` ###### Out @@ -202,9 +199,7 @@ When configured with `'.'`. ###### In ```markdown -1. Foo - -2. Bar +1. Mercury ``` ###### Out @@ -218,9 +213,7 @@ When configured with `')'`. ###### In ```markdown -1) Foo - -2) Bar +1) Mercury ``` ###### Out @@ -232,25 +225,25 @@ No messages. ###### In ```markdown -1. Foo +1. Mercury -2) Bar +1) Venus ``` ###### Out ```text -3:1-3:8: Marker style should be `.` +3:2: Unexpected ordered list marker `)`, expected `.` ``` ##### `not-ok.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect ordered list item marker style `💩`: use either `'.'` or `')'` +1:1: Unexpected value `🌍` for `options`, expected `'.'`, `')'`, or `'consistent'` ``` ## Compatibility @@ -278,12 +271,12 @@ abide by its terms. [MIT][file-license] © [Titus Wormer][author] -[api-marker]: #marker - [api-options]: #options [api-remark-lint-ordered-list-marker-style]: #unifieduseremarklintorderedlistmarkerstyle-options +[api-style]: #style + [author]: https://wooorm.com [badge-build-image]: https://github.com/remarkjs/remark-lint/workflows/main/badge.svg diff --git a/packages/remark-lint-ordered-list-marker-value/index.js b/packages/remark-lint-ordered-list-marker-value/index.js index dac7f7ec..539c4eaf 100644 --- a/packages/remark-lint-ordered-list-marker-value/index.js +++ b/packages/remark-lint-ordered-list-marker-value/index.js @@ -28,17 +28,30 @@ * * Configuration (TypeScript type). * + * `consistent` looks at the first list with two or more items, and + * infer `'single'` if both are the same, and `'ordered'` otherwise. + * + * ###### Type + * + * ```ts + * type Options = Style | 'consistent' + * ``` + * + * ### `Style` + * + * Counter style (TypeScript type). + * + * * `'one'` + * — values should always be exactly `1` * * `'ordered'` * — values should increment by one from the first item * * `'single'` * — values should stay the same as the first item - * * `'one'` - * — values should always be exactly `1` * * ###### Type * * ```ts - * type Options = 'one' | 'ordered' | 'single' + * type Style = 'one' | 'ordered' | 'single' * ``` * * ## Recommendation @@ -61,6 +74,7 @@ * Pass `incrementListMarker: false` to not increment further items. * * [api-options]: #options + * [api-style]: #style * [api-remark-lint-ordered-list-marker-value]: #unifieduseremarklintorderedlistmarkervalue-options * [github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify * [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer @@ -69,128 +83,165 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md"} * - * The default value is `ordered`, so unless changed, the below - * is OK. - * - * 1. Foo - * 2. Bar - * 3. Baz + * 1. Mercury + * 2. Venus * - * Paragraph. + * *** * - * 3. Alpha - * 4. Bravo - * 5. Charlie + * 3. Earth + * 4. Mars * - * Unordered lists are not affected by this rule. + * *** * - * * Anton + * * Jupiter * * @example - * {"name": "ok.md", "config": "one"} + * {"name": "ok-infer-single.md"} * - * 1. Foo - * 1. Bar - * 1. Baz + * 2. Mercury + * 2. Venus * - * Paragraph. + * *** * - * 1. Alpha - * 1. Bravo - * 1. Charlie + * 3. Earth + * 3. Mars * * @example - * {"name": "ok.md", "config": "single"} + * {"label": "input", "name": "nok-chaotic.md"} * - * 1. Foo - * 1. Bar - * 1. Baz + * 2. Mercury + * 1. Venus * - * Paragraph. + * *** * - * 3. Alpha - * 3. Bravo - * 3. Charlie + * 1. Earth + * 1. Mars + * @example + * {"label": "output", "name": "nok-chaotic.md"} + * + * 2:2: Unexpected ordered list item value `1`, expected `3` + * 7:2: Unexpected ordered list item value `1`, expected `2` * - * Paragraph. + * @example + * {"config": "one", "name": "ok.md"} * - * 0. Delta - * 0. Echo - * 0. Foxtrot + * 1. Mercury + * 1. Venus * * @example * {"name": "ok.md", "config": "ordered"} * - * 1. Foo - * 2. Bar - * 3. Baz + * 1. Mercury + * 2. Venus * - * Paragraph. + * *** * - * 3. Alpha - * 4. Bravo - * 5. Charlie + * 3. Earth + * 4. Mars * - * Paragraph. + * *** * - * 0. Delta - * 1. Echo - * 2. Foxtrot + * 0. Jupiter + * 1. Saturn * * @example - * {"name": "not-ok.md", "config": "one", "label": "input"} + * {"config": "single", "name": "ok.md"} + * + * 1. Mercury + * 1. Venus + * + * *** + * + * 3. Earth + * 3. Mars + * + * *** * - * 1. Foo - * 2. Bar + * 0. Jupiter + * 0. Saturn * * @example - * {"name": "not-ok.md", "config": "one", "label": "output"} + * {"config": "one", "label": "input", "name": "not-ok.md"} * - * 2:1-2:8: Marker should be `1`, was `2` + * 1. Mercury + * 2. Venus * + * *** + * + * 3. Earth + * + * *** + * + * 2. Mars + * 1. Jupiter * @example - * {"name": "also-not-ok.md", "config": "one", "label": "input"} + * {"config": "one", "label": "output", "name": "not-ok.md"} * - * 2. Foo - * 1. Bar + * 2:2: Unexpected ordered list item value `2`, expected `1` + * 6:2: Unexpected ordered list item value `3`, expected `1` + * 10:2: Unexpected ordered list item value `2`, expected `1` * * @example - * {"name": "also-not-ok.md", "config": "one", "label": "output"} + * {"config": "ordered", "label": "input", "name": "not-ok.md"} + * + * 1. Mercury + * 1. Venus * - * 1:1-1:8: Marker should be `1`, was `2` + * *** * + * 2. Mars + * 1. Jupiter * @example - * {"name": "not-ok.md", "config": "ordered", "label": "input"} + * {"config": "ordered", "label": "output", "name": "not-ok.md"} * - * 1. Foo - * 1. Bar + * 2:2: Unexpected ordered list item value `1`, expected `2` + * 7:2: Unexpected ordered list item value `1`, expected `3` * * @example - * {"name": "not-ok.md", "config": "ordered", "label": "output"} + * {"config": "single", "label": "input", "name": "not-ok.md"} * - * 2:1-2:8: Marker should be `2`, was `1` + * 1. Mercury + * 2. Venus * + * *** + * + * 2. Mars + * 1. Jupiter * @example - * {"name": "not-ok.md", "config": "💩", "label": "output", "positionless": true} + * {"config": "single", "label": "output", "name": "not-ok.md"} + * + * 2:2: Unexpected ordered list item value `2`, expected `1` + * 7:2: Unexpected ordered list item value `1`, expected `2` * - * 1:1: Incorrect ordered list item marker value `💩`: use either `'ordered'`, `'one'`, or `'single'` + * @example + * {"name": "not-ok.md", "config": "🌍", "label": "output", "positionless": true} + * + * 1:1: Unexpected value `🌍` for `options`, expected `'one'`, `'ordered'`, `'single'`, or `'consistent'` */ /** + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root */ /** - * @typedef {'one' | 'ordered' | 'single'} Options + * @typedef {Style | 'consistent'} Options * Configuration. - */ + * @typedef {'one' | 'ordered' | 'single'} Style + * Counter style. +*/ + +import {ok as assert} from 'devlop' +import {asciiDigit} from 'micromark-util-character' import {lintRule} from 'unified-lint-rule' -import {visit} from 'unist-util-visit' import {pointStart} from 'unist-util-position' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintOrderedListMarkerValue = lintRule( { @@ -200,66 +251,156 @@ const remarkLintOrderedListMarkerValue = lintRule( /** * @param {Root} tree * Tree. - * @param {Options | null | undefined} [options='ordered'] + * @param {Options | null | undefined} [options='consistent'] * Configuration (default: `'ordered'`). * @returns {undefined} * Nothing. */ function (tree, file, options) { const value = String(file) - const option = options || 'ordered' + /** @type {Style | undefined} */ + let style + /** @type {VFileMessage | undefined} */ + let cause - if (option !== 'one' && option !== 'ordered' && option !== 'single') { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if ( + options === 'one' || + options === 'ordered' || + options === 'single' + ) { + style = options + } else { file.fail( - 'Incorrect ordered list item marker value `' + - option + - "`: use either `'ordered'`, `'one'`, or `'single'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'one'`, `'ordered'`, `'single'`, or `'consistent'`" ) } - visit(tree, 'list', function (node) { + /** @type {Array<{ancestors: Array, counters: Array}>} */ + const lists = [] + + visitParents(tree, 'list', function (node, parents) { if (!node.ordered) return - let expected = - option === 'one' || typeof node.start !== 'number' ? 1 : node.start - let index = -1 + /** @type {Array} */ + const values = [] + + for (const item of node.children) { + const start = pointStart(item) + /** @type {string | undefined} */ + let counter + + if (start && typeof start.offset === 'number') { + let index = start.offset + let code = value.charCodeAt(index) - while (++index < node.children.length) { - const child = node.children[index] - const end = pointStart(child.children[0]) - const start = pointStart(child) + while (asciiDigit(code)) { + index++ + code = value.charCodeAt(index) + } - // Ignore generated nodes, first items. - if ( - !end || - !start || - typeof end.offset !== 'number' || - typeof start.offset !== 'number' || - (index === 0 && option !== 'one') - ) { - continue + counter = value.slice(start.offset, index) } - // Increase the expected line number when in `ordered` mode. - if (option === 'ordered') { - expected++ + values.push(counter) + } + + lists.push({ancestors: [...parents, node], counters: values}) + }) + + // Infer style. + if (!style) { + for (const info of lists) { + // Could be `undefined` for short lists *or* w/o positional info. + const [first, second] = info.counters + + if (first && second) { + const inferredStyle = + second === String(Number(first) + 1) + ? 'ordered' + : second === first + ? 'single' + : undefined + + if (inferredStyle) { + const node = info.ancestors.at(-1) + assert(node) // Always defined. + assert(node.type === 'list') // Always list. + style = inferredStyle + cause = new VFileMessage( + 'Ordered list marker style `' + + style + + "` first defined for `'consistent'` here", + { + ancestors: info.ancestors, + place: node.position, + ruleId: 'ordered-list-marker-value', + source: 'remark-lint' + } + ) + } + + break } + } + } + + if (!style) { + style = 'ordered' + cause = new VFileMessage( + "Ordered list marker style `ordered` assumed for `'consistent'`", + {ruleId: 'ordered-list-marker-value', source: 'remark-lint'} + ) + } + + for (const info of lists) { + const startValue = style === 'one' ? 1 : info.counters[0] + const node = info.ancestors.at(-1) + assert(node) // Always defined. + assert(node.type === 'list') // Always list. + + // No positional info on first item. + if (!startValue) continue - const marker = Number( - value - .slice(start.offset, end.offset) - .replace(/[\s.)]/g, '') - .replace(/\[[x ]?]\s*$/i, '') + const start = Number(startValue) + let index = -1 + + while (++index < info.counters.length) { + const item = node.children[index] + const actual = info.counters[index] + if (!actual) continue + + const startPoint = pointStart(item) + assert(startPoint) // Always defined, we checked when we found items. + assert(typeof startPoint.offset === 'number') // Same. + + const expected = String( + style === 'one' ? 1 : style === 'single' ? start : start + index ) - if (marker !== expected) { + if (actual !== expected) { file.message( - 'Marker should be `' + expected + '`, was `' + marker + '`', - child + 'Unexpected ordered list item value `' + + actual + + '`, expected `' + + expected + + '`', + { + ancestors: [...info.ancestors, item], + cause, + place: { + line: startPoint.line, + column: startPoint.column + actual.length, + offset: startPoint.offset + actual.length + } + } ) } } - }) + } } ) diff --git a/packages/remark-lint-ordered-list-marker-value/package.json b/packages/remark-lint-ordered-list-marker-value/package.json index 25ff0f26..a9f3c0fe 100644 --- a/packages/remark-lint-ordered-list-marker-value/package.json +++ b/packages/remark-lint-ordered-list-marker-value/package.json @@ -34,9 +34,12 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { @@ -49,6 +52,8 @@ "prettier": true, "rules": { "capitalized-comments": "off", + "complexity": "off", + "unicorn/prefer-code-point": "off", "unicorn/prefer-string-replace-all": "off" } } diff --git a/packages/remark-lint-ordered-list-marker-value/readme.md b/packages/remark-lint-ordered-list-marker-value/readme.md index 404ec8ca..5706f8ca 100644 --- a/packages/remark-lint-ordered-list-marker-value/readme.md +++ b/packages/remark-lint-ordered-list-marker-value/readme.md @@ -22,6 +22,7 @@ * [API](#api) * [`unified().use(remarkLintOrderedListMarkerValue[, options])`](#unifieduseremarklintorderedlistmarkervalue-options) * [`Options`](#options) + * [`Style`](#style) * [Recommendation](#recommendation) * [Fix](#fix) * [Examples](#examples) @@ -118,8 +119,9 @@ On the CLI in a config file (here a `package.json`): ## API This package exports no identifiers. -It exports the [TypeScript][typescript] type -[`Options`][api-options]. +It exports the [TypeScript][typescript] types +[`Options`][api-options] and +[`Style`][api-style]. The default export is [`remarkLintOrderedListMarkerValue`][api-remark-lint-ordered-list-marker-value]. @@ -140,17 +142,30 @@ Transform ([`Transformer` from `unified`][github-unified-transformer]). Configuration (TypeScript type). +`consistent` looks at the first list with two or more items, and +infer `'single'` if both are the same, and `'ordered'` otherwise. + +###### Type + +```ts +type Options = Style | 'consistent' +``` + +### `Style` + +Counter style (TypeScript type). + +* `'one'` + — values should always be exactly `1` * `'ordered'` — values should increment by one from the first item * `'single'` — values should stay the same as the first item -* `'one'` - — values should always be exactly `1` ###### Type ```ts -type Options = 'one' | 'ordered' | 'single' +type Style = 'one' | 'ordered' | 'single' ``` ## Recommendation @@ -179,28 +194,62 @@ Pass `incrementListMarker: false` to not increment further items. ###### In ```markdown -The default value is `ordered`, so unless changed, the below -is OK. +1. Mercury +2. Venus + +*** + +3. Earth +4. Mars -1. Foo -2. Bar -3. Baz +*** -Paragraph. +* Jupiter +``` + +###### Out + +No messages. + +##### `ok-infer-single.md` + +###### In -3. Alpha -4. Bravo -5. Charlie +```markdown +2. Mercury +2. Venus -Unordered lists are not affected by this rule. +*** -* Anton +3. Earth +3. Mars ``` ###### Out No messages. +##### `nok-chaotic.md` + +###### In + +```markdown +2. Mercury +1. Venus + +*** + +1. Earth +1. Mars +``` + +###### Out + +```text +2:2: Unexpected ordered list item value `1`, expected `3` +7:2: Unexpected ordered list item value `1`, expected `2` +``` + ##### `ok.md` When configured with `'one'`. @@ -208,15 +257,8 @@ When configured with `'one'`. ###### In ```markdown -1. Foo -1. Bar -1. Baz - -Paragraph. - -1. Alpha -1. Bravo -1. Charlie +1. Mercury +1. Venus ``` ###### Out @@ -225,26 +267,23 @@ No messages. ##### `ok.md` -When configured with `'single'`. +When configured with `'ordered'`. ###### In ```markdown -1. Foo -1. Bar -1. Baz +1. Mercury +2. Venus -Paragraph. +*** -3. Alpha -3. Bravo -3. Charlie +3. Earth +4. Mars -Paragraph. +*** -0. Delta -0. Echo -0. Foxtrot +0. Jupiter +1. Saturn ``` ###### Out @@ -253,26 +292,23 @@ No messages. ##### `ok.md` -When configured with `'ordered'`. +When configured with `'single'`. ###### In ```markdown -1. Foo -2. Bar -3. Baz +1. Mercury +1. Venus -Paragraph. +*** -3. Alpha -4. Bravo -5. Charlie +3. Earth +3. Mars -Paragraph. +*** -0. Delta -1. Echo -2. Foxtrot +0. Jupiter +0. Saturn ``` ###### Out @@ -286,58 +322,81 @@ When configured with `'one'`. ###### In ```markdown -1. Foo -2. Bar +1. Mercury +2. Venus + +*** + +3. Earth + +*** + +2. Mars +1. Jupiter ``` ###### Out ```text -2:1-2:8: Marker should be `1`, was `2` +2:2: Unexpected ordered list item value `2`, expected `1` +6:2: Unexpected ordered list item value `3`, expected `1` +10:2: Unexpected ordered list item value `2`, expected `1` ``` -##### `also-not-ok.md` +##### `not-ok.md` -When configured with `'one'`. +When configured with `'ordered'`. ###### In ```markdown -2. Foo -1. Bar +1. Mercury +1. Venus + +*** + +2. Mars +1. Jupiter ``` ###### Out ```text -1:1-1:8: Marker should be `1`, was `2` +2:2: Unexpected ordered list item value `1`, expected `2` +7:2: Unexpected ordered list item value `1`, expected `3` ``` ##### `not-ok.md` -When configured with `'ordered'`. +When configured with `'single'`. ###### In ```markdown -1. Foo -1. Bar +1. Mercury +2. Venus + +*** + +2. Mars +1. Jupiter ``` ###### Out ```text -2:1-2:8: Marker should be `2`, was `1` +2:2: Unexpected ordered list item value `2`, expected `1` +7:2: Unexpected ordered list item value `1`, expected `2` ``` ##### `not-ok.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect ordered list item marker value `💩`: use either `'ordered'`, `'one'`, or `'single'` +1:1: Unexpected value `🌍` for `options`, expected `'one'`, `'ordered'`, `'single'`, or `'consistent'` ``` ## Compatibility @@ -369,6 +428,8 @@ abide by its terms. [api-remark-lint-ordered-list-marker-value]: #unifieduseremarklintorderedlistmarkervalue-options +[api-style]: #style + [author]: https://wooorm.com [badge-build-image]: https://github.com/remarkjs/remark-lint/workflows/main/badge.svg diff --git a/packages/remark-lint-rule-style/index.js b/packages/remark-lint-rule-style/index.js index a61ba984..4d3c03ea 100644 --- a/packages/remark-lint-rule-style/index.js +++ b/packages/remark-lint-rule-style/index.js @@ -67,36 +67,36 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example - * {"name": "ok.md", "config": "* * *"} + * {"config": "* * *", "name": "ok.md"} * * * * * * * * * * * * @example - * {"name": "ok.md", "config": "_______"} + * {"config": "_______", "name": "ok.md"} * * _______ * * _______ * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * * *** * * * * * - * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 3:1-3:6: Rules should use `***` + * 3:1-3:6: Unexpected thematic rule `* * *`, expected `***` * * @example - * {"name": "not-ok.md", "label": "output", "config": "💩", "positionless": true} + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} * - * 1:1: Incorrect preferred rule style: provide a correct markdown rule or `'consistent'` + * 1:1: Unexpected value `🌍` for `options`, expected thematic rule or `'consistent'` */ /** @@ -110,7 +110,8 @@ import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintRuleStyle = lintRule( { @@ -127,15 +128,29 @@ const remarkLintRuleStyle = lintRule( */ function (tree, file, options) { const value = String(file) - let option = options || 'consistent' + /** @type {string | undefined} */ + let expected + /** @type {VFileMessage | undefined} */ + let cause - if (option !== 'consistent' && /[^-_* ]/.test(option)) { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if ( + /[^-_* ]/.test(options) || + options.at(0) === ' ' || + options.at(-1) === ' ' || + options.replaceAll(' ', '').length < 3 + ) { file.fail( - "Incorrect preferred rule style: provide a correct markdown rule or `'consistent'`" + 'Unexpected value `' + + options + + "` for `options`, expected thematic rule or `'consistent'`" ) + } else { + expected = options } - visit(tree, 'thematicBreak', function (node) { + visitParents(tree, 'thematicBreak', function (node, parents) { const end = pointEnd(node) const start = pointStart(node) @@ -145,12 +160,33 @@ const remarkLintRuleStyle = lintRule( typeof start.offset === 'number' && typeof end.offset === 'number' ) { - const rule = value.slice(start.offset, end.offset) + const place = {start, end} + const actual = value.slice(start.offset, end.offset) - if (option === 'consistent') { - option = rule - } else if (rule !== option) { - file.message('Rules should use `' + option + '`', node) + if (expected) { + if (actual !== expected) { + file.message( + 'Unexpected thematic rule `' + + actual + + '`, expected `' + + expected + + '`', + {ancestors: [...parents, node], cause, place} + ) + } + } else { + expected = actual + cause = new VFileMessage( + 'Thematic rule style `' + + expected + + "` first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place, + ruleId: 'rule-style', + source: 'remark-lint' + } + ) } } }) diff --git a/packages/remark-lint-rule-style/package.json b/packages/remark-lint-rule-style/package.json index 3c662a11..b15e1ffb 100644 --- a/packages/remark-lint-rule-style/package.json +++ b/packages/remark-lint-rule-style/package.json @@ -35,7 +35,8 @@ "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-rule-style/readme.md b/packages/remark-lint-rule-style/readme.md index 876db724..bbc435c9 100644 --- a/packages/remark-lint-rule-style/readme.md +++ b/packages/remark-lint-rule-style/readme.md @@ -218,17 +218,17 @@ No messages. ###### Out ```text -3:1-3:6: Rules should use `***` +3:1-3:6: Unexpected thematic rule `* * *`, expected `***` ``` ##### `not-ok.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect preferred rule style: provide a correct markdown rule or `'consistent'` +1:1: Unexpected value `🌍` for `options`, expected thematic rule or `'consistent'` ``` ## Compatibility diff --git a/packages/remark-lint-strikethrough-marker/index.js b/packages/remark-lint-strikethrough-marker/index.js index 7f860071..f3089498 100644 --- a/packages/remark-lint-strikethrough-marker/index.js +++ b/packages/remark-lint-strikethrough-marker/index.js @@ -70,51 +70,48 @@ * @author Denis Augsburger * @copyright 2021 Denis Augsburger * @license MIT - * @example - * {"config": "~", "name": "ok.md", "gfm": true} * - * ~foo~ + * @example + * {"gfm": true, "label": "input", "name": "not-ok.md"} * + * ~Mercury~Venus and ~~Earth~~Mars. * @example - * {"config": "~", "name": "not-ok.md", "label": "input", "gfm": true} + * {"gfm": true, "label": "output", "name": "not-ok.md"} * - * ~~foo~~ + * 1:20-1:29: Unexpected double tilde strikethrough sequences (`~~`), expected single tilde (`~`) * * @example - * {"config": "~", "name": "not-ok.md", "label": "output", "gfm": true} + * {"config": "~", "gfm": true, "name": "ok.md"} * - * 1:1-1:8: Strikethrough should use `~` as a marker + * ~Mercury~Venus. * * @example - * {"config": "~~", "name": "ok.md", "gfm": true} - * - * ~~foo~~ + * {"config": "~", "gfm": true, "label": "input", "name": "not-ok.md"} * + * ~~Mercury~~Venus. * @example - * {"config": "~~", "name": "not-ok.md", "label": "input", "gfm": true} + * {"config": "~", "gfm": true, "label": "output", "name": "not-ok.md"} * - * ~foo~ + * 1:1-1:12: Unexpected double tilde strikethrough sequences (`~~`), expected single tilde (`~`) * * @example - * {"config": "~~", "name": "not-ok.md", "label": "output", "gfm": true} + * {"config": "~~", "gfm": true, "name": "ok.md"} * - * 1:1-1:6: Strikethrough should use `~~` as a marker + * ~~Mercury~~Venus. * * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} - * - * ~~foo~~ - * ~bar~ + * {"config": "~~", "gfm": true, "label": "input", "name": "not-ok.md"} * + * ~Mercury~Venus. * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} + * {"config": "~~", "gfm": true, "label": "output", "name": "not-ok.md"} * - * 2:1-2:6: Strikethrough should use `~~` as a marker + * 1:1-1:10: Unexpected single tilde strikethrough sequences (`~`), expected double tilde (`~~`) * * @example - * {"config": "💩", "name": "not-ok.md", "label": "output", "positionless": true, "gfm": true} + * {"config": "🌍", "name": "not-ok.md", "label": "output", "positionless": true, "gfm": true} * - * 1:1: Incorrect strikethrough marker `💩`: use either `'consistent'`, `'~'`, or `'~~'` + * 1:1: Unexpected value `🌍` for `options`, expected `'~~'`, `'~'`, or `'consistent'` */ /** @@ -131,7 +128,8 @@ import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintStrikethroughMarker = lintRule( { @@ -148,29 +146,58 @@ const remarkLintStrikethroughMarker = lintRule( */ function (tree, file, options) { const value = String(file) - let option = options || 'consistent' + /** @type {VFileMessage | undefined} */ + let cause + /** @type {Marker | undefined} */ + let expected - if (option !== '~' && option !== '~~' && option !== 'consistent') { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (options === '~~' || options === '~') { + expected = options + } else { file.fail( - 'Incorrect strikethrough marker `' + - option + - "`: use either `'consistent'`, `'~'`, or `'~~'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'~~'`, `'~'`, or `'consistent'`" ) } - visit(tree, 'delete', function (node) { + visitParents(tree, 'delete', function (node, parents) { const start = pointStart(node) if (start && typeof start.offset === 'number') { - const both = value.slice(start.offset, start.offset + 2) - const marker = both === '~~' ? '~~' : '~' + /* c8 ignore next -- Weird AST. */ + if (value.charAt(start.offset) !== '~') return + const actual = value.charAt(start.offset + 1) === '~' ? '~~' : '~' - if (option === 'consistent') { - option = marker - } else if (marker !== option) { - file.message( - 'Strikethrough should use `' + option + '` as a marker', - node + if (expected) { + if (actual !== expected) { + file.message( + 'Unexpected ' + + (actual === '~' ? 'single' : 'double') + + ' tilde strikethrough sequences (`' + + actual + + '`), expected ' + + (expected === '~' ? 'single' : 'double') + + ' tilde (`' + + expected + + '`)', + {ancestors: [...parents, node], cause, place: node.position} + ) + } + } else { + expected = actual + cause = new VFileMessage( + "Strikethrough sequence style `'" + + actual + + "'` first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place: node.position, + ruleId: 'strikethrough-marker', + source: 'remark-lint' + } ) } } diff --git a/packages/remark-lint-strikethrough-marker/package.json b/packages/remark-lint-strikethrough-marker/package.json index 01fb8507..3f3eb57e 100644 --- a/packages/remark-lint-strikethrough-marker/package.json +++ b/packages/remark-lint-strikethrough-marker/package.json @@ -34,7 +34,8 @@ "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-strikethrough-marker/readme.md b/packages/remark-lint-strikethrough-marker/readme.md index f07af217..bb682b82 100644 --- a/packages/remark-lint-strikethrough-marker/readme.md +++ b/packages/remark-lint-strikethrough-marker/readme.md @@ -171,9 +171,7 @@ It’s recommended to use two tildes. ## Examples -##### `ok.md` - -When configured with `'~'`. +##### `not-ok.md` ###### In @@ -181,14 +179,16 @@ When configured with `'~'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -~foo~ +~Mercury~Venus and ~~Earth~~Mars. ``` ###### Out -No messages. +```text +1:20-1:29: Unexpected double tilde strikethrough sequences (`~~`), expected single tilde (`~`) +``` -##### `not-ok.md` +##### `ok.md` When configured with `'~'`. @@ -198,18 +198,16 @@ When configured with `'~'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -~~foo~~ +~Mercury~Venus. ``` ###### Out -```text -1:1-1:8: Strikethrough should use `~` as a marker -``` +No messages. -##### `ok.md` +##### `not-ok.md` -When configured with `'~~'`. +When configured with `'~'`. ###### In @@ -217,14 +215,16 @@ When configured with `'~~'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -~~foo~~ +~~Mercury~~Venus. ``` ###### Out -No messages. +```text +1:1-1:12: Unexpected double tilde strikethrough sequences (`~~`), expected single tilde (`~`) +``` -##### `not-ok.md` +##### `ok.md` When configured with `'~~'`. @@ -234,41 +234,40 @@ When configured with `'~~'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -~foo~ +~~Mercury~~Venus. ``` ###### Out -```text -1:1-1:6: Strikethrough should use `~~` as a marker -``` +No messages. ##### `not-ok.md` +When configured with `'~~'`. + ###### In > 👉 **Note**: this example uses > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -~~foo~~ -~bar~ +~Mercury~Venus. ``` ###### Out ```text -2:1-2:6: Strikethrough should use `~~` as a marker +1:1-1:10: Unexpected single tilde strikethrough sequences (`~`), expected double tilde (`~~`) ``` ##### `not-ok.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect strikethrough marker `💩`: use either `'consistent'`, `'~'`, or `'~~'` +1:1: Unexpected value `🌍` for `options`, expected `'~~'`, `'~'`, or `'consistent'` ``` ## Compatibility diff --git a/packages/remark-lint-strong-marker/index.js b/packages/remark-lint-strong-marker/index.js index 55625208..54e5dc91 100644 --- a/packages/remark-lint-strong-marker/index.js +++ b/packages/remark-lint-strong-marker/index.js @@ -47,13 +47,13 @@ * * ## Recommendation * - * Whether asterisks or underscores are used affects how and whether emphasis + * Whether asterisks or underscores are used affects how and whether strong * works. * Underscores are sometimes used to represent normal underscores inside words, * so there are extra rules in markdown to support that. * Asterisks are not used in natural language, * so they don’t need these rules, - * and thus can form emphasis in more cases. + * and thus can form strong in more cases. * Asterisks can also be used as the marker of more constructs than underscores: * lists. * Due to having simpler parsing rules, @@ -77,40 +77,51 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * + * @example + * {"config": "*", "name": "ok-asterisk.md"} + * + * **Mercury**. + * * @example - * {"name": "ok.md"} + * {"config": "*", "label": "input", "name": "not-ok-asterisk.md"} * - * **foo** and **bar**. + * __Mercury__. * * @example - * {"name": "also-ok.md"} + * {"config": "*", "label": "output", "name": "not-ok-asterisk.md"} * - * __foo__ and __bar__. + * 1:1-1:12: Unexpected strong marker `_`, expected `*` * * @example - * {"name": "ok.md", "config": "*"} + * {"config": "_", "name": "ok-underscore.md"} * - * **foo**. + * __Mercury__. * * @example - * {"name": "ok.md", "config": "_"} + * {"config": "_", "label": "input", "name": "not-ok-underscore.md"} * - * __foo__. + * **Mercury**. * * @example - * {"name": "not-ok.md", "label": "input"} + * {"config": "_", "label": "output", "name": "not-ok-underscore.md"} * - * **foo** and __bar__. + * 1:1-1:12: Unexpected strong marker `*`, expected `_` * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "input", "name": "not-ok-consistent.md"} * - * 1:13-1:20: Strong should use `*` as a marker + * **Mercury** and __Venus__. * * @example - * {"name": "not-ok.md", "label": "output", "config": "💩", "positionless": true} + * {"label": "output", "name": "not-ok-consistent.md"} * - * 1:1: Incorrect strong marker `💩`: use either `'consistent'`, `'*'`, or `'_'` + * 1:17-1:26: Unexpected strong marker `_`, expected `*` + * + * @example + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} + * + * 1:1: Unexpected value `🌍` for `options`, expected `'*'`, `'_'`, or `'consistent'` */ /** @@ -127,7 +138,8 @@ import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintStrongMarker = lintRule( { @@ -144,26 +156,56 @@ const remarkLintStrongMarker = lintRule( */ function (tree, file, options) { const value = String(file) - let option = options || 'consistent' + /** @type {VFileMessage | undefined} */ + let cause + /** @type {Marker | undefined} */ + let expected - if (option !== '*' && option !== '_' && option !== 'consistent') { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (options === '*' || options === '_') { + expected = options + } else { file.fail( - 'Incorrect strong marker `' + - option + - "`: use either `'consistent'`, `'*'`, or `'_'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'*'`, `'_'`, or `'consistent'`" ) } - visit(tree, 'strong', function (node) { + visitParents(tree, 'strong', function (node, parents) { const start = pointStart(node) if (start && typeof start.offset === 'number') { - const marker = /** @type {Marker} */ (value.charAt(start.offset)) + const actual = value.charAt(start.offset) + + /* c8 ignore next -- should not happen. */ + if (actual !== '*' && actual !== '_') return - if (option === 'consistent') { - option = marker - } else if (marker !== option) { - file.message('Strong should use `' + option + '` as a marker', node) + if (expected) { + if (actual !== expected) { + file.message( + 'Unexpected strong marker `' + + actual + + '`, expected `' + + expected + + '`', + {ancestors: [...parents, node], cause, place: node.position} + ) + } + } else { + expected = actual + cause = new VFileMessage( + "Strong marker style `'" + + actual + + "'` first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place: node.position, + ruleId: 'strong-marker', + source: 'remark-lint' + } + ) } } }) diff --git a/packages/remark-lint-strong-marker/package.json b/packages/remark-lint-strong-marker/package.json index 70961e56..1808f53d 100644 --- a/packages/remark-lint-strong-marker/package.json +++ b/packages/remark-lint-strong-marker/package.json @@ -35,7 +35,8 @@ "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-strong-marker/readme.md b/packages/remark-lint-strong-marker/readme.md index 65a45ae6..c2cc6245 100644 --- a/packages/remark-lint-strong-marker/readme.md +++ b/packages/remark-lint-strong-marker/readme.md @@ -162,13 +162,13 @@ type Options = Marker | 'consistent' ## Recommendation -Whether asterisks or underscores are used affects how and whether emphasis +Whether asterisks or underscores are used affects how and whether strong works. Underscores are sometimes used to represent normal underscores inside words, so there are extra rules in markdown to support that. Asterisks are not used in natural language, so they don’t need these rules, -and thus can form emphasis in more cases. +and thus can form strong in more cases. Asterisks can also be used as the marker of more constructs than underscores: lists. Due to having simpler parsing rules, @@ -184,80 +184,88 @@ Pass `strong: '_'` to always use underscores. ## Examples -##### `ok.md` +##### `ok-asterisk.md` + +When configured with `'*'`. ###### In ```markdown -**foo** and **bar**. +**Mercury**. ``` ###### Out No messages. -##### `also-ok.md` +##### `not-ok-asterisk.md` + +When configured with `'*'`. ###### In ```markdown -__foo__ and __bar__. +__Mercury__. ``` ###### Out -No messages. +```text +1:1-1:12: Unexpected strong marker `_`, expected `*` +``` -##### `ok.md` +##### `ok-underscore.md` -When configured with `'*'`. +When configured with `'_'`. ###### In ```markdown -**foo**. +__Mercury__. ``` ###### Out No messages. -##### `ok.md` +##### `not-ok-underscore.md` When configured with `'_'`. ###### In ```markdown -__foo__. +**Mercury**. ``` ###### Out -No messages. +```text +1:1-1:12: Unexpected strong marker `*`, expected `_` +``` -##### `not-ok.md` +##### `not-ok-consistent.md` ###### In ```markdown -**foo** and __bar__. +**Mercury** and __Venus__. ``` ###### Out ```text -1:13-1:20: Strong should use `*` as a marker +1:17-1:26: Unexpected strong marker `_`, expected `*` ``` ##### `not-ok.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect strong marker `💩`: use either `'consistent'`, `'*'`, or `'_'` +1:1: Unexpected value `🌍` for `options`, expected `'*'`, `'_'`, or `'consistent'` ``` ## Compatibility diff --git a/packages/remark-lint-table-cell-padding/index.js b/packages/remark-lint-table-cell-padding/index.js index 8afbeeff..5d9279d3 100644 --- a/packages/remark-lint-table-cell-padding/index.js +++ b/packages/remark-lint-table-cell-padding/index.js @@ -74,159 +74,327 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * + * @example + * {"config": "padded", "gfm": true, "name": "ok.md"} + * + * | Planet | Symbol | Satellites | Mean anomaly (°) | + * | ------- | :----- | :--------: | ---------------: | + * | Mercury | ☿ | None | 174 796 | + * + * | Planet | Symbol | Satellites | Mean anomaly (°) | + * | - | :- | :-: | -: | + * | Venus | ♀ | None | 50 115 | + * + * @example + * {"config": "padded", "gfm": true, "label": "input", "name": "not-ok.md"} + * + * | Planet | + * | -------| + * | Mercury| + * + * |Planet | + * |------ | + * |Venus | + * + * | Planet | + * | ------ | + * | Venus | + * @example + * {"config": "padded", "gfm": true, "label": "output", "name": "not-ok.md"} + * + * 2:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 3:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 5:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 6:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 7:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 9:4: Unexpected `2` spaces between cell edge and content, expected `1` space, remove `1` space + * 9:12: Unexpected `2` spaces between cell content and edge, expected `1` space, remove `1` space + * 10:4: Unexpected `2` spaces between cell edge and content, expected `1` space, remove `1` space + * 10:12: Unexpected `2` spaces between cell content and edge, expected `1` space, remove `1` space + * 11:4: Unexpected `2` spaces between cell edge and content, expected `1` space, remove `1` space + * 11:12: Unexpected `3` spaces between cell content and edge, expected between `1` (unaligned) and `2` (aligned) spaces, remove between `1` and `2` spaces + * * @example - * {"name": "ok.md", "config": "padded", "gfm": true} + * {"config": "compact", "gfm": true, "name": "ok.md"} + * + * |Planet |Symbol|Satellites|Mean anomaly (°)| + * |-------|:-----|:--------:|---------------:| + * |Mercury|☿ | None | 174 796| * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | + * |Planet|Symbol|Satellites|Mean anomaly (°)| + * |-|:-|:-:|-:| + * |Venus|♀|None|50 115| * * @example - * {"name": "not-ok.md", "label": "input", "config": "padded", "gfm": true} + * {"config": "compact", "gfm": true, "label": "input", "name": "not-ok.md"} * - * | A | B | - * | :----|----: | - * | Alpha|Bravo | + * | Planet | + * | -------| + * | Mercury| * - * | C | D | - * | :----- | ---: | - * |Charlie | Delta| + * |Planet | + * |------ | + * |Venus | * - * Too much padding isn’t good either: + * | Planet | + * | ------ | + * | Venus | + * @example + * {"config": "compact", "gfm": true, "label": "output", "name": "not-ok.md"} + * + * 1:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 2:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 3:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 5:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 6:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 7:9: Unexpected `2` spaces between cell content and edge, expected between `0` (unaligned) and `1` (aligned) space, remove between `1` and `2` spaces + * 9:4: Unexpected `2` spaces between cell edge and content, expected `0` spaces, remove `2` spaces + * 9:12: Unexpected `2` spaces between cell content and edge, expected `0` spaces, remove `2` spaces + * 10:4: Unexpected `2` spaces between cell edge and content, expected `0` spaces, remove `2` spaces + * 10:12: Unexpected `2` spaces between cell content and edge, expected `0` spaces, remove `2` spaces + * 11:4: Unexpected `2` spaces between cell edge and content, expected `0` spaces, remove `2` spaces + * 11:12: Unexpected `3` spaces between cell content and edge, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces + * + * @example + * {"gfm": true, "name": "consistent-padded-ok.md"} * - * | E | F | G | H | - * | :---- | -------- | :----: | -----: | - * | Echo | Foxtrot | Golf | Hotel | + * | Planet | + * | - | * * @example - * {"name": "not-ok.md", "label": "output", "config": "padded", "gfm": true} - * - * 3:8: Cell should be padded - * 3:9: Cell should be padded - * 7:2: Cell should be padded - * 7:17: Cell should be padded - * 13:7: Cell should be padded with 1 space, not 2 - * 13:18: Cell should be padded with 1 space, not 2 - * 13:23: Cell should be padded with 1 space, not 2 - * 13:27: Cell should be padded with 1 space, not 2 - * 13:32: Cell should be padded with 1 space, not 2 + * {"gfm": true, "label": "input", "name": "consistent-padded-nok.md"} * + * | Planet| + * | - | * @example - * {"name": "ok.md", "config": "compact", "gfm": true} + * {"gfm": true, "label": "output", "name": "consistent-padded-nok.md"} * - * |A |B | - * |-----|-----| - * |Alpha|Bravo| + * 1:9: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space * * @example - * {"name": "not-ok.md", "label": "input", "config": "compact", "gfm": true} + * {"gfm": true, "name": "consistent-compact-ok.md"} * - * | A | B | - * | -----| -----| - * | Alpha| Bravo| + * |Planet| + * |-| * - * |C | D| - * |:------|-----:| - * |Charlie|Delta | + * @example + * {"gfm": true, "label": "input", "name": "consistent-compact-nok.md"} * + * |Planet | + * |-| * @example - * {"name": "not-ok.md", "label": "output", "config": "compact", "gfm": true} + * {"gfm": true, "label": "output", "name": "consistent-compact-nok.md"} * - * 3:5: Cell should be compact - * 3:12: Cell should be compact - * 7:15: Cell should be compact + * 1:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space * * @example - * {"name": "ok-padded.md", "gfm": true} + * {"gfm": true, "name": "empty.md"} * - * The default is `'consistent'`. + * | | Satellites | + * | - | - | + * | Mercury | | * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | + * @example + * {"gfm": true, "name": "missing-cells.md"} * - * | C | D | - * | ------- | ----- | - * | Charlie | Delta | + * | Planet | Symbol | Satellites | + * | - | - | - | + * | Mercury | + * | Venus | ♀ | + * | Earth | 🜨 and ♁ | 1 | + * | Mars | ♂ | 2 | 19 412 | * * @example - * {"name": "not-ok-padded.md", "label": "input", "config": "consistent", "gfm": true} + * {"config": "padded", "gfm": true, "label": "input", "name": "missing-fences.md"} * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | + * ␠Planet|Symbol|Satellites + * ------:|:-----|---------- + * Mercury|☿ |0 * - * | C | D | - * | :----- | ----: | - * |Charlie | Delta | + * Planet|Symbol + * -----:|------ + * ␠Venus|♀ + * @example + * {"config": "padded", "gfm": true, "label": "output", "name": "missing-fences.md"} + * + * 1:8: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 1:9: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 1:15: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 1:16: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 2:8: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 2:9: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 2:15: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 2:16: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 3:8: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 3:9: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 3:16: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 5:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 5:8: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 6:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 6:8: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 7:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 7:8: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space * * @example - * {"name": "not-ok-padded.md", "label": "output", "config": "consistent", "gfm": true} + * {"config": "compact", "gfm": true, "label": "input", "name": "missing-fences.md"} * - * 7:2: Cell should be padded + * Planet | Symbol | Satellites + * -: | - | - + * Mercury | ☿ | 0 + * + * Planet | Symbol + * -----: | ------ + * ␠Venus | ♀ + * @example + * {"config": "compact", "gfm": true, "label": "output", "name": "missing-fences.md"} + * + * 1:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 1:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 1:17: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 1:19: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 2:4: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 2:6: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 2:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 3:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 3:11: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 3:15: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 5:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 5:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 6:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 6:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 7:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 7:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space * * @example - * {"name": "ok-compact.md", "config": "consistent", "gfm": true} + * {"config": "compact", "gfm": true, "label": "input", "name": "trailing-spaces.md"} * - * |A |B | - * |-----|-----| - * |Alpha|Bravo| + * Planet | Symbol␠ + * -: | -␠ + * Mercury | ☿␠␠ * - * |C |D | - * |-------|-----| - * |Charlie|Delta| + * | Planet | Symbol |␠ + * | ------ | ------ |␠ + * | Venus | ♀ |␠␠ + * @example + * {"config": "compact", "gfm": true, "label": "output", "name": "trailing-spaces.md"} + * + * 1:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 1:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 2:4: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 2:6: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 3:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 3:11: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 5:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 5:10: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 5:12: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 5:19: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 6:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 6:10: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 6:12: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 6:19: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 7:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 7:10: Unexpected `2` spaces between cell content and edge, expected between `0` (unaligned) and `1` (aligned) space, remove between `1` and `2` spaces + * 7:12: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 7:19: Unexpected `6` spaces between cell content and edge, expected between `0` (unaligned) and `5` (aligned) spaces, remove between `1` and `6` spaces + * + * @example + * {"config": "compact", "gfm": true, "label": "input", "name": "nothing.md"} * + * | | | | + * | - | - | - | + * | | | | * @example - * {"name": "not-ok-compact.md", "label": "input", "config": "consistent", "gfm": true} + * {"config": "compact", "gfm": true, "label": "output", "name": "nothing.md"} + * + * 1:5: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces + * 1:9: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces + * 1:13: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces + * 2:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 2:5: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 2:7: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 2:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 2:11: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space + * 2:13: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space + * 3:5: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces + * 3:9: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces + * 3:13: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces * - * |A |B | - * |-----|-----| - * |Alpha|Bravo| + * @example + * {"config": "padded", "gfm": true, "label": "input", "name": "nothing.md"} * - * |C | D| - * |:------|-----:| - * |Charlie|Delta | + * |||| + * |-|-|-| + * |||| + * @example + * {"config": "padded", "gfm": true, "label": "output", "name": "nothing.md"} + * + * 1:2: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space + * 1:3: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space + * 1:4: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space + * 2:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 2:3: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 2:4: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 2:5: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 2:6: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 2:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 3:2: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space + * 3:3: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space + * 3:4: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space * * @example - * {"name": "not-ok-compact.md", "label": "output", "config": "consistent", "gfm": true} + * {"config": "padded", "gfm": true, "label": "input", "name": "more-weirdness.md"} * - * 7:15: Cell should be compact + * Mercury + * |- * + * Venus + * -| * @example - * {"name": "not-ok.md", "label": "output", "config": "💩", "positionless": true, "gfm": true} + * {"config": "padded", "gfm": true, "label": "output", "name": "more-weirdness.md"} * - * 1:1: Incorrect table cell padding style `💩`, expected `'padded'`, `'compact'`, or `'consistent'` + * 2:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space + * 5:2: Unexpected `0` spaces between cell content and edge, expected between `1` (unaligned) and `5` (aligned) spaces, add between `5` and `1` space * * @example - * {"name": "empty.md", "label": "input", "config": "padded", "gfm": true} + * {"config": "padded", "gfm": true, "label": "input", "name": "containers.md"} * - * + * > | Mercury| + * > | - | * - * | | Alpha | Bravo| - * | ------ | ----- | ---: | - * | Charlie| | Echo| + * * | Venus| + * | - | * + * > * > | Earth| + * > > | - | * @example - * {"name": "empty.md", "label": "output", "config": "padded", "gfm": true} + * {"config": "padded", "gfm": true, "label": "output", "name": "containers.md"} + * + * 1:12: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 4:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space + * 7:14: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space * - * 3:25: Cell should be padded - * 5:10: Cell should be padded - * 5:25: Cell should be padded + * @example + * {"config": "padded", "gfm": true, "label": "input", "name": "windows.md"} * + * | Mercury|␍␊| --- |␍␊| None | * @example - * {"name": "missing-body.md", "config": "padded", "gfm": true} + * {"config": "padded", "gfm": true, "label": "output", "name": "windows.md"} + * + * 1:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space * - * + * @example + * {"config": "🌍", "gfm": true, "label": "output", "name": "not-ok.md", "positionless": true} * - * | Alpha | Bravo | Charlie | - * | ----- | ------- | ------- | - * | Delta | - * | Echo | Foxtrot | + * 1:1: Unexpected value `🌍` for `options`, expected `'compact'`, `'padded'`, or `'consistent'` */ /** + * @typedef {import('mdast').AlignType} Align + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root - * @typedef {import('mdast').TableCell} TableCell + * + * @typedef {import('unist').Point} Point */ /** @@ -237,9 +405,12 @@ * Styles. */ +import {ok as assert} from 'devlop' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {SKIP, visit} from 'unist-util-visit' +import {SKIP, visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintTableCellPadding = lintRule( { @@ -255,165 +426,416 @@ const remarkLintTableCellPadding = lintRule( * Nothing. */ function (tree, file, options) { - const option = options || 'consistent' + /** + * @typedef Entry + * @property {Align} align + * @property {Array} ancestors + * @property {number} column + * @property {Size | undefined} size + * + * @typedef Size + * @property {number | undefined} left + * @property {Point} leftPoint + * @property {number} middle + * @property {number | undefined} right + * @property {Point} rightPoint + */ - if ( - option !== 'compact' && - option !== 'consistent' && - option !== 'padded' - ) { + const value = String(file) + /** @type {Style | undefined} */ + let expected + /** @type {VFileMessage | undefined} */ + let cause + + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (options === 'compact' || options === 'padded') { + expected = options + } else { file.fail( - 'Incorrect table cell padding style `' + - option + - "`, expected `'padded'`, `'compact'`, or `'consistent'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'compact'`, `'padded'`, or `'consistent'`" ) } - visit(tree, 'table', function (node) { - const rows = node.children - /* c8 ignore next -- generated AST can omit `align`. */ - const align = node.align || [] + visitParents(tree, 'table', function (table, parents) { + const entries = inferTable([...parents, table]) + + // Find max column sizes. /** @type {Array} */ const sizes = [] - /** @type {Array<{column: number, end: number, node: TableCell, start: number}>} */ - const entries = [] - let index = -1 - - // Check align row. - // Because there’s zero to two `:`, and there must be one `-`. - while (++index < align.length) { - const alignment = align[index] - sizes[index] = alignment === 'center' ? 3 : alignment ? 2 : 1 - } - index = -1 - - // Check rows. - while (++index < rows.length) { - const row = rows[index] - let column = -1 - - // Check fences (before, between, and after cells). - while (++column < row.children.length) { - const cell = row.children[column] - const cellStart = pointStart(cell)?.offset - const cellEnd = pointEnd(cell)?.offset - const contentStart = pointStart(cell.children[0])?.offset - const contentEnd = pointEnd( - cell.children[cell.children.length - 1] - )?.offset + for (const entry of entries) { + if ( + entry.size && + (sizes[entry.column] === undefined || + entry.size.middle > sizes[entry.column]) + ) { + sizes[entry.column] = entry.size.middle + } + } + // Find the first cell that is the biggest in its column. + if (!expected) { + for (const info of entries) { if ( - typeof cellStart !== 'number' || - typeof cellEnd !== 'number' || - typeof contentStart !== 'number' || - typeof contentEnd !== 'number' + info.size && + info.size.middle && + info.size.middle === sizes[info.column] ) { - continue + const node = info.ancestors.at(-1) + assert(node) // Always defined. + expected = info.size.left ? 'padded' : 'compact' + cause = new VFileMessage( + "Cell padding style `'" + + expected + + "'` first defined for `'consistent'` here", + { + ancestors: info.ancestors, + place: node.position, + ruleId: 'table-cell-padding', + source: 'remark-lint' + } + ) } - - entries.push({ - node: cell, - start: contentStart - cellStart - 1, - end: - cellEnd - - contentEnd - - (column === row.children.length - 1 ? 1 : 0), - column - }) - - // Detect max space per column. - sizes[column] = Math.max( - /* c8 ignore next */ - sizes[column] || 0, - contentEnd - contentStart - ) } } - const style = - option === 'consistent' - ? entries[0] && (!entries[0].start || !entries[0].end) - ? 0 - : 1 - : option === 'padded' - ? 1 - : 0 + /* c8 ignore next -- always a cell. */ + if (!expected) return - index = -1 - - while (++index < entries.length) { - checkSide('start', entries[index], style, sizes) - checkSide('end', entries[index], style, sizes) + for (const info of entries) { + checkSide('left', info, sizes) + checkSide('right', info, sizes) } + // No tables in tables. return SKIP }) /** - * @param {'end' | 'start'} side + * @param {'left' | 'right'} side * Side to check. - * @param {{column: number, end: number, node: TableCell, start: number}} entry - * Cell info. - * @param {0 | 1} style - * Expected style. + * @param {Entry} info + * Info. * @param {Array} sizes - * Max sizes per column. + * Max column sizes. * @returns {undefined} * Nothing. */ - function checkSide(side, entry, style, sizes) { - const cell = entry.node - const column = entry.column - const spacing = entry[side] + function checkSide(side, info, sizes) { + if (!info.size) { + return + } + + const actual = info.size[side] - if (spacing === undefined || spacing === style) { + if (actual === undefined) { return } - let reason = 'Cell should be ' + const alignSpaces = sizes[info.column] - info.size.middle + const min = expected === 'compact' ? 0 : 1 + /** @type {number} */ + let max = min + + if (info.align === 'center') { + max += Math.ceil(alignSpaces / 2) + } else if (info.align === 'right' ? side === 'left' : side === 'right') { + max += alignSpaces + } + + // For empty cells, + // the `left` field is used for all the whitespace in them. + if (info.size.middle === 0) { + if (side === 'right') return + max = Math.max(max, sizes[info.column] + 2 * min) + } + + if (actual < min || actual > max) { + const differenceMin = min - actual + const differenceMinAbsolute = Math.abs(differenceMin) + const differenceMax = max - actual + const differenceMaxAbsolute = Math.abs(differenceMax) + + file.message( + 'Unexpected `' + + actual + + '` ' + + pluralize('space', actual) + + ' between cell ' + + (side === 'left' ? 'edge' : 'content') + + ' and ' + + (side === 'left' ? 'content' : 'edge') + + ', expected ' + + (min === max ? '' : 'between `' + min + '` (unaligned) and ') + + '`' + + max + + '` ' + + (min === max ? '' : '(aligned) ') + + pluralize('space', max) + + ', ' + + (differenceMin < 0 ? 'remove' : 'add') + + (differenceMin === differenceMax + ? '' + : ' between `' + differenceMaxAbsolute + '` and') + + ' `' + + differenceMinAbsolute + + '` ' + + pluralize('space', differenceMinAbsolute), + { + ancestors: info.ancestors, + cause, + place: side === 'left' ? info.size.leftPoint : info.size.rightPoint + } + ) + } + } + + // Note: this code is also in `remark-lint-table-pipe-alignment`. + /** + * Get info about cells in a table. + * + * @param {Array} ancestors + * Ancestors. + * @returns {Array} + * Entries. + */ + function inferTable(ancestors) { + const node = ancestors.at(-1) + assert(node) // Always defined. + assert(node.type === 'table') // Always table. + /* c8 ignore next -- `align` is optional in AST. */ + const align = node.align || [] + /** @type {Array} */ + const result = [] + let rowIndex = -1 - if (style === 0) { - // Ignore every cell except the biggest in the column. - if (size(cell) < sizes[column]) { - return + // Regular rows. + while (++rowIndex < node.children.length) { + const row = node.children[rowIndex] + let column = -1 + + while (++column < row.children.length) { + const node = row.children[column] + + result.push({ + align: align[column], + ancestors: [...ancestors, row, node], + column, + size: inferSize( + pointStart(node), + pointEnd(node), + column === row.children.length - 1 + ) + }) + } + + if (rowIndex === 0) { + const alignRow = inferAlignRow(ancestors, align) + if (alignRow) result.push(...alignRow) } + } + + return result + } + + /** + * @param {Array} ancestors + * @param {Array} align + * @returns {Array | undefined} + */ + function inferAlignRow(ancestors, align) { + const node = ancestors.at(-1) + assert(node) // Always defined. + assert(node.type === 'table') // Always table. + const headEnd = pointEnd(node.children[0]) + + if (!headEnd || typeof headEnd.offset !== 'number') return + + let index = headEnd.offset + + if (value.charCodeAt(index) === 13 /* `\r` */) index++ + /* c8 ignore next -- should never happen, alignment is needed. */ + if (value.charCodeAt(index) !== 10 /* `\n` */) return - reason += 'compact' - } else { - reason += 'padded' + index++ - if (spacing > style) { - // May be right or center aligned. - if (size(cell) < sizes[column]) { - return + /** @type {Array} */ + const result = [] + const line = headEnd.line + 1 + // Alignment row can only be on the second line, + // so containers can only indent with `>` or spaces. + let code = value.charCodeAt(index) + while ( + code === 9 /* `\t` */ || + code === 32 /* ` ` */ || + code === 62 /* `>` */ + ) { + index++ + code = value.charCodeAt(index) + } + + /* c8 ignore next 7 -- should always be found. */ + if ( + code !== 45 /* `-` */ && + code !== 58 /* `:` */ && + code !== 124 /* `|` */ + ) { + return + } + + let lineEndOffset = value.indexOf('\n', index) + if (lineEndOffset === -1) lineEndOffset = value.length + if (value.charCodeAt(lineEndOffset - 1) === 13 /* `\r` */) lineEndOffset-- + + let column = 0 + let cellStart = index + let cellEnd = value.indexOf('|', index + (code === 124 ? 1 : 0)) + if (cellEnd === -1 || cellEnd > lineEndOffset) { + cellEnd = lineEndOffset + } + + while (cellStart !== cellEnd) { + let nextCellEnd = value.indexOf('|', cellEnd + 1) + + if (nextCellEnd === -1 || nextCellEnd > lineEndOffset) { + nextCellEnd = lineEndOffset + } + + // Check if the trail is empty, + // which means it’s a closing pipe with trailing whitespace. + if (nextCellEnd === lineEndOffset) { + let maybeEnd = lineEndOffset + let code = value.charCodeAt(maybeEnd - 1) + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + maybeEnd-- + code = value.charCodeAt(maybeEnd - 1) } - reason += ' with 1 space, not ' + spacing + if (cellEnd + 1 === maybeEnd) { + cellEnd = lineEndOffset + } } + + result.push({ + align: align[column], + ancestors, + column, + size: inferSize( + { + line, + column: cellStart - index + 1, + offset: cellStart + }, + {line, column: cellEnd - index + 1, offset: cellEnd}, + cellEnd === lineEndOffset + ) + }) + + cellStart = cellEnd + cellEnd = nextCellEnd + column++ } - file.message( - reason, - side === 'start' - ? pointStart(cell.children[0]) - : pointEnd(cell.children[cell.children.length - 1]) - ) + return result + } + + /** + * @param {Point | undefined} start + * Start point. + * @param {Point | undefined} end + * End point. + * @param {boolean} tailCell + * Whether this is the last cell in a row. + * @returns {Size | undefined} + * Size info. + */ + function inferSize(start, end, tailCell) { + if ( + end && + start && + typeof end.offset === 'number' && + typeof start.offset === 'number' + ) { + let leftIndex = start.offset + /** @type {number | undefined} */ + let left + /** @type {number | undefined} */ + let right + + if (value.charCodeAt(leftIndex) === 124 /* `|` */) { + left = 0 + leftIndex++ + + while (value.charCodeAt(leftIndex) === 32) { + left++ + leftIndex++ + } + } + // Else, A leading pipe can only be omitted in the first cell. + // Where we never want leading whitespace, as it’s seen as + // indentation, and could turn into an indented block. + + let rightIndex = end.offset + + // The final pipe, if it exists, is part of the last cell in a row + // according to positional info. + if (tailCell) { + while (value.charCodeAt(rightIndex - 1) === 32) { + rightIndex-- + } + + // Found a pipe: we expect more whitespace. + if ( + rightIndex > leftIndex && + value.charCodeAt(rightIndex - 1) === 124 /* `|` */ + ) { + rightIndex-- + } + // No pipe at the last cell: the trailing whitespace is part of + // the cell. + else { + rightIndex = end.offset + } + } + + /** @type {number} */ + const rightEdgeIndex = rightIndex + + if (value.charCodeAt(rightIndex) === 124 /* `|` */) { + right = 0 + + while ( + rightIndex - 1 > leftIndex && + value.charCodeAt(rightIndex - 1) === 32 + ) { + right++ + rightIndex-- + } + } + // Else, a trailing pipe can only be omitted in the last cell. + // Where we never want trailing whitespace. + + return { + left, + leftPoint: { + line: start.line, + column: start.column + (leftIndex - start.offset), + offset: leftIndex + }, + middle: rightIndex - leftIndex, + right, + rightPoint: { + line: end.line, + column: end.column - (end.offset - rightEdgeIndex), + offset: rightEdgeIndex + } + } + } } } ) export default remarkLintTableCellPadding - -/** - * @param {TableCell} node - * Cell. - * @returns {number} - * Size of `node`. - */ -function size(node) { - const head = pointStart(node.children[0])?.offset - const tail = pointEnd(node.children[node.children.length - 1])?.offset - /* c8 ignore next -- Only called when we’re sure offsets exist. */ - return typeof head === 'number' && typeof tail === 'number' ? tail - head : 0 -} diff --git a/packages/remark-lint-table-cell-padding/package.json b/packages/remark-lint-table-cell-padding/package.json index 7fc48311..5cbdd203 100644 --- a/packages/remark-lint-table-cell-padding/package.json +++ b/packages/remark-lint-table-cell-padding/package.json @@ -34,9 +34,12 @@ "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { @@ -48,9 +51,10 @@ "xo": { "prettier": true, "rules": { + "complexity": "off", "capitalized-comments": "off", - "unicorn/prefer-at": "off", - "unicorn/prefer-default-parameters": "off" + "unicorn/explicit-length-check": "off", + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-table-cell-padding/readme.md b/packages/remark-lint-table-cell-padding/readme.md index 354d2196..e4402f2e 100644 --- a/packages/remark-lint-table-cell-padding/readme.md +++ b/packages/remark-lint-table-cell-padding/readme.md @@ -190,9 +190,13 @@ When configured with `'padded'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| A | B | -| ----- | ----- | -| Alpha | Bravo | +| Planet | Symbol | Satellites | Mean anomaly (°) | +| ------- | :----- | :--------: | ---------------: | +| Mercury | ☿ | None | 174 796 | + +| Planet | Symbol | Satellites | Mean anomaly (°) | +| - | :- | :-: | -: | +| Venus | ♀ | None | 50 115 | ``` ###### Out @@ -209,33 +213,33 @@ When configured with `'padded'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| A | B | -| :----|----: | -| Alpha|Bravo | - -| C | D | -| :----- | ---: | -|Charlie | Delta| +| Planet | +| -------| +| Mercury| -Too much padding isn’t good either: +|Planet | +|------ | +|Venus | -| E | F | G | H | -| :---- | -------- | :----: | -----: | -| Echo | Foxtrot | Golf | Hotel | +| Planet | +| ------ | +| Venus | ``` ###### Out ```text -3:8: Cell should be padded -3:9: Cell should be padded -7:2: Cell should be padded -7:17: Cell should be padded -13:7: Cell should be padded with 1 space, not 2 -13:18: Cell should be padded with 1 space, not 2 -13:23: Cell should be padded with 1 space, not 2 -13:27: Cell should be padded with 1 space, not 2 -13:32: Cell should be padded with 1 space, not 2 +2:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +3:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +5:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +6:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +7:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +9:4: Unexpected `2` spaces between cell edge and content, expected `1` space, remove `1` space +9:12: Unexpected `2` spaces between cell content and edge, expected `1` space, remove `1` space +10:4: Unexpected `2` spaces between cell edge and content, expected `1` space, remove `1` space +10:12: Unexpected `2` spaces between cell content and edge, expected `1` space, remove `1` space +11:4: Unexpected `2` spaces between cell edge and content, expected `1` space, remove `1` space +11:12: Unexpected `3` spaces between cell content and edge, expected between `1` (unaligned) and `2` (aligned) spaces, remove between `1` and `2` spaces ``` ##### `ok.md` @@ -248,9 +252,13 @@ When configured with `'compact'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -|A |B | -|-----|-----| -|Alpha|Bravo| +|Planet |Symbol|Satellites|Mean anomaly (°)| +|-------|:-----|:--------:|---------------:| +|Mercury|☿ | None | 174 796| + +|Planet|Symbol|Satellites|Mean anomaly (°)| +|-|:-|:-:|-:| +|Venus|♀|None|50 115| ``` ###### Out @@ -267,24 +275,105 @@ When configured with `'compact'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| A | B | -| -----| -----| -| Alpha| Bravo| +| Planet | +| -------| +| Mercury| + +|Planet | +|------ | +|Venus | + +| Planet | +| ------ | +| Venus | +``` + +###### Out + +```text +1:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +2:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +3:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +5:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +6:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +7:9: Unexpected `2` spaces between cell content and edge, expected between `0` (unaligned) and `1` (aligned) space, remove between `1` and `2` spaces +9:4: Unexpected `2` spaces between cell edge and content, expected `0` spaces, remove `2` spaces +9:12: Unexpected `2` spaces between cell content and edge, expected `0` spaces, remove `2` spaces +10:4: Unexpected `2` spaces between cell edge and content, expected `0` spaces, remove `2` spaces +10:12: Unexpected `2` spaces between cell content and edge, expected `0` spaces, remove `2` spaces +11:4: Unexpected `2` spaces between cell edge and content, expected `0` spaces, remove `2` spaces +11:12: Unexpected `3` spaces between cell content and edge, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces +``` + +##### `consistent-padded-ok.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| Planet | +| - | +``` + +###### Out + +No messages. + +##### `consistent-padded-nok.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| Planet| +| - | +``` + +###### Out + +```text +1:9: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +``` + +##### `consistent-compact-ok.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +|Planet| +|-| +``` + +###### Out -|C | D| -|:------|-----:| -|Charlie|Delta | +No messages. + +##### `consistent-compact-nok.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +|Planet | +|-| ``` ###### Out ```text -3:5: Cell should be compact -3:12: Cell should be compact -7:15: Cell should be compact +1:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space ``` -##### `ok-padded.md` +##### `empty.md` ###### In @@ -292,24 +381,38 @@ When configured with `'compact'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -The default is `'consistent'`. +| | Satellites | +| - | - | +| Mercury | | +``` + +###### Out + +No messages. + +##### `missing-cells.md` -| A | B | -| ----- | ----- | -| Alpha | Bravo | +###### In -| C | D | -| ------- | ----- | -| Charlie | Delta | +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| Planet | Symbol | Satellites | +| - | - | - | +| Mercury | +| Venus | ♀ | +| Earth | 🜨 and ♁ | 1 | +| Mars | ♂ | 2 | 19 412 | ``` ###### Out No messages. -##### `not-ok-padded.md` +##### `missing-fences.md` -When configured with `'consistent'`. +When configured with `'padded'`. ###### In @@ -317,24 +420,40 @@ When configured with `'consistent'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| A | B | -| ----- | ----- | -| Alpha | Bravo | +␠Planet|Symbol|Satellites +------:|:-----|---------- +Mercury|☿ |0 -| C | D | -| :----- | ----: | -|Charlie | Delta | +Planet|Symbol +-----:|------ +␠Venus|♀ ``` ###### Out ```text -7:2: Cell should be padded +1:8: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +1:9: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +1:15: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +1:16: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +2:8: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +2:9: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +2:15: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +2:16: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +3:8: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +3:9: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +3:16: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +5:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +5:8: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +6:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +6:8: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +7:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +7:8: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space ``` -##### `ok-compact.md` +##### `missing-fences.md` -When configured with `'consistent'`. +When configured with `'compact'`. ###### In @@ -342,22 +461,39 @@ When configured with `'consistent'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -|A |B | -|-----|-----| -|Alpha|Bravo| +Planet | Symbol | Satellites +-: | - | - +Mercury | ☿ | 0 -|C |D | -|-------|-----| -|Charlie|Delta| +Planet | Symbol +-----: | ------ +␠Venus | ♀ ``` ###### Out -No messages. +```text +1:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +1:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +1:17: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +1:19: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +2:4: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +2:6: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +2:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +3:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +3:11: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +3:15: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +5:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +5:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +6:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +6:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +7:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +7:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +``` -##### `not-ok-compact.md` +##### `trailing-spaces.md` -When configured with `'consistent'`. +When configured with `'compact'`. ###### In @@ -365,32 +501,103 @@ When configured with `'consistent'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -|A |B | -|-----|-----| -|Alpha|Bravo| +Planet | Symbol␠ +-: | -␠ +Mercury | ☿␠␠ -|C | D| -|:------|-----:| -|Charlie|Delta | +| Planet | Symbol |␠ +| ------ | ------ |␠ +| Venus | ♀ |␠␠ ``` ###### Out ```text -7:15: Cell should be compact +1:8: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +1:10: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +2:4: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +2:6: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +3:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +3:11: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +5:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +5:10: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +5:12: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +5:19: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +6:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +6:10: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +6:12: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +6:19: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +7:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +7:10: Unexpected `2` spaces between cell content and edge, expected between `0` (unaligned) and `1` (aligned) space, remove between `1` and `2` spaces +7:12: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +7:19: Unexpected `6` spaces between cell content and edge, expected between `0` (unaligned) and `5` (aligned) spaces, remove between `1` and `6` spaces ``` -##### `not-ok.md` +##### `nothing.md` + +When configured with `'compact'`. -When configured with `'💩'`. +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| | | | +| - | - | - | +| | | | +``` ###### Out ```text -1:1: Incorrect table cell padding style `💩`, expected `'padded'`, `'compact'`, or `'consistent'` +1:5: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces +1:9: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces +1:13: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces +2:3: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +2:5: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +2:7: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +2:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +2:11: Unexpected `1` space between cell edge and content, expected `0` spaces, remove `1` space +2:13: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space +3:5: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces +3:9: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces +3:13: Unexpected `3` spaces between cell edge and content, expected between `0` (unaligned) and `1` (aligned) space, remove between `2` and `3` spaces ``` -##### `empty.md` +##### `nothing.md` + +When configured with `'padded'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +|||| +|-|-|-| +|||| +``` + +###### Out + +```text +1:2: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space +1:3: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space +1:4: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space +2:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +2:3: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +2:4: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +2:5: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +2:6: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +2:7: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +3:2: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space +3:3: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space +3:4: Unexpected `0` spaces between cell edge and content, expected between `1` (unaligned) and `3` (aligned) spaces, add between `3` and `1` space +``` + +##### `more-weirdness.md` When configured with `'padded'`. @@ -400,22 +607,21 @@ When configured with `'padded'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown - +Mercury +|- -| | Alpha | Bravo| -| ------ | ----- | ---: | -| Charlie| | Echo| +Venus +-| ``` ###### Out ```text -3:25: Cell should be padded -5:10: Cell should be padded -5:25: Cell should be padded +2:2: Unexpected `0` spaces between cell edge and content, expected `1` space, add `1` space +5:2: Unexpected `0` spaces between cell content and edge, expected between `1` (unaligned) and `5` (aligned) spaces, add between `5` and `1` space ``` -##### `missing-body.md` +##### `containers.md` When configured with `'padded'`. @@ -425,17 +631,52 @@ When configured with `'padded'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown - +> | Mercury| +> | - | -| Alpha | Bravo | Charlie | -| ----- | ------- | ------- | -| Delta | -| Echo | Foxtrot | +* | Venus| + | - | + +> * > | Earth| +> > | - | ``` ###### Out -No messages. +```text +1:12: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +4:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +7:14: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +``` + +##### `windows.md` + +When configured with `'padded'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| Mercury|␍␊| --- |␍␊| None | +``` + +###### Out + +```text +1:10: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space +``` + +##### `not-ok.md` + +When configured with `'🌍'`. + +###### Out + +```text +1:1: Unexpected value `🌍` for `options`, expected `'compact'`, `'padded'`, or `'consistent'` +``` ## Compatibility diff --git a/packages/remark-lint-table-pipe-alignment/index.js b/packages/remark-lint-table-pipe-alignment/index.js index aa7b5ac5..9e5a7dc3 100644 --- a/packages/remark-lint-table-pipe-alignment/index.js +++ b/packages/remark-lint-table-pipe-alignment/index.js @@ -53,48 +53,159 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * + * @example + * {"gfm": true, "name": "ok.md"} + * + * | Planet | Mean anomaly (°) | + * | ------- | ---------------: | + * | Mercury | 174 796 | + * + * |Planet|Mean anomaly (°)| + * |------|---------------:| + * |Venus | 50 115 | + * + * @example + * {"gfm": true, "label": "input", "name": "not-ok.md"} + * + * | Planet | Mean anomaly (°) | + * | - | -: | + * | Mercury | 174 796 | + * + * @example + * {"gfm": true, "label": "output", "name": "not-ok.md"} + * + * 1:10: Unexpected unaligned cell, expected aligned pipes, add `1` space + * 2:5: Unexpected unaligned cell, expected aligned pipes, add `6` spaces (or add `-` to pad alignment row cells) + * 2:7: Unexpected unaligned cell, expected aligned pipes, add `14` spaces (or add `-` to pad alignment row cells) + * 3:13: Unexpected unaligned cell, expected aligned pipes, add `9` spaces + * + * @example + * {"gfm": true, "name": "empty.md"} + * + * | | Satellites | | + * | ------- | ---------- | --- | + * | Mercury | | | + * + * @example + * {"gfm": true, "name": "missing-cells.md"} + * + * | Planet | Symbol | Satellites | + * | ------- | ------ | ---------- | + * | Mercury | + * | Venus | ♀ | + * | Earth | ♁ | 1 | + * | Mars | ♂ | 2 | 19 412 | + * + * @example + * {"gfm": true, "label": "input", "name": "alignment.md"} + * + * | Planet | Symbol | Satellites | Mean anomaly (°) | + * | - | :- | :-: | -: | + * | Mercury | ☿ | None | 174 796 | + * @example + * {"gfm": true, "label": "output", "name": "alignment.md"} + * + * 1:10: Unexpected unaligned cell, expected aligned pipes, add `1` space + * 2:5: Unexpected unaligned cell, expected aligned pipes, add `6` spaces (or add `-` to pad alignment row cells) + * 2:10: Unexpected unaligned cell, expected aligned pipes, add `4` spaces (or add `-` to pad alignment row cells) + * 2:12: Unexpected unaligned cell, expected aligned pipes, add `4` spaces (or add `-` to pad alignment row cells) + * 2:16: Unexpected unaligned cell, expected aligned pipes, add `3` spaces (or add `-` to pad alignment row cells) + * 2:18: Unexpected unaligned cell, expected aligned pipes, add `14` spaces (or add `-` to pad alignment row cells) + * 3:15: Unexpected unaligned cell, expected aligned pipes, add `5` spaces + * 3:17: Unexpected unaligned cell, expected aligned pipes, add `3` spaces + * 3:22: Unexpected unaligned cell, expected aligned pipes, add `3` spaces + * 3:24: Unexpected unaligned cell, expected aligned pipes, add `9` spaces + * + * @example + * {"gfm": true, "label": "input", "name": "missing-fences.md"} + * + * Planet | Satellites + * -: | - + * Mercury | ☿ + * @example + * {"gfm": true, "label": "output", "name": "missing-fences.md"} + * + * 1:1: Unexpected unaligned cell, expected aligned pipes, add `1` space + * 2:1: Unexpected unaligned cell, expected aligned pipes, add `5` spaces (or add `-` to pad alignment row cells) + * * @example - * {"name": "ok.md", "gfm": true} + * {"gfm": true, "label": "input", "name": "trailing-spaces.md"} * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | + * | Planet |␠␠ + * | -: |␠ + * @example + * {"gfm": true, "label": "output", "name": "trailing-spaces.md"} + * + * 2:3: Unexpected unaligned cell, expected aligned pipes, add `4` spaces (or add `-` to pad alignment row cells) + * + * @example + * {"gfm": true, "label": "input", "name": "nothing.md"} + * + * |||| + * |-|-|-| + * @example + * {"gfm": true, "label": "output", "name": "nothing.md"} + * + * 1:2: Unexpected unaligned cell, expected aligned pipes, add `1` space + * 1:3: Unexpected unaligned cell, expected aligned pipes, add `1` space + * 1:4: Unexpected unaligned cell, expected aligned pipes, add `1` space * * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} + * {"gfm": true, "label": "input", "name": "more-weirdness.md"} * - * | A | B | - * | -- | -- | - * | Alpha | Bravo | + * Mercury + * |- * + * Venus + * -| * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} + * {"gfm": true, "label": "output", "name": "more-weirdness.md"} * - * 3:9-3:10: Misaligned table fence - * 3:17-3:18: Misaligned table fence + * 5:2: Unexpected unaligned cell, expected aligned pipes, add `4` spaces (or add `-` to pad alignment row cells) * * @example - * {"name": "ok-empty-columns.md", "gfm": true} + * {"gfm": true, "label": "input", "name": "containers.md"} * - * | | B | | - * |-| ----- | - | - * | | Bravo | | + * > | Mercury| + * > | - | + * + * * | Venus| + * | - | + * + * > * > | Earth| + * > > | - | + * @example + * {"gfm": true, "label": "output", "name": "containers.md"} + * + * 2:5: Unexpected unaligned cell, expected aligned pipes, add `5` spaces (or add `-` to pad alignment row cells) + * 5:5: Unexpected unaligned cell, expected aligned pipes, add `3` spaces (or add `-` to pad alignment row cells) + * 8:5: Unexpected unaligned cell, expected aligned pipes, add `3` spaces (or add `-` to pad alignment row cells) * * @example - * {"name": "ok-empty-cells.md", "gfm": true} + * {"gfm": true, "label": "input", "name": "windows.md"} * - * | | | | - * | - | --- | ------- | - * | A | Bra | Charlie | + * | Mercury|␍␊| --- |␍␊| None | + * @example + * {"gfm": true, "label": "output", "name": "windows.md"} + * + * 2:7: Unexpected unaligned cell, expected aligned pipes, add `3` spaces (or add `-` to pad alignment row cells) + * 3:8: Unexpected unaligned cell, expected aligned pipes, add `2` spaces */ /** + * @typedef {import('mdast').AlignType} Align + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root + * + * @typedef {import('unist').Point} Point */ +import {ok as assert} from 'devlop' +import pluralize from 'pluralize' import {lintRule} from 'unified-lint-rule' import {pointEnd, pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' +import {SKIP, visitParents} from 'unist-util-visit-parents' const remarkLintTablePipeAlignment = lintRule( { @@ -108,59 +219,348 @@ const remarkLintTablePipeAlignment = lintRule( * Nothing. */ function (tree, file) { + /** + * @typedef Entry + * @property {Align} align + * @property {Array} ancestors + * @property {number} column + * @property {number | undefined} row + * @property {Size | undefined} size + * + * @typedef Size + * @property {number | undefined} left + * @property {Point} leftPoint + * @property {number} middle + * @property {number | undefined} right + * @property {Point} rightPoint + */ + const value = String(file) - visit(tree, 'table', function (node) { + visitParents(tree, 'table', function (node, parents) { + const entries = inferTable([...parents, node]) + // Find max column sizes. /** @type {Array} */ - const indices = [] - let index = -1 + const sizes = [] + + for (const info of entries) { + if (info.size) { + let total = info.size.middle + if (info.size.left) total += info.size.left + if (info.size.right) total += info.size.right + + if (sizes[info.column] === undefined || total > sizes[info.column]) { + sizes[info.column] = total + } + } + } + + for (const info of entries) { + if (!info.size) continue + + let total = info.size.middle + if (info.size.left) total += info.size.left + if (info.size.right) total += info.size.right + + const difference = sizes[info.column] - total + assert(difference >= 0) // Always positive. + let left = 0 + let right = 0 + + if (info.align === 'right') { + left = difference + } else if (info.align === 'center') { + // Maximum number of spaces we would want on the left. + const max = Math.floor((sizes[info.column] - info.size.middle) / 2) + + if (info.size.right !== undefined && max > info.size.right) { + right = max - info.size.right + } + + left = difference - right + } else { + right = difference + } + + warn(info, left, info.size.leftPoint) + + // If there is no final pipe, we don’t ask for trailing spaces. + if (info.size.right !== undefined) { + warn(info, right, info.size.rightPoint) + } + } + + return SKIP + }) - while (++index < node.children.length) { - const row = node.children[index] - const begin = pointStart(row) - let column = -2 // Start without a first cell. + /** + * @param {Entry} info + * Info. + * @param {number} add + * Number of spaces to add. + * @param {Point} place + * Place to add spaces. + * @returns {undefined} + * Nothing. + */ + function warn(info, add, place) { + if (add === 0) return + file.message( + 'Unexpected unaligned cell, expected aligned pipes, add `' + + add + + '` ' + + pluralize('space', add) + + (info.row === undefined + ? ' (or add `-` to pad alignment row cells)' + : ''), + {ancestors: info.ancestors, place} + ) + } + + // Note: this code is also in `remark-lint-table-pipe-alignment`. + /** + * Get info about cells in a table. + * + * @param {Array} ancestors + * Ancestors. + * @returns {Array} + * Entries. + */ + function inferTable(ancestors) { + const node = ancestors.at(-1) + assert(node) // Always defined. + assert(node.type === 'table') // Always table. + /* c8 ignore next -- `align` is optional in AST. */ + const align = node.align || [] + /** @type {Array} */ + const result = [] + let rowIndex = -1 + + // Regular rows. + while (++rowIndex < node.children.length) { + const row = node.children[rowIndex] + let column = -1 while (++column < row.children.length) { - const cell = row.children[column] - const nextColumn = column + 1 - const next = row.children[nextColumn] - let initial = cell - ? cell.children.length === 0 - ? pointStart(cell)?.offset - : pointEnd(cell.children[cell.children.length - 1])?.offset - : pointStart(row)?.offset - let final = next - ? next.children.length === 0 - ? pointEnd(next)?.offset - : pointStart(next.children[0])?.offset - : pointEnd(row)?.offset + const node = row.children[column] + + result.push({ + align: align[column], + ancestors: [...ancestors, row, node], + column, + row: rowIndex, + size: inferSize( + pointStart(node), + pointEnd(node), + column === row.children.length - 1 + ) + }) + } + + if (rowIndex === 0) { + const alignRow = inferAlignRow(ancestors, align) + if (alignRow) result.push(...alignRow) + } + } + + return result + } + + /** + * @param {Array} ancestors + * @param {Array} align + * @returns {Array | undefined} + */ + function inferAlignRow(ancestors, align) { + const node = ancestors.at(-1) + assert(node) // Always defined. + assert(node.type === 'table') // Always table. + const headEnd = pointEnd(node.children[0]) + + if (!headEnd || typeof headEnd.offset !== 'number') return + + let index = headEnd.offset + + if (value.charCodeAt(index) === 13 /* `\r` */) index++ + /* c8 ignore next -- should never happen, alignment is needed. */ + if (value.charCodeAt(index) !== 10 /* `\n` */) return + + index++ + + /** @type {Array} */ + const result = [] + const line = headEnd.line + 1 + // Alignment row can only be on the second line, + // so containers can only indent with `>` or spaces. + let code = value.charCodeAt(index) + while ( + code === 9 /* `\t` */ || + code === 32 /* ` ` */ || + code === 62 /* `>` */ + ) { + index++ + code = value.charCodeAt(index) + } + + /* c8 ignore next 7 -- should always be found. */ + if ( + code !== 45 /* `-` */ && + code !== 58 /* `:` */ && + code !== 124 /* `|` */ + ) { + return + } + + let lineEndOffset = value.indexOf('\n', index) + if (lineEndOffset === -1) lineEndOffset = value.length + if (value.charCodeAt(lineEndOffset - 1) === 13 /* `\r` */) lineEndOffset-- + + let column = 0 + let cellStart = index + let cellEnd = value.indexOf('|', index + (code === 124 ? 1 : 0)) + if (cellEnd === -1 || cellEnd > lineEndOffset) { + cellEnd = lineEndOffset + } + while (cellStart !== cellEnd) { + let nextCellEnd = value.indexOf('|', cellEnd + 1) + + if (nextCellEnd === -1 || nextCellEnd > lineEndOffset) { + nextCellEnd = lineEndOffset + } + + // Check if the trail is empty, + // which means it’s a closing pipe with trailing whitespace. + if (nextCellEnd === lineEndOffset) { + let maybeEnd = lineEndOffset + let code = value.charCodeAt(maybeEnd - 1) + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + maybeEnd-- + code = value.charCodeAt(maybeEnd - 1) + } + + if (cellEnd + 1 === maybeEnd) { + cellEnd = lineEndOffset + } + } + + result.push({ + align: align[column], + ancestors, + column, + row: undefined, + size: inferSize( + { + line, + column: cellStart - index + 1, + offset: cellStart + }, + {line, column: cellEnd - index + 1, offset: cellEnd}, + cellEnd === lineEndOffset + ) + }) + + cellStart = cellEnd + cellEnd = nextCellEnd + column++ + } + + return result + } + + /** + * @param {Point | undefined} start + * Start point. + * @param {Point | undefined} end + * End point. + * @param {boolean} tailCell + * Whether this is the last cell in a row. + * @returns {Size | undefined} + * Size info. + */ + function inferSize(start, end, tailCell) { + if ( + end && + start && + typeof end.offset === 'number' && + typeof start.offset === 'number' + ) { + let leftIndex = start.offset + /** @type {number | undefined} */ + let left + /** @type {number | undefined} */ + let right + + if (value.charCodeAt(leftIndex) === 124 /* `|` */) { + left = 0 + leftIndex++ + + while (value.charCodeAt(leftIndex) === 32) { + left++ + leftIndex++ + } + } + // Else, A leading pipe can only be omitted in the first cell. + // Where we never want leading whitespace, as it’s seen as + // indentation, and could turn into an indented block. + + let rightIndex = end.offset + + // The final pipe, if it exists, is part of the last cell in a row + // according to positional info. + if (tailCell) { + while (value.charCodeAt(rightIndex - 1) === 32) { + rightIndex-- + } + + // Found a pipe: we expect more whitespace. if ( - typeof initial !== 'number' || - typeof final !== 'number' || - typeof begin?.offset !== 'number' + rightIndex > leftIndex && + value.charCodeAt(rightIndex - 1) === 124 /* `|` */ ) { - continue + rightIndex-- + } + // No pipe at the last cell: the trailing whitespace is part of + // the cell. + else { + rightIndex = end.offset } + } + + /** @type {number} */ + const rightEdgeIndex = rightIndex - if (cell && cell.children.length === 0) initial++ - if (next && next.children.length === 0) final-- + if (value.charCodeAt(rightIndex) === 124 /* `|` */) { + right = 0 - const fence = value.slice(initial, final) - const pos = initial + fence.indexOf('|') - begin.offset + 1 + while ( + rightIndex - 1 > leftIndex && + value.charCodeAt(rightIndex - 1) === 32 + ) { + right++ + rightIndex-- + } + } + // Else, a trailing pipe can only be omitted in the last cell. + // Where we never want trailing whitespace. - // First cell at this column. - if (indices[nextColumn] === undefined) { - indices[nextColumn] = pos - } else if (pos !== indices[nextColumn]) { - file.message('Misaligned table fence', { - start: {line: begin.line, column: pos}, - end: {line: begin.line, column: pos + 1} - }) + return { + left, + leftPoint: { + line: start.line, + column: start.column + (leftIndex - start.offset), + offset: leftIndex + }, + middle: rightIndex - leftIndex, + right, + rightPoint: { + line: end.line, + column: end.column - (end.offset - rightEdgeIndex), + offset: rightEdgeIndex } } } - }) + } } ) diff --git a/packages/remark-lint-table-pipe-alignment/package.json b/packages/remark-lint-table-pipe-alignment/package.json index a861df4e..15ff773d 100644 --- a/packages/remark-lint-table-pipe-alignment/package.json +++ b/packages/remark-lint-table-pipe-alignment/package.json @@ -34,9 +34,12 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "pluralize": "^8.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { @@ -48,8 +51,10 @@ "xo": { "prettier": true, "rules": { + "complexity": "off", "capitalized-comments": "off", - "unicorn/prefer-at": "off" + "unicorn/explicit-length-check": "off", + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-table-pipe-alignment/readme.md b/packages/remark-lint-table-pipe-alignment/readme.md index 31c8dc34..ea6fb665 100644 --- a/packages/remark-lint-table-pipe-alignment/readme.md +++ b/packages/remark-lint-table-pipe-alignment/readme.md @@ -164,9 +164,13 @@ in which case this rule must be turned off. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| A | B | -| ----- | ----- | -| Alpha | Bravo | +| Planet | Mean anomaly (°) | +| ------- | ---------------: | +| Mercury | 174 796 | + +|Planet|Mean anomaly (°)| +|------|---------------:| +|Venus | 50 115 | ``` ###### Out @@ -181,19 +185,21 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| A | B | -| -- | -- | -| Alpha | Bravo | +| Planet | Mean anomaly (°) | +| - | -: | +| Mercury | 174 796 | ``` ###### Out ```text -3:9-3:10: Misaligned table fence -3:17-3:18: Misaligned table fence +1:10: Unexpected unaligned cell, expected aligned pipes, add `1` space +2:5: Unexpected unaligned cell, expected aligned pipes, add `6` spaces (or add `-` to pad alignment row cells) +2:7: Unexpected unaligned cell, expected aligned pipes, add `14` spaces (or add `-` to pad alignment row cells) +3:13: Unexpected unaligned cell, expected aligned pipes, add `9` spaces ``` -##### `ok-empty-columns.md` +##### `empty.md` ###### In @@ -201,16 +207,16 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| | B | | -|-| ----- | - | -| | Bravo | | +| | Satellites | | +| ------- | ---------- | --- | +| Mercury | | | ``` ###### Out No messages. -##### `ok-empty-cells.md` +##### `missing-cells.md` ###### In @@ -218,15 +224,169 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| | | | -| - | --- | ------- | -| A | Bra | Charlie | +| Planet | Symbol | Satellites | +| ------- | ------ | ---------- | +| Mercury | +| Venus | ♀ | +| Earth | ♁ | 1 | +| Mars | ♂ | 2 | 19 412 | ``` ###### Out No messages. +##### `alignment.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| Planet | Symbol | Satellites | Mean anomaly (°) | +| - | :- | :-: | -: | +| Mercury | ☿ | None | 174 796 | +``` + +###### Out + +```text +1:10: Unexpected unaligned cell, expected aligned pipes, add `1` space +2:5: Unexpected unaligned cell, expected aligned pipes, add `6` spaces (or add `-` to pad alignment row cells) +2:10: Unexpected unaligned cell, expected aligned pipes, add `4` spaces (or add `-` to pad alignment row cells) +2:12: Unexpected unaligned cell, expected aligned pipes, add `4` spaces (or add `-` to pad alignment row cells) +2:16: Unexpected unaligned cell, expected aligned pipes, add `3` spaces (or add `-` to pad alignment row cells) +2:18: Unexpected unaligned cell, expected aligned pipes, add `14` spaces (or add `-` to pad alignment row cells) +3:15: Unexpected unaligned cell, expected aligned pipes, add `5` spaces +3:17: Unexpected unaligned cell, expected aligned pipes, add `3` spaces +3:22: Unexpected unaligned cell, expected aligned pipes, add `3` spaces +3:24: Unexpected unaligned cell, expected aligned pipes, add `9` spaces +``` + +##### `missing-fences.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Planet | Satellites +-: | - +Mercury | ☿ +``` + +###### Out + +```text +1:1: Unexpected unaligned cell, expected aligned pipes, add `1` space +2:1: Unexpected unaligned cell, expected aligned pipes, add `5` spaces (or add `-` to pad alignment row cells) +``` + +##### `trailing-spaces.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| Planet |␠␠ +| -: |␠ +``` + +###### Out + +```text +2:3: Unexpected unaligned cell, expected aligned pipes, add `4` spaces (or add `-` to pad alignment row cells) +``` + +##### `nothing.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +|||| +|-|-|-| +``` + +###### Out + +```text +1:2: Unexpected unaligned cell, expected aligned pipes, add `1` space +1:3: Unexpected unaligned cell, expected aligned pipes, add `1` space +1:4: Unexpected unaligned cell, expected aligned pipes, add `1` space +``` + +##### `more-weirdness.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Mercury +|- + +Venus +-| +``` + +###### Out + +```text +5:2: Unexpected unaligned cell, expected aligned pipes, add `4` spaces (or add `-` to pad alignment row cells) +``` + +##### `containers.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +> | Mercury| +> | - | + +* | Venus| + | - | + +> * > | Earth| +> > | - | +``` + +###### Out + +```text +2:5: Unexpected unaligned cell, expected aligned pipes, add `5` spaces (or add `-` to pad alignment row cells) +5:5: Unexpected unaligned cell, expected aligned pipes, add `3` spaces (or add `-` to pad alignment row cells) +8:5: Unexpected unaligned cell, expected aligned pipes, add `3` spaces (or add `-` to pad alignment row cells) +``` + +##### `windows.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| Mercury|␍␊| --- |␍␊| None | +``` + +###### Out + +```text +2:7: Unexpected unaligned cell, expected aligned pipes, add `3` spaces (or add `-` to pad alignment row cells) +3:8: Unexpected unaligned cell, expected aligned pipes, add `2` spaces +``` + ## Compatibility Projects maintained by the unified collective are compatible with maintained diff --git a/packages/remark-lint-table-pipes/index.js b/packages/remark-lint-table-pipes/index.js index f98147eb..785a8a9d 100644 --- a/packages/remark-lint-table-pipes/index.js +++ b/packages/remark-lint-table-pipes/index.js @@ -46,39 +46,96 @@ * @author Titus Wormer * @copyright 2015 Titus Wormer * @license MIT + * * @example * {"name": "ok.md", "gfm": true} * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | + * | Planet | Mean anomaly (°) | + * | :- | -: | + * | Mercury | 174 796 | * * @example * {"name": "not-ok.md", "label": "input", "gfm": true} * - * A | B - * ----- | ----- - * Alpha | Bravo - * + * Planet | Mean anomaly (°) + * :- | -: + * Mercury | 174 796 * @example * {"name": "not-ok.md", "label": "output", "gfm": true} * - * 1:1: Missing initial pipe in table fence - * 1:10: Missing final pipe in table fence - * 3:1: Missing initial pipe in table fence - * 3:14: Missing final pipe in table fence + * 1:1: Unexpected missing closing pipe in row, expected `|` + * 1:26: Unexpected missing opening pipe in row, expected `|` + * 2:1: Unexpected missing closing pipe in row, expected `|` + * 2:8: Unexpected missing opening pipe in row, expected `|` + * 3:1: Unexpected missing closing pipe in row, expected `|` + * 3:18: Unexpected missing opening pipe in row, expected `|` + * + * @example + * {"gfm": true, "label": "input", "name": "missing-cells.md"} + * + * Planet | Symbol | Satellites + * :- | - | - + * Mercury + * Venus | ♀ + * Earth | ♁ | 1 + * Mars | ♂ | 2 | 19 412 + * @example + * {"gfm": true, "label": "output", "name": "missing-cells.md"} + * + * 1:1: Unexpected missing closing pipe in row, expected `|` + * 1:29: Unexpected missing opening pipe in row, expected `|` + * 2:1: Unexpected missing closing pipe in row, expected `|` + * 2:11: Unexpected missing opening pipe in row, expected `|` + * 3:1: Unexpected missing closing pipe in row, expected `|` + * 3:8: Unexpected missing opening pipe in row, expected `|` + * 4:1: Unexpected missing closing pipe in row, expected `|` + * 4:10: Unexpected missing opening pipe in row, expected `|` + * 5:1: Unexpected missing closing pipe in row, expected `|` + * 5:14: Unexpected missing opening pipe in row, expected `|` + * 6:1: Unexpected missing closing pipe in row, expected `|` + * 6:22: Unexpected missing opening pipe in row, expected `|` + * + * @example + * {"gfm": true, "label": "input", "name": "trailing-spaces.md"} + * + * ␠␠Planet␠␠ + * ␠-:␠ + * + * ␠␠| Planet |␠␠ + * ␠| -: |␠ + * @example + * {"gfm": true, "label": "output", "name": "trailing-spaces.md"} + * + * 1:3: Unexpected missing closing pipe in row, expected `|` + * 1:11: Unexpected missing opening pipe in row, expected `|` + * 2:2: Unexpected missing closing pipe in row, expected `|` + * 2:5: Unexpected missing opening pipe in row, expected `|` + * + * @example + * {"gfm": true, "label": "input", "name": "windows.md"} + * + * Mercury␍␊:-␍␊None + * @example + * {"gfm": true, "label": "output", "name": "windows.md"} + * + * 1:1: Unexpected missing closing pipe in row, expected `|` + * 1:8: Unexpected missing opening pipe in row, expected `|` + * 2:1: Unexpected missing closing pipe in row, expected `|` + * 2:3: Unexpected missing opening pipe in row, expected `|` + * 3:1: Unexpected missing closing pipe in row, expected `|` + * 3:5: Unexpected missing opening pipe in row, expected `|` */ /** + * @typedef {import('mdast').Nodes} Nodes * @typedef {import('mdast').Root} Root + * + * @typedef {import('unist').Point} Point */ import {lintRule} from 'unified-lint-rule' -import {visit} from 'unist-util-visit' import {pointEnd, pointStart} from 'unist-util-position' - -const reasonStart = 'Missing initial pipe in table fence' -const reasonEnd = 'Missing final pipe in table fence' +import {visitParents} from 'unist-util-visit-parents' const remarkLintTablePipes = lintRule( { @@ -94,7 +151,7 @@ const remarkLintTablePipes = lintRule( function (tree, file) { const value = String(file) - visit(tree, 'table', function (node) { + visitParents(tree, 'table', function (node, parents) { let index = -1 while (++index < node.children.length) { @@ -102,23 +159,106 @@ const remarkLintTablePipes = lintRule( const start = pointStart(row) const end = pointEnd(row) - if ( - start && - typeof start.offset === 'number' && - value.charCodeAt(start.offset) !== 124 - ) { - file.message(reasonStart, start) + if (start && typeof start.offset === 'number') { + checkStart(start.offset, start, [...parents, node, row]) } - if ( - end && - typeof end.offset === 'number' && - value.charCodeAt(end.offset - 1) !== 124 - ) { - file.message(reasonEnd, end) + if (end && typeof end.offset === 'number') { + checkEnd(end.offset, end, [...parents, node, row]) + + // Align row. + if (index === 0) { + let index = end.offset + + if (value.charCodeAt(index) === 13 /* `\r` */) index++ + /* c8 ignore next -- should never happen, alignment is needed. */ + if (value.charCodeAt(index) !== 10 /* `\n` */) continue + index++ + + const lineStart = index + + // Alignment row can only be on the second line, + // so containers can only indent with `>` or spaces. + let code = value.charCodeAt(index) + + while ( + code === 9 /* `\t` */ || + code === 32 /* ` ` */ || + code === 62 /* `>` */ + ) { + index++ + code = value.charCodeAt(index) + } + + checkStart( + index, + { + line: end.line + 1, + column: index - lineStart + 1, + offset: index + }, + [...parents, node] + ) + + index = value.indexOf('\n', index) + if (index === -1) index = value.length + if (value.charCodeAt(index - 1) === 13 /* `\r` */) index-- + + checkEnd( + index, + { + line: end.line + 1, + column: index - lineStart + 1, + offset: index + }, + [...parents, node] + ) + } } } }) + + /** + * @param {number} index + * @param {Point} place + * @param {Array} ancestors + */ + function checkStart(index, place, ancestors) { + let code = value.charCodeAt(index) + + /* c8 ignore next 3 -- parser currently places indent outside. */ + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + code = value.charCodeAt(++index) + } + + if (code !== 124 /* `|` */) { + file.message('Unexpected missing closing pipe in row, expected `|`', { + ancestors, + place + }) + } + } + + /** + * @param {number} index + * @param {Point} place + * @param {Array} ancestors + */ + function checkEnd(index, place, ancestors) { + let code = value.charCodeAt(index - 1) + + while (code === 9 /* `\t` */ || code === 32 /* ` ` */) { + index-- + code = value.charCodeAt(index - 1) + } + + if (code !== 124 /* `|` */) { + file.message('Unexpected missing opening pipe in row, expected `|`', { + ancestors, + place + }) + } + } } ) diff --git a/packages/remark-lint-table-pipes/package.json b/packages/remark-lint-table-pipes/package.json index 9bd472f8..ecf09f8e 100644 --- a/packages/remark-lint-table-pipes/package.json +++ b/packages/remark-lint-table-pipes/package.json @@ -33,9 +33,10 @@ ], "dependencies": { "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0" }, "scripts": {}, "typeCoverage": { diff --git a/packages/remark-lint-table-pipes/readme.md b/packages/remark-lint-table-pipes/readme.md index 2bf9e485..93f07459 100644 --- a/packages/remark-lint-table-pipes/readme.md +++ b/packages/remark-lint-table-pipes/readme.md @@ -157,9 +157,9 @@ delimiters. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| A | B | -| ----- | ----- | -| Alpha | Bravo | +| Planet | Mean anomaly (°) | +| :- | -: | +| Mercury | 174 796 | ``` ###### Out @@ -174,18 +174,99 @@ No messages. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -A | B ------ | ----- -Alpha | Bravo +Planet | Mean anomaly (°) +:- | -: +Mercury | 174 796 ``` ###### Out ```text -1:1: Missing initial pipe in table fence -1:10: Missing final pipe in table fence -3:1: Missing initial pipe in table fence -3:14: Missing final pipe in table fence +1:1: Unexpected missing closing pipe in row, expected `|` +1:26: Unexpected missing opening pipe in row, expected `|` +2:1: Unexpected missing closing pipe in row, expected `|` +2:8: Unexpected missing opening pipe in row, expected `|` +3:1: Unexpected missing closing pipe in row, expected `|` +3:18: Unexpected missing opening pipe in row, expected `|` +``` + +##### `missing-cells.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Planet | Symbol | Satellites +:- | - | - +Mercury +Venus | ♀ +Earth | ♁ | 1 +Mars | ♂ | 2 | 19 412 +``` + +###### Out + +```text +1:1: Unexpected missing closing pipe in row, expected `|` +1:29: Unexpected missing opening pipe in row, expected `|` +2:1: Unexpected missing closing pipe in row, expected `|` +2:11: Unexpected missing opening pipe in row, expected `|` +3:1: Unexpected missing closing pipe in row, expected `|` +3:8: Unexpected missing opening pipe in row, expected `|` +4:1: Unexpected missing closing pipe in row, expected `|` +4:10: Unexpected missing opening pipe in row, expected `|` +5:1: Unexpected missing closing pipe in row, expected `|` +5:14: Unexpected missing opening pipe in row, expected `|` +6:1: Unexpected missing closing pipe in row, expected `|` +6:22: Unexpected missing opening pipe in row, expected `|` +``` + +##### `trailing-spaces.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +␠␠Planet␠␠ +␠-:␠ + +␠␠| Planet |␠␠ +␠| -: |␠ +``` + +###### Out + +```text +1:3: Unexpected missing closing pipe in row, expected `|` +1:11: Unexpected missing opening pipe in row, expected `|` +2:2: Unexpected missing closing pipe in row, expected `|` +2:5: Unexpected missing opening pipe in row, expected `|` +``` + +##### `windows.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Mercury␍␊:-␍␊None +``` + +###### Out + +```text +1:1: Unexpected missing closing pipe in row, expected `|` +1:8: Unexpected missing opening pipe in row, expected `|` +2:1: Unexpected missing closing pipe in row, expected `|` +2:3: Unexpected missing opening pipe in row, expected `|` +3:1: Unexpected missing closing pipe in row, expected `|` +3:5: Unexpected missing opening pipe in row, expected `|` ``` ## Compatibility diff --git a/packages/remark-lint-unordered-list-marker-style/index.js b/packages/remark-lint-unordered-list-marker-style/index.js index c822311b..aba18b14 100644 --- a/packages/remark-lint-unordered-list-marker-style/index.js +++ b/packages/remark-lint-unordered-list-marker-style/index.js @@ -25,24 +25,24 @@ * * Transform ([`Transformer` from `unified`][github-unified-transformer]). * - * ### `Marker` + * ### `Options` * - * Marker (TypeScript type). + * Configuration (TypeScript type). * * ###### Type * * ```ts - * type Marker = '*' | '+' | '-' + * type Options = Style | 'consistent' * ``` * - * ### `Options` + * ### `Style` * - * Configuration (TypeScript type). + * Style (TypeScript type). * * ###### Type * * ```ts - * type Options = Marker | 'consistent' + * type Style = '*' | '+' | '-' * ``` * * ## Recommendation @@ -57,8 +57,8 @@ * asterisks by default. * Pass `bullet: '+'` or `bullet: '-'` to use a different marker. * - * [api-marker]: #marker * [api-options]: #options + * [api-style]: #style * [api-remark-lint-unordered-list-marker-style]: #unifieduseremarklintunorderedlistmarkerstyle-options * [github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify * [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer @@ -70,51 +70,45 @@ * @example * {"name": "ok.md"} * - * By default (`'consistent'`), if the file uses only one marker, - * that’s OK. - * - * * Foo - * * Bar - * * Baz + * * Mercury * - * Ordered lists are not affected. + * 1. Venus * - * 1. Foo - * 2. Bar - * 3. Baz + * * Earth * * @example * {"name": "ok.md", "config": "*"} * - * * Foo + * * Mercury * * @example * {"name": "ok.md", "config": "-"} * - * - Foo + * - Mercury * * @example * {"name": "ok.md", "config": "+"} * - * + Foo + * + Mercury * * @example * {"name": "not-ok.md", "label": "input"} * - * * Foo - * - Bar - * + Baz + * * Mercury * + * - Venus + * + * + Earth * @example * {"name": "not-ok.md", "label": "output"} * - * 2:1-2:6: Marker style should be `*` - * 3:1-3:6: Marker style should be `*` + * 3:1: Unexpected unordered list marker `-`, expected `*` + * 5:1: Unexpected unordered list marker `+`, expected `*` * * @example - * {"name": "not-ok.md", "label": "output", "config": "💩", "positionless": true} + * {"name": "not-ok.md", "label": "output", "config": "🌍", "positionless": true} * - * 1:1: Incorrect unordered list item marker style `💩`: use either `'-'`, `'*'`, or `'+'` + * 1:1: Unexpected value `🌍` for `options`, expected `'*'`, `'+'`, `'-'`, or `'consistent'` */ /** @@ -122,18 +116,17 @@ */ /** - * @typedef {'*' | '+' | '-'} Marker - * Styles. - * - * @typedef {Marker | 'consistent'} Options + * @typedef {Style | 'consistent'} Options * Configuration. + * + * @typedef {'*' | '+' | '-'} Style + * Styles. */ import {lintRule} from 'unified-lint-rule' import {pointStart} from 'unist-util-position' -import {visit} from 'unist-util-visit' - -const markers = new Set(['*', '+', '-']) +import {visitParents} from 'unist-util-visit-parents' +import {VFileMessage} from 'vfile-message' const remarkLintUnorderedListMarkerStyle = lintRule( { @@ -150,45 +143,73 @@ const remarkLintUnorderedListMarkerStyle = lintRule( */ function (tree, file, options) { const value = String(file) - let option = options || 'consistent' + /** @type {Style | undefined} */ + let expected + /** @type {VFileMessage | undefined} */ + let cause + + console.log('check:', file.path) - if (option !== 'consistent' && !markers.has(option)) { + if (options === null || options === undefined || options === 'consistent') { + // Empty. + } else if (options === '*' || options === '+' || options === '-') { + expected = options + } else { file.fail( - 'Incorrect unordered list item marker style `' + - option + - "`: use either `'-'`, `'*'`, or `'+'`" + 'Unexpected value `' + + options + + "` for `options`, expected `'*'`, `'+'`, `'-'`, or `'consistent'`" ) } - visit(tree, 'list', function (node) { - if (node.ordered) return + visitParents(tree, 'listItem', function (node, parents) { + const parent = parents.at(-1) - let index = -1 + if (!parent || parent.type !== 'list' || parent.ordered) return - while (++index < node.children.length) { - const child = node.children[index] - const end = pointStart(child.children[0]) - const start = pointStart(child) + const place = pointStart(node) - if ( - end && - start && - typeof end.offset === 'number' && - typeof start.offset === 'number' - ) { - const marker = /** @type {Marker} */ ( - value - .slice(start.offset, end.offset) - .replace(/\[[x ]?]\s*$/i, '') - .replace(/\s/g, '') - ) + if (!place || typeof place.offset !== 'number') return - if (option === 'consistent') { - option = marker - } else if (marker !== option) { - file.message('Marker style should be `' + option + '`', child) - } + const code = value.charCodeAt(place.offset) + + const actual = + code === 42 /* `*` */ + ? '*' + : code === 43 /* `+` */ + ? '+' + : code === 45 /* `-` */ + ? '-' + : /* c8 ignore next -- weird ASTs. */ + undefined + + /* c8 ignore next -- weird ASTs. */ + if (!actual) return + + if (expected) { + if (actual !== expected) { + file.message( + 'Unexpected unordered list marker `' + + actual + + '`, expected `' + + expected + + '`', + {ancestors: [...parents, node], cause, place} + ) } + } else { + expected = actual + cause = new VFileMessage( + 'Unordered list marker style `' + + expected + + "` first defined for `'consistent'` here", + { + ancestors: [...parents, node], + place, + ruleId: 'unordered-list-marker-style', + source: 'remark-lint' + } + ) } }) } diff --git a/packages/remark-lint-unordered-list-marker-style/package.json b/packages/remark-lint-unordered-list-marker-style/package.json index f5420445..bc8c94c6 100644 --- a/packages/remark-lint-unordered-list-marker-style/package.json +++ b/packages/remark-lint-unordered-list-marker-style/package.json @@ -35,7 +35,8 @@ "@types/mdast": "^4.0.0", "unified-lint-rule": "^2.0.0", "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0" + "unist-util-visit-parents": "^6.0.0", + "vfile-message": "^4.0.0" }, "scripts": {}, "typeCoverage": { @@ -48,7 +49,7 @@ "prettier": true, "rules": { "capitalized-comments": "off", - "unicorn/prefer-string-replace-all": "off" + "unicorn/prefer-code-point": "off" } } } diff --git a/packages/remark-lint-unordered-list-marker-style/readme.md b/packages/remark-lint-unordered-list-marker-style/readme.md index 95bf37cb..d7705e63 100644 --- a/packages/remark-lint-unordered-list-marker-style/readme.md +++ b/packages/remark-lint-unordered-list-marker-style/readme.md @@ -21,8 +21,8 @@ * [Use](#use) * [API](#api) * [`unified().use(remarkLintUnorderedListMarkerStyle[, options])`](#unifieduseremarklintunorderedlistmarkerstyle-options) - * [`Marker`](#marker) * [`Options`](#options) + * [`Style`](#style) * [Recommendation](#recommendation) * [Fix](#fix) * [Examples](#examples) @@ -120,8 +120,8 @@ On the CLI in a config file (here a `package.json`): This package exports no identifiers. It exports the [TypeScript][typescript] types -[`Marker`][api-marker] and -[`Options`][api-options]. +[`Options`][api-options] and +[`Style`][api-style]. The default export is [`remarkLintUnorderedListMarkerStyle`][api-remark-lint-unordered-list-marker-style]. @@ -139,24 +139,24 @@ Warn when unordered list markers are inconsistent. Transform ([`Transformer` from `unified`][github-unified-transformer]). -### `Marker` +### `Options` -Marker (TypeScript type). +Configuration (TypeScript type). ###### Type ```ts -type Marker = '*' | '+' | '-' +type Options = Style | 'consistent' ``` -### `Options` +### `Style` -Configuration (TypeScript type). +Style (TypeScript type). ###### Type ```ts -type Options = Marker | 'consistent' +type Style = '*' | '+' | '-' ``` ## Recommendation @@ -178,18 +178,11 @@ Pass `bullet: '+'` or `bullet: '-'` to use a different marker. ###### In ```markdown -By default (`'consistent'`), if the file uses only one marker, -that’s OK. - -* Foo -* Bar -* Baz +* Mercury -Ordered lists are not affected. +1. Venus -1. Foo -2. Bar -3. Baz +* Earth ``` ###### Out @@ -203,7 +196,7 @@ When configured with `'*'`. ###### In ```markdown -* Foo +* Mercury ``` ###### Out @@ -217,7 +210,7 @@ When configured with `'-'`. ###### In ```markdown -- Foo +- Mercury ``` ###### Out @@ -231,7 +224,7 @@ When configured with `'+'`. ###### In ```markdown -+ Foo ++ Mercury ``` ###### Out @@ -243,26 +236,28 @@ No messages. ###### In ```markdown -* Foo -- Bar -+ Baz +* Mercury + +- Venus + ++ Earth ``` ###### Out ```text -2:1-2:6: Marker style should be `*` -3:1-3:6: Marker style should be `*` +3:1: Unexpected unordered list marker `-`, expected `*` +5:1: Unexpected unordered list marker `+`, expected `*` ``` ##### `not-ok.md` -When configured with `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect unordered list item marker style `💩`: use either `'-'`, `'*'`, or `'+'` +1:1: Unexpected value `🌍` for `options`, expected `'*'`, `'+'`, `'-'`, or `'consistent'` ``` ## Compatibility @@ -290,12 +285,12 @@ abide by its terms. [MIT][file-license] © [Titus Wormer][author] -[api-marker]: #marker - [api-options]: #options [api-remark-lint-unordered-list-marker-style]: #unifieduseremarklintunorderedlistmarkerstyle-options +[api-style]: #style + [author]: https://wooorm.com [badge-build-image]: https://github.com/remarkjs/remark-lint/workflows/main/badge.svg diff --git a/script/info.js b/script/info.js index bc61cacd..57a6aeb5 100644 --- a/script/info.js +++ b/script/info.js @@ -10,6 +10,8 @@ * Configuration. * @property {boolean} directive * Whether to use directives. + * @property {boolean} frontmatter + * Whether to use frontmatter. * @property {boolean} gfm * Whether to use GFM. * @property {string} input @@ -30,7 +32,9 @@ * @property {unknown} [config] * Configuration (optional). * @property {boolean} [directive] - * Whether to use directives. + * Whether to use directives (optional). + * @property {boolean} [frontmatter] + * Whether to use frontmatter (optional). * @property {boolean} [gfm] * Whether to use GFM (optional). * @property {'input' | 'output'} [label] @@ -108,6 +112,7 @@ for (const name of names) { * @returns {Promise} * Nothing. */ +// eslint-disable-next-line complexity async function addPlugin(name) { const code = await fs.readFile( new URL(name + '/index.js', packagesUrl), @@ -182,6 +187,7 @@ async function addPlugin(name) { result.checks.push({ configuration, directive: info.directive || false, + frontmatter: info.frontmatter || false, gfm: info.gfm || false, input: exampleValue, math: info.math || false, @@ -204,6 +210,7 @@ async function addPlugin(name) { found = { configuration, directive: info.directive || false, + frontmatter: info.frontmatter || false, gfm: info.gfm || false, input: '', math: info.math || false, @@ -216,8 +223,34 @@ async function addPlugin(name) { } if (info.label === 'input') { + /* c8 ignore next 11 -- just to be sure */ + if (found.input) { + console.log( + 'Duplicate input in `' + + ruleId + + '` for `' + + name + + '` w/ config `' + + info.config + + '`' + ) + } + found.input = exampleValue } else { + /* c8 ignore next 11 -- just to be sure */ + if (found.output.length > 0) { + console.log( + 'Duplicate output in `' + + ruleId + + '` for `' + + name + + '` w/ config `' + + info.config + + '`' + ) + } + found.output = exampleValue.split('\n') } } diff --git a/script/pipeline-package.js b/script/pipeline-package.js index 54320799..da09d712 100644 --- a/script/pipeline-package.js +++ b/script/pipeline-package.js @@ -1256,6 +1256,10 @@ function generateReadmeExample(state) { 'github-remark-directive', 'https://github.com/remarkjs/remark-directive' ) + state.urls.set( + 'github-remark-frontmatter', + 'https://github.com/remarkjs/remark-frontmatter' + ) state.urls.set( 'github-remark-gfm', 'https://github.com/remarkjs/remark-gfm' @@ -1296,6 +1300,23 @@ function generateReadmeExample(state) { ) } + if (check.frontmatter) { + if (phrasing.length > 0) { + phrasing.push({type: 'text', value: ',\n'}) + } + + phrasing.push( + {type: 'text', value: 'frontmatter ('}, + { + type: 'linkReference', + identifier: 'github-remark-frontmatter', + referenceType: 'full', + children: [{type: 'inlineCode', value: 'remark-frontmatter'}] + }, + {type: 'text', value: ')'} + ) + } + if (check.gfm) { if (phrasing.length > 0) { phrasing.push({type: 'text', value: ',\n'}) diff --git a/test.js b/test.js index 366be95d..cbc5715e 100644 --- a/test.js +++ b/test.js @@ -11,6 +11,7 @@ import test from 'node:test' import {controlPictures} from 'control-pictures' import {remark} from 'remark' import remarkDirective from 'remark-directive' +import remarkFrontmatter from 'remark-frontmatter' import remarkGfm from 'remark-gfm' import remarkLint from 'remark-lint' import remarkLintFinalNewline from 'remark-lint-final-newline' @@ -49,8 +50,8 @@ test('remark-lint', async function (t) { .process({path: 'virtual.md', value: doc}) assert.deepEqual(file.messages.map(String), [ - 'virtual.md:3:1-3:24: Don’t add a trailing `.` to headings', - 'virtual.md:3:1-3:24: Don’t use multiple top level headings (1:1)' + 'virtual.md:3:1-3:24: Unexpected character `.` at end of heading, remove it', + 'virtual.md:3:1-3:24: Unexpected duplicate toplevel heading, exected a single heading with rank `1`' ]) }) @@ -62,8 +63,8 @@ test('remark-lint', async function (t) { .process({path: 'virtual.md', value: doc}) assert.deepEqual(file.messages.map(String), [ - 'virtual.md:3:1-3:24: Don’t add a trailing `.` to headings', - 'virtual.md:3:1-3:24: Don’t use multiple top level headings (1:1)' + 'virtual.md:3:1-3:24: Unexpected character `.` at end of heading, remove it', + 'virtual.md:3:1-3:24: Unexpected duplicate toplevel heading, exected a single heading with rank `1`' ]) }) @@ -87,10 +88,12 @@ test('remark-lint', async function (t) { column: 2, fatal: true, line: 1, - message: 'Missing newline character at end of file', + message: + 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', name: '1:2', place: {column: 2, line: 1, offset: 1}, - reason: 'Missing newline character at end of file', + reason: + 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', ruleId: 'final-newline', source: 'remark-lint', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-newline#readme' @@ -102,7 +105,7 @@ test('remark-lint', async function (t) { const file = await remark().use(remarkLintFinalNewline, true).process('.') assert.deepEqual(file.messages.map(String), [ - '1:2: Missing newline character at end of file' + '1:2: Unexpected missing final newline character, expected line feed (`\\n`) at end of file' ]) }) @@ -120,7 +123,7 @@ test('remark-lint', async function (t) { .process('.') assert.deepEqual(file.messages.map(String), [ - '1:2: Missing newline character at end of file' + '1:2: Unexpected missing final newline character, expected line feed (`\\n`) at end of file' ]) } ) @@ -148,10 +151,12 @@ test('remark-lint', async function (t) { column: 2, fatal: true, line: 1, - message: 'Missing newline character at end of file', + message: + 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', name: '1:2', place: {column: 2, line: 1, offset: 1}, - reason: 'Missing newline character at end of file', + reason: + 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', ruleId: 'final-newline', source: 'remark-lint', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-newline#readme' @@ -172,10 +177,12 @@ test('remark-lint', async function (t) { column: 2, fatal: false, line: 1, - message: 'Missing newline character at end of file', + message: + 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', name: '1:2', place: {column: 2, line: 1, offset: 1}, - reason: 'Missing newline character at end of file', + reason: + 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', ruleId: 'final-newline', source: 'remark-lint', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-newline#readme' @@ -196,10 +203,12 @@ test('remark-lint', async function (t) { column: 2, fatal: false, line: 1, - message: 'Missing newline character at end of file', + message: + 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', name: '1:2', place: {column: 2, line: 1, offset: 1}, - reason: 'Missing newline character at end of file', + reason: + 'Unexpected missing final newline character, expected line feed (`\\n`) at end of file', ruleId: 'final-newline', source: 'remark-lint', url: 'https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-final-newline#readme' @@ -248,7 +257,7 @@ test('remark-lint', async function (t) { }) assert.deepEqual(file.messages.map(String), [ - 'virtual.md:3:1-3:9: Found reference to undefined definition' + 'virtual.md:3:1-3:9: Unexpected reference to undefined definition, expected corresponding definition (`b`) for a link or escaped opening bracket (`\\[`) for regular text' ]) } ) @@ -281,8 +290,8 @@ test('remark-lint', async function (t) { test('plugins', async function (t) { for (const plugin of plugins) { - await t.test(plugin.name, async function () { - await assertPlugin(plugin) + await t.test(plugin.name, async function (t) { + await assertPlugin(plugin, t) }) } }) @@ -290,16 +299,24 @@ test('plugins', async function (t) { /** * @param {PluginInfo} info * Info. + * @param {any} t + * Test context. * @returns {Promise} * Nothing. */ -async function assertPlugin(info) { +// type-coverage:ignore-next-line -- `TestContext` not exposed from `node:test`. +async function assertPlugin(info, t) { /** @type {{default: Plugin}} */ const pluginMod = await import(info.name) const plugin = pluginMod.default for (const check of info.checks) { - await assertCheck(plugin, info, check) + const name = check.name + ':' + check.configuration + + // type-coverage:ignore-next-line -- `TestContext` not exposed from `node:test`. + await t.test(name, async function () { + await assertCheck(plugin, info, check) + }) } } @@ -321,6 +338,7 @@ async function assertCheck(plugin, info, check) { const value = controlPictures(check.input) if (check.directive) extras.push(remarkDirective) + if (check.frontmatter) extras.push(remarkFrontmatter) if (check.gfm) extras.push(remarkGfm) if (check.math) extras.push(remarkMath) if (check.mdx) extras.push(remarkMdx)