From 45aeac273ab1865020522aa43389f0d5024252c7 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 19 Jan 2024 17:29:44 +0100 Subject: [PATCH] Add better messages, rewrite and improve rules --- package.json | 1 + .../index.js | 125 ++- .../package.json | 2 +- .../readme.md | 80 +- .../index.js | 179 ++-- .../package.json | 3 +- .../readme.md | 62 +- .../index.js | 94 +- .../package.json | 7 +- .../readme.md | 40 +- .../remark-lint-code-block-style/index.js | 147 +-- .../remark-lint-code-block-style/package.json | 3 +- .../remark-lint-code-block-style/readme.md | 122 +-- packages/remark-lint-definition-case/index.js | 59 +- .../remark-lint-definition-case/package.json | 3 +- .../remark-lint-definition-case/readme.md | 12 +- .../remark-lint-definition-spacing/index.js | 72 +- .../package.json | 5 +- .../remark-lint-definition-spacing/readme.md | 24 +- packages/remark-lint-emphasis-marker/index.js | 97 +- .../remark-lint-emphasis-marker/package.json | 3 +- .../remark-lint-emphasis-marker/readme.md | 31 +- .../remark-lint-fenced-code-flag/index.js | 138 ++- .../remark-lint-fenced-code-flag/package.json | 3 +- .../remark-lint-fenced-code-flag/readme.md | 72 +- .../remark-lint-fenced-code-marker/index.js | 115 ++- .../package.json | 3 +- .../remark-lint-fenced-code-marker/readme.md | 40 +- packages/remark-lint-file-extension/index.js | 40 +- packages/remark-lint-file-extension/readme.md | 18 +- .../remark-lint-final-definition/index.js | 131 ++- .../remark-lint-final-definition/package.json | 6 +- .../remark-lint-final-definition/readme.md | 76 +- packages/remark-lint-final-newline/index.js | 20 +- .../remark-lint-final-newline/package.json | 1 + packages/remark-lint-final-newline/readme.md | 6 +- .../remark-lint-first-heading-level/index.js | 182 ++-- .../package.json | 3 +- .../remark-lint-first-heading-level/readme.md | 124 +-- .../remark-lint-hard-break-spaces/index.js | 43 +- .../package.json | 3 +- .../remark-lint-hard-break-spaces/readme.md | 31 +- .../remark-lint-heading-increment/index.js | 170 +++- .../package.json | 5 +- .../remark-lint-heading-increment/readme.md | 62 +- packages/remark-lint-heading-style/index.js | 128 ++- .../remark-lint-heading-style/package.json | 3 +- packages/remark-lint-heading-style/readme.md | 34 +- packages/remark-lint-linebreak-style/index.js | 121 ++- .../remark-lint-linebreak-style/package.json | 4 +- .../remark-lint-linebreak-style/readme.md | 45 +- .../remark-lint-link-title-style/index.js | 241 ++--- .../remark-lint-link-title-style/package.json | 8 +- .../remark-lint-link-title-style/readme.md | 125 ++- .../index.js | 78 +- .../package.json | 3 +- .../readme.md | 22 +- .../index.js | 148 ++- .../package.json | 6 +- .../readme.md | 86 +- .../remark-lint-list-item-indent/index.js | 336 ++++--- .../remark-lint-list-item-indent/package.json | 6 +- .../remark-lint-list-item-indent/readme.md | 359 ++++--- .../remark-lint-list-item-spacing/index.js | 231 +++-- .../package.json | 4 +- .../remark-lint-list-item-spacing/readme.md | 117 +-- .../index.js | 54 +- .../package.json | 2 +- .../readme.md | 26 +- .../remark-lint-maximum-line-length/index.js | 272 +++--- .../package.json | 1 + .../remark-lint-maximum-line-length/readme.md | 171 +++- .../index.js | 164 +++- .../package.json | 8 +- .../readme.md | 65 +- .../index.js | 349 +++++-- .../package.json | 9 +- .../readme.md | 260 +++++- .../index.js | 61 +- .../package.json | 6 +- .../readme.md | 10 +- .../index.js | 76 +- .../package.json | 6 +- .../readme.md | 22 +- .../index.js | 94 +- .../package.json | 6 +- .../readme.md | 48 +- .../index.js | 70 +- .../package.json | 6 +- .../readme.md | 22 +- .../index.js | 74 +- .../package.json | 3 +- .../readme.md | 18 +- packages/remark-lint-no-empty-url/index.js | 41 +- .../remark-lint-no-empty-url/package.json | 3 +- packages/remark-lint-no-empty-url/readme.md | 20 +- .../index.js | 24 +- .../readme.md | 14 +- .../index.js | 5 +- .../readme.md | 2 +- .../index.js | 55 +- .../readme.md | 36 +- .../index.js | 15 +- .../readme.md | 8 +- .../index.js | 19 +- .../readme.md | 10 +- .../index.js | 193 ++-- .../package.json | 6 +- .../readme.md | 31 +- .../remark-lint-no-heading-indent/index.js | 85 +- .../package.json | 5 +- .../remark-lint-no-heading-indent/readme.md | 50 +- .../index.js | 68 +- .../package.json | 6 +- .../readme.md | 18 +- .../index.js | 125 ++- .../package.json | 3 +- .../readme.md | 96 +- packages/remark-lint-no-html/index.js | 71 +- packages/remark-lint-no-html/package.json | 3 +- packages/remark-lint-no-html/readme.md | 48 +- packages/remark-lint-no-literal-urls/index.js | 74 +- .../remark-lint-no-literal-urls/package.json | 7 +- .../remark-lint-no-literal-urls/readme.md | 21 +- .../index.js | 184 ++-- .../package.json | 6 +- .../readme.md | 97 +- .../index.js | 100 +- .../package.json | 6 +- .../readme.md | 54 +- .../index.js | 192 ++-- .../package.json | 8 +- .../readme.md | 73 +- .../index.js | 100 +- .../package.json | 4 +- .../readme.md | 26 +- .../remark-lint-no-shell-dollars/index.js | 69 +- .../remark-lint-no-shell-dollars/package.json | 6 +- .../remark-lint-no-shell-dollars/readme.md | 26 +- .../index.js | 29 +- .../package.json | 3 +- .../readme.md | 10 +- .../index.js | 29 +- .../package.json | 3 +- .../readme.md | 10 +- .../remark-lint-no-table-indentation/index.js | 159 ++-- .../package.json | 9 +- .../readme.md | 40 +- packages/remark-lint-no-tabs/index.js | 40 +- packages/remark-lint-no-tabs/readme.md | 29 +- .../index.js | 495 +++++----- .../package.json | 4 +- .../readme.md | 151 +-- .../index.js | 92 +- .../package.json | 2 +- .../readme.md | 57 +- .../index.js | 101 +- .../package.json | 2 +- .../readme.md | 63 +- .../index.js | 77 +- .../package.json | 2 +- .../readme.md | 20 +- .../index.js | 151 +-- .../package.json | 5 +- .../readme.md | 59 +- .../index.js | 367 +++++--- .../package.json | 7 +- .../readme.md | 245 +++-- packages/remark-lint-rule-style/index.js | 76 +- packages/remark-lint-rule-style/package.json | 3 +- packages/remark-lint-rule-style/readme.md | 6 +- .../remark-lint-strikethrough-marker/index.js | 111 ++- .../package.json | 3 +- .../readme.md | 51 +- packages/remark-lint-strong-marker/index.js | 102 +- .../remark-lint-strong-marker/package.json | 3 +- packages/remark-lint-strong-marker/readme.md | 78 +- .../remark-lint-table-cell-padding/index.js | 874 +++++++++++++----- .../package.json | 10 +- .../remark-lint-table-cell-padding/readme.md | 481 +++++++--- .../remark-lint-table-pipe-alignment/index.js | 532 +++++++++-- .../package.json | 9 +- .../readme.md | 192 +++- packages/remark-lint-table-pipes/index.js | 196 +++- packages/remark-lint-table-pipes/package.json | 3 +- packages/remark-lint-table-pipes/readme.md | 101 +- .../index.js | 157 ++-- .../package.json | 5 +- .../readme.md | 67 +- script/info.js | 35 +- script/pipeline-package.js | 21 + test.js | 56 +- 192 files changed, 9167 insertions(+), 4850 deletions(-) diff --git a/package.json b/package.json index 8d5a9a3..dc375ad 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 a6221df..3f4c553 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} - * - * > Hello - * - * Paragraph. - * - * > World - * @example - * {"name": "ok.md", "config": 2} - * - * > Hello - * - * Paragraph. - * - * > World * * @example - * {"name": "not-ok.md", "label": "input"} + * {"config": 2, "name": "ok-2.md"} * - * > Hello + * > Mercury. * - * Paragraph. + * Venus. * - * > World - * - * Paragraph. - * - * > World + * > Earth. * * @example - * {"name": "not-ok.md", "label": "output"} + * {"config": 4, "name": "ok-4.md"} * - * 5:5: Remove 1 space between block quote and content - * 9:3: Add 1 space between block quote and content + * > Mercury. + * + * Venus. + * + * > Earth. + * + * @example + * { "name": "ok-tab.md"} + * + * >␉Mercury. + * + * @example + * {"label": "input", "name": "not-ok.md"} + * + * > Mercury. + * + * Venus. + * + * > Earth. + * + * Mars. + * + * > Jupiter + * @example + * {"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 + * + * @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 - visit(tree, 'blockquote', function (node) { + 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'`" + ) + } + + 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 8921573..b59db7d 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 9f80fcb..8a1fd01 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` - -When configured with `4`. - -###### In - -```markdown -> Hello - -Paragraph. - -> World -``` - -###### Out - -No messages. - -##### `ok.md` +##### `ok-2.md` When configured with `2`. ###### In ```markdown -> Hello +> Mercury. -Paragraph. +Venus. -> World +> Earth. +``` + +###### Out + +No messages. + +##### `ok-4.md` + +When configured with `4`. + +###### In + +```markdown +> Mercury. + +Venus. + +> Earth. +``` + +###### Out + +No messages. + +##### `ok-tab.md` + +###### In + +```markdown +>␉Mercury. ``` ###### Out @@ -212,22 +224,32 @@ No messages. ###### In ```markdown -> Hello +> Mercury. -Paragraph. +Venus. -> World +> Earth. -Paragraph. +Mars. -> World +> Jupiter ``` ###### Out ```text -5:5: Remove 1 space between block quote and content -9:3: Add 1 space between block quote and content +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 +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 7cc101a..2655e40 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} - * - * - [x] List item - * - [x] List item * * @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": {"unchecked": " "}, "gfm": true} + * {"config": {"checked": "X"}, "gfm": true, "name": "ok-x-upper.md"} * - * - [ ] List item - * - [ ] List item + * - [X] Mercury. + * - [X] Venus. + * + * @example + * {"config": {"unchecked": " "}, "gfm": true, "name": "ok-space.md"} + * + * - [ ] Mercury. + * - [ ] Venus. * - [ ]␠␠ * - [ ] * * @example - * {"name": "ok.md", "config": {"unchecked": "\t"}, "gfm": true} + * {"config": {"unchecked": "\t"}, "gfm": true, "name": "ok-tab.md"} * - * - [␉] List item - * - [␉] List item + * - [␉] Mercury. + * - [␉] Venus. * * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} + * {"label": "input", "gfm": true, "name": "not-ok-default.md"} * - * - [x] List item - * - [X] List item - * - [ ] List item - * - [␉] List item + * - [x] Mercury. + * - [X] Venus. + * - [ ] Earth. + * - [␉] Mars. + * @example + * {"label": "output", "gfm": true, "name": "not-ok-default.md"} + * + * 2:5: Unexpected checked checkbox value `X`, expected `x` + * 4:5: Unexpected unchecked checkbox value `\t`, expected ` ` * * @example - * {"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 (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 (unchecked !== 'consistent' && unchecked !== ' ' && unchecked !== '\t') { + 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 unchecked checkbox marker `' + - unchecked + - "`: use either `'\\t'`, or `' '`" + 'Unexpected value `' + + options + + "` for `options`, expected an object or `'consistent'`" ) } - if (checked !== 'consistent' && checked !== 'x' && checked !== 'X') { - file.fail( - 'Incorrect checked checkbox marker `' + - checked + - "`: use either `'x'`, or `'X'`" - ) - } - - 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 3cd1697..5838979 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 35b519c..0fc7c4a 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 1a5f9d1..2d323e7 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} - * - * - [ ] List item - * + [x] List Item - * * [X] List item - * - [ ] List item * * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} + * {"gfm": true, "name": "ok.md"} * - * - [ ] List item - * + [x] List item - * * [X] List item - * - [ ] List item + * - [ ] Mercury. + * + [x] Venus. + * * [X] Earth. + * - [ ] Mars. * * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} + * {"gfm": true, "label": "input", "name": "not-ok.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. + * * [X] Earth. + * - [ ] Mars. + * @example + * {"gfm": true, "label": "output", "name": "not-ok.md"} + * + * 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 + * {"gfm": true, "label": "input", "name": "tab.md"} + * + * - [ ]␉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 597c9bd..e5910a1 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 b21309e..6ee51f7 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 de2ceba..c220e8f 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() - * - * Paragraph. - * - * bravo() - * - * @example - * {"config": "fenced", "name": "not-ok-fenced.md", "label": "output"} - * - * 1:1-1:12: Code blocks should be fenced - * 5:1-5:12: Code blocks should be fenced - * - * @example - * {"name": "not-ok-consistent.md", "label": "input"} - * - * alpha() - * - * Paragraph. + * {"config": "indented", "label": "input", "name": "not-ok-indented.md"} * * ``` - * bravo() + * venus() * ``` * - * @example - * {"name": "not-ok-consistent.md", "label": "output"} + * Mercury. * - * 5:1-7:4: Code blocks should be indented + * ``` + * earth() + * ``` + * @example + * {"config": "indented", "label": "output", "name": "not-ok-indented.md"} + * + * 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 - * {"config": "💩", "name": "not-ok-incorrect.md", "label": "output", "positionless": true} + * {"config": "fenced", "label": "input", "name": "not-ok-fenced.md"} * - * 1:1: Incorrect code block style `💩`: use either `'consistent'`, `'fenced'`, or `'indented'` + * venus() + * + * Mercury. + * + * earth() + * + * @example + * {"config": "fenced", "label": "output", "name": "not-ok-fenced.md"} + * + * 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": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true} + * + * 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 d129800..2316cfb 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 dc43b7b..e7a598c 100644 --- a/packages/remark-lint-code-block-style/readme.md +++ b/packages/remark-lint-code-block-style/readme.md @@ -180,50 +180,25 @@ 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` - -When configured with `'indented'`. - -###### In - -````markdown -``` -alpha() -``` - -Paragraph. - -``` -bravo() -``` -```` - -###### Out - -```text -1:1-3:4: Code blocks should be indented -7:1-9:4: Code blocks should be indented -``` - -##### `ok.md` +##### `ok-fenced.md` When configured with `'fenced'`. @@ -231,13 +206,13 @@ When configured with `'fenced'`. ````markdown ``` -alpha() +venus() ``` -Paragraph. +Mercury. ``` -bravo() +earth() ``` ```` @@ -245,6 +220,51 @@ bravo() No messages. +##### `not-ok-consistent.md` + +###### In + +````markdown + venus() + +Mercury. + +``` +earth() +``` +```` + +###### Out + +```text +5:1-7:4: Unexpected fenced code block, expected indented code blocks +``` + +##### `not-ok-indented.md` + +When configured with `'indented'`. + +###### In + +````markdown +``` +venus() +``` + +Mercury. + +``` +earth() +``` +```` + +###### Out + +```text +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-fenced.md` When configured with `'fenced'`. @@ -252,48 +272,28 @@ When configured with `'fenced'`. ###### In ```markdown - alpha() + 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-1:12: Unexpected indented code block, expected fenced code blocks +5:1-5:12: Unexpected indented code block, expected fenced code blocks ``` -##### `not-ok-consistent.md` +##### `not-ok-options.md` -###### In - -````markdown - alpha() - -Paragraph. - -``` -bravo() -``` -```` +When configured with `'🌍'`. ###### Out ```text -5:1-7:4: Code blocks should be indented -``` - -##### `not-ok-incorrect.md` - -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 bec81d6..16b717e 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 c5a153b..8c76c2e 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 4414f6c..1b40a60 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 610ac1d..d034f19 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 - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok-consecutive.md"} * - * [example␠␠␠␠domain]: http://example.com "Example Domain" + * [planet␠␠␠␠mercury]: http://example.com + * @example + * {"label": "output", "name": "not-ok-consecutive.md"} + * + * 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 271a417..d6cac90 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 3d3482f..525863c 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 d63cfa7..6108d14 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"} - * - * *foo* * * @example - * {"config": "*", "name": "not-ok.md", "label": "input"} + * {"config": "*", "name": "ok-asterisk.md"} * - * _foo_ + * *Mercury*. * * @example - * {"config": "*", "name": "not-ok.md", "label": "output"} + * {"config": "*", "label": "input", "name": "not-ok-asterisk.md"} * - * 1:1-1:6: Emphasis should use `*` as a marker + * _Mercury_. * * @example - * {"config": "_", "name": "ok.md"} + * {"config": "*", "label": "output", "name": "not-ok-asterisk.md"} * - * _foo_ + * 1:1-1:10: Unexpected emphasis marker `_`, expected `*` * * @example - * {"config": "_", "name": "not-ok.md", "label": "input"} + * {"config": "_", "name": "ok-underscore.md"} * - * *foo* + * _Mercury_. * * @example - * {"config": "_", "name": "not-ok.md", "label": "output"} + * {"config": "_", "label": "input", "name": "not-ok-underscore.md"} * - * 1:1-1:6: Emphasis should use `_` as a marker + * *Mercury*. * * @example - * {"name": "not-ok.md", "label": "input"} + * {"config": "_", "label": "output", "name": "not-ok-underscore.md"} * - * *foo* - * _bar_ + * 1:1-1:10: Unexpected emphasis marker `*`, expected `_` * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "input", "name": "not-ok-consistent.md"} * - * 2:1-2:6: Emphasis should use `*` as a marker + * *Mercury* and _Venus_. * * @example - * {"config": "💩", "name": "not-ok.md", "label": "output", "positionless": true} + * {"label": "output", "name": "not-ok-consistent.md"} * - * 1:1: Incorrect emphasis marker `💩`: use either `'consistent'`, `'*'`, or `'_'` + * 1:15-1:22: Unexpected emphasis marker `_`, expected `*` + * + * @example + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} + * + * 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) - if (option === 'consistent') { - option = marker - } else if (marker !== option) { - file.message('Emphasis should use `' + option + '` as a marker', node) + /* c8 ignore next -- should not happen. */ + if (actual !== '*' && actual !== '_') return + + 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 8a0344c..a41ff19 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 59349fd..1690a13 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 3d6b2f0..58f2a8e 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 + * {"label": "output", "name": "not-ok.md"} + * + * 1:1-3:4: Unexpected missing fenced code language flag in info string, expected keyword + * + * @example + * {"config": {"allowEmpty": true}, "name": "ok-allow-empty.md"} + * + * ``` + * mercury() * ``` * * @example - * {"name": "not-ok.md", "label": "output"} - * - * 1:1-3:4: Missing code language flag - * - * @example - * {"name": "ok.md", "config": {"allowEmpty": true}} + * {"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 + * {"config": ["markdown"], "name": "ok-array.md"} + * + * ```markdown + * # Mercury * ``` * * @example - * {"name": "not-ok.md", "config": {"allowEmpty": false}, "label": "input"} + * {"config": {"flags":["markdown"]}, "name": "ok-options.md"} * - * ``` - * alpha() + * ```markdown + * # Mercury * ``` * * @example - * {"name": "not-ok.md", "config": {"allowEmpty": false}, "label": "output"} + * {"config": ["markdown"], "label": "input", "name": "not-ok-array.md"} * - * 1:1-3:4: Missing code language flag - * - * @example - * {"name": "ok.md", "config": ["alpha"]} - * - * ```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": "ok.md", "config": {"flags":["alpha"]}} + * {"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": "input"} + * {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true} * - * ```alpha - * bravo() - * ``` - * - * @example - * {"name": "not-ok.md", "config": ["charlie"], "label": "output"} - * - * 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' + ) } - visit(tree, 'code', function (node) { + /** @type {string} */ + let allowedDisplay + + if (allowed) { + allowedDisplay = + allowed.length > 3 + ? listFormatUnit.format([...quotation(allowed.slice(0, 3), '`'), '…']) + : listFormat.format(quotation(allowed, '`')) + } else { + allowedDisplay = 'keyword' + } + + 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 e41be38..92a2cf2 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 05f6528..4bf0faa 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 `[ 'charlie' ]`. +When configured with `[ 'markdown' ]`. ###### In ````markdown -```alpha -bravo() +```javascript +mercury() ``` ```` ###### Out ```text -1:1-3:4: Incorrect code language flag +1:1-3:4: Unexpected fenced code language flag `javascript` in info string, expected `markdown` +``` + +##### `not-ok-long-array.md` + +When configured with `[ 'javascript', 'markdown', 'mdx', 'typescript' ]`. + +###### In + +````markdown +```html +

Mercury

+``` +```` + +###### Out + +```text +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 6e2808a..d23657c 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 + * {"label": "output", "name": "not-ok-consistent-tick.md"} + * + * 5:1-7:4: Unexpected fenced code marker `~`, expected `` ` `` * * @example - * {"name": "not-ok-consistent-tick.md", "label": "output"} + * {"label": "input", "name": "not-ok-consistent-tilde.md"} * - * 5:1-7:4: Fenced code should use `` ` `` as a marker - * - * @example - * {"name": "not-ok-consistent-tilde.md", "label": "input"} - * - * ~~~alpha - * bravo() + * ~~~javascript + * mercury() * ~~~ * * ``` - * charlie() + * venus() * ``` + * @example + * {"label": "output", "name": "not-ok-consistent-tilde.md"} + * + * 5:1-7:4: Unexpected fenced code marker `` ` ``, expected `~` * * @example - * {"name": "not-ok-consistent-tilde.md", "label": "output"} + * {"config": "🌍", "label": "output", "name": "not-ok-incorrect.md", "positionless": true} * - * 5:1-7:4: Fenced code should use `~` as a marker - * - * @example - * {"name": "not-ok-incorrect.md", "config": "💩", "label": "output", "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 9ba80a9..c4b542b 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 6889585..48636ee 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 ee876c9..a39a18b 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 c3f5c50..83a7913 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 252c140..5ee19f2 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. * - * [example]: http://example.com "Example Domain" + * [venus]: http://example.com * * @example - * {"name": "not-ok.md", "label": "input"} + * {"name": "ok.md"} * - * Paragraph. - * - * [example]: http://example.com "Example Domain" - * - * Another paragraph. - * - * @example - * {"name": "not-ok.md", "label": "output"} - * - * 3:1-3:47: Move definitions to the end of the file (after `5:19`) + * [mercury]: http://example.com/mercury/ + * [venus]: http://example.com/venus/ * * @example * {"name": "ok-html-comments.md"} * - * Paragraph. + * Mercury. * - * [example-1]: http://example.com/one/ + * [venus]: http://example.com/venus/ * - * + * * - * [example-2]: http://example.com/two/ + * [earth]: http://example.com/earth/ * * @example * {"name": "ok-mdx-comments.mdx", "mdx": true} * - * 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/ + * + * @example + * {"label": "input", "name": "not-ok.md"} + * + * Mercury. + * + * [venus]: https://example.com/venus/ + * + * Earth. + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 3:1-3:36: Unexpected definition before last content, expected definitions after line `5` + * + * @example + * {"gfm": true, "label": "input", "name": "gfm.md"} + * + * Mercury. + * + * [^venus]: + * **Venus** is the second planet from + * the Sun. + * + * Earth. + * @example + * {"gfm": true, "label": "output", "name": "gfm.md"} + * + * 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 (point && last && point.line < last.line) { - file.message( - 'Move definitions to the end of the file (after `' + - stringifyPosition(last) + - '`)', - node - ) + if (contentEnd) { + assert(content) // Always defined. + assert(contentAncestors) // Always defined. + + for (const definitionAncestors of definitionStacks) { + const definition = definitionAncestors.at(-1) + assert(definition) // Always defined. + + 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 77c964f..e93d269 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 3f3c300..7588df7 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 6de935a..a2d8091 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 52f26dc..c5e98a5 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 3a7ffff..92096ad 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 c31098d..1ef8e27 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 + * # Mercury + * + * @example + * {"name": "ok-delay.md"} + * + * Mercury. + * + * # Venus + * + * @example + * {"label": "input", "name": "not-ok.md"} + * + * ## Mercury + * + * Venus. + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 1:1-1:11: Unexpected first heading rank `2`, expected rank `1` + * + * @example + * {"config": 2, "name": "ok.md"} + * + * ## Mercury + * + * Venus. * * @example * {"name": "ok-html.md"} * - *

An HTML heading is also seen by this rule.

+ *
Mercury.
+ * + *

Venus

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

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

+ *

Venus

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

Charlie

- * - * Paragraph. - * - * @example - * {"name": "not-ok-html.md", "label": "output"} - * - * 1:1-1:17: First heading level should be `1` - * - * @example - * {"name": "ok.md", "config": 2} - * - * ## Delta - * - * Paragraph. - * - * @example - * {"name": "ok-html.md", "config": 2} - * - *

Echo

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

Golf

- * - * Paragraph. - * - * @example - * {"name": "not-ok-html.md", "config": 2, "label": "output"} - * - * 1:1-1:14: First heading level should be `2` - * - * @example - * {"mdx": true, "name": "ok.mdx"} - * - * 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 +## Mercury -Paragraph. +Venus. ``` ###### Out ```text -1:1-1:9: First heading level should be `1` -``` - -##### `not-ok-html.md` - -###### In - -```markdown -

Charlie

- -Paragraph. -``` - -###### 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,57 +214,19 @@ 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`. - -###### In - -```markdown -# Foxtrot - -Paragraph. -``` - -###### Out - -```text -1:1-1:10: First heading level should be `2` -``` - -##### `not-ok-html.md` - -When configured with `2`. - -###### In - -```markdown -

Golf

- -Paragraph. -``` - -###### Out - -```text -1:1-1:14: First heading level should be `2` -``` - -##### `ok.mdx` +##### `ok-mdx.mdx` ###### In @@ -314,15 +234,25 @@ Paragraph. > MDX ([`remark-mdx`][github-remark-mdx]). ```mdx -In MDX, JSX is supported. +
Mercury.
-

First heading

+

Venus

``` ###### Out No messages. +##### `not-ok-options.md` + +When configured with `'🌍'`. + +###### Out + +```text +1:1: Unexpected value `🌍` for `options`, expected `1`, `2`, `3`, `4`, `5`, or `6` +``` + ## 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 49f5eab..3d01158 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 - * {"name": "not-ok.md", "label": "output"} + * {"gfm": true, "label": "input", "name": "containers.md"} * - * 1:12-2:1: Use two spaces for hard line breaks + * [^mercury]: + * > * > * **Mercury** is the first planet from the Sun␠␠␠ + * > > and the smallest in the Solar System. + * @example + * {"gfm": true, "label": "output", "name": "containers.md"} + * + * 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) - if (slice.length > 2) { - file.message('Use two spaces for hard line breaks', node) + let actual = 0 + while (slice.charCodeAt(actual) === 32) actual++ + + 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 2968169..fafe5bb 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 b1b5911..2804c01 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 754b24a..8bb5e3e 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"} * - * # Charlie + * #### Impact basins and craters * - * ### Delta + * #### Plains + * + * #### Compressional features * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "input", "name": "not-ok.md"} * - * 3:1-3:10: Heading levels should increment by one level at a time + * # Mercury + * + * ### Internal structure + * + * ### Surface geology + * + * ## Observation history + * + * #### Mariner 10 * * @example - * {"name": "html.md"} + * {"label": "output", "name": "not-ok.md"} * - * In markdown, HTML is supported. - * - *

First heading

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

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 ba17bb0..f73ccb5 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 55551d0..c0bba9f 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 -### Delta +### Internal structure + +### 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 -

First heading

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

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 -

First heading

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

Internal structure

+ +

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 c1b1b27..76fc34c 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"} - * - * # Alpha - * - * ## Bravo - * - * ### Charlie * * @example - * {"name": "ok.md", "config": "atx-closed"} + * {"config": "atx", "name": "ok.md"} * - * # Delta ## + * # Mercury * - * ## Echo ## + * ## Venus * - * ### Foxtrot ### + * ### Earth * * @example - * {"name": "ok.md", "config": "setext"} + * {"config": "atx-closed", "name": "ok.md"} * - * Golf - * ==== + * # Mercury ## * - * Hotel - * ----- + * ## Venus ## * - * ### India + * ### Earth ### * * @example - * {"name": "not-ok.md", "label": "input"} + * {"config": "setext", "name": "ok.md"} * - * Juliett + * Mercury * ======= * - * ## Kilo + * Venus + * ----- * - * ### Lima ### + * ### Earth * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "input", "name": "not-ok.md"} * - * 4:1-4:8: Headings should use setext - * 6:1-6:13: Headings should use setext + * Mercury + * ======= + * + * ## Venus + * + * ### Earth ### + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 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 cf30747..117c854 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 eb0e3e9..d00d744 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 ef18622..959f20e 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 - * {"name": "not-ok-unix.md", "label": "input", "config": "unix", "positionless": true} + * {"config": "unix", "label": "input", "name": "not-ok-unix.md", "positionless": true} * - * Alpha␍␊ + * Mercury.␍␊ * * @example - * {"name": "not-ok-unix.md", "label": "output", "config": "unix"} + * {"config": "unix", "label": "output", "name": "not-ok-unix.md", "positionless": true} * - * 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 * * @example - * {"name": "not-ok-windows.md", "label": "input", "config": "windows", "positionless": true} + * {"config": "windows", "label": "input", "name": "not-ok-windows.md", "positionless": true} * - * Alpha␊ + * Mercury.␊ * * @example - * {"name": "not-ok-windows.md", "label": "output", "config": "windows"} + * {"config": "windows", "label": "output", "name": "not-ok-windows.md", "positionless": true} * - * 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 + * + * @example + * {"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.␊ + * + * @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 00d3ef9..5eea647 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 e1fd8d6..a35a80f 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 2a4564d..781f9d1 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" - * - * You can use parens in URLs if they’re not a title (see GH-166): - * - * [Example](#Heading-(optional)) * * @example - * {"name": "not-ok.md", "label": "input", "config": "\""} + * {"name": "ok-consistent.md"} * - * [Example]: http://example.com 'Example Domain' + * [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" * * @example - * {"name": "not-ok.md", "label": "output", "config": "\""} + * {"label": "input", "name": "not-ok-consistent.md"} * - * 1:31-1:47: Titles should use `"` as a quote + * [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 + * {"label": "output", "name": "not-ok-consistent.md"} + * + * 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') - * - * [Example]: http://example.com 'Example Domain' + * [Mercury](http://example.com/mercury/ "Go to Mercury"). * * @example - * {"name": "not-ok.md", "label": "input", "config": "'"} + * {"config": "\"", "label": "input", "name": "not-ok-double.md"} * - * [Example]: http://example.com "Example Domain" + * [Mercury](http://example.com/mercury/ 'Go to Mercury'). + * @example + * {"config": "\"", "label": "output", "name": "not-ok-double.md"} + * + * 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"} * - * [Example](http://example.com 'Example Domain') + * [Mercury](http://example.com/mercury/ (Go to Mercury)). * * @example - * {"name": "not-ok.md", "label": "output", "config": "()"} + * {"config": "()", "label": "input", "name": "not-ok-paren.md"} * - * 1:30-1:46: Titles should use `()` as a quote + * [Mercury](http://example.com/mercury/ "Go to Mercury"). + * @example + * {"config": "()", "label": "output", "name": "not-ok-paren.md"} + * + * 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"} * - * 2:30-2:46: Titles should use `"` as a quote + * Parens in URLs work correctly: + * + * [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"} * - * 1:1: Incorrect link title style marker `💩`: use either `'consistent'`, `'"'`, `'\''`, or `'()'` + * Trailing whitespace works correctly: + * + * [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) + // Exit w/o title. + if (!node.title) return + const end = pointEnd(node) + let endIndex = end ? end.offset : undefined - if ( - !begin || - !end || - typeof begin.offset !== 'number' || - typeof end.offset !== 'number' - ) { - return + // Exit w/o position. + if (!endIndex) return + + // `)` + if (node.type !== 'definition') endIndex-- + + // Whitespace. + let before = value.charCodeAt(endIndex - 1) + while (before === 9 || before === 32) { + endIndex-- + before = value.charCodeAt(endIndex - 1) } - let last = end.offset - 1 + /** @type {Style | undefined} */ + const actual = + before === 34 /* `"` */ + ? '"' + : before === 39 /* `'` */ + ? "'" + : before === 41 /* `)` */ + ? '()' + : /* c8 ignore next -- we should find a correct marker. */ + undefined - if (node.type !== 'definition') { - last-- - } + /* c8 ignore next -- we should find a correct marker. */ + if (!actual) return - const final = /** @type {keyof markers} */ (value.charAt(last)) - - // Exit if the final marker is not a known marker. - if (!(final in markers)) { - return - } - - const initial = markers[final] - - // Find the starting delimiter - const first = value.lastIndexOf(initial, last - 1) - - // 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 - } - - 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 be78fed..ddefa23 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 727eab0..bed2214 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,143 +175,179 @@ 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` + +###### 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 +[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) +``` + +###### Out + +```text +2:1-2:50: Unexpected title markers `'`, expected `"` +4:1-4:49: Unexpected title markers `'('` and `')'`, expected `"` +``` + +##### `ok-double.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" - -You can use parens in URLs if they’re not a title (see GH-166): - -[Example](#Heading-(optional)) +[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. + +##### `ok-whitespace.md` + +When configured with `'"'`. + +###### In + +```markdown +Trailing whitespace works correctly: + +[Mercury](http://example.com/mercury/␠"Go to Mercury"␠). ``` -##### `not-ok.md` - -When configured with `'💩'`. - ###### Out -```text -1:1: Incorrect link title style marker `💩`: use either `'consistent'`, `'"'`, `'\''`, or `'()'` -``` +No messages. ## Compatibility diff --git a/packages/remark-lint-list-item-bullet-indent/index.js b/packages/remark-lint-list-item-bullet-indent/index.js index 609089d..48d9f2c 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 e6119f3..61c7fe3 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 4eab252..5e9c6f1 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 2eacaa0..61cec61 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} - * - * 1.␠[x] Alpha - * ␠␠␠1. Bravo * * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} + * {"name": "ok.md"} * - * 1.␠[x] Charlie - * ␠␠␠␠1. Delta + * 1.␠Mercury. + * ␠␠␠*** + * ␠␠␠* Venus. * * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} + * {"label": "input", "name": "not-ok.md"} * - * 2:5: Don’t use mixed indentation for children, remove 1 space + * 1.␠Mercury. + * ␠␠␠␠␠*** + * ␠␠␠␠* Venus. + * @example + * {"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. + * ␠␠␠␠*** + * + * @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 + * {"label": "output", "gfm": true, "name": "gfm-nok.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 + * {"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 0557908..2706c92 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 7df9258..6df5d13 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 -> 👉 **Note**: this example uses -> GFM ([`remark-gfm`][github-remark-gfm]). - ```markdown -1.␠[x] Charlie -␠␠␠␠1. Delta +1.␠Mercury. +␠␠␠␠␠*** +␠␠␠␠* Venus. ``` ###### Out ```text -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 +``` + +##### `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] 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 +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 5d082c0..e127471 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. * - * 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": "ok.md", "config": "mixed"} + * {"config": "mixed", "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": "ok.md", "config": "one"} + * {"config": "mixed", "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": "mixed", "label": "output", "name": "not-ok.md"} + * + * 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 + * + * @example + * {"config": "one", "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. + * + * ␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter. + * + * @example + * {"config": "one", "label": "input", "name": "not-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. + * + * ␠␠␠␠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"} * - * *␠␠␠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": "tab", "label": "output", "name": "not-ok.md"} + * + * 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": "one", "label": "output"} + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} * - * 1:5: Incorrect list-item indent: remove 2 spaces + * 1:1: Unexpected value `🌍` for `options`, expected `'mixed'`, `'one'`, or `'tab'` * * @example - * {"name": "not-ok.md", "config": "tab", "label": "input"} + * {"config": "mixed", "gfm": true, "label": "input", "name": "gfm.md"} * - * *␠List - * ␠␠item. + * *␠[x] Mercury. + * + * 1.␠␠[ ] Venus. + * + * 2.␠␠[ ] Earth. * * @example - * {"name": "not-ok.md", "config": "tab", "label": "output"} + * {"config": "one", "gfm": true, "name": "gfm.md"} * - * 1:3: Incorrect list-item indent: add 2 spaces + * *␠[x] Mercury. + * + * 1.␠[ ] Venus. + * + * 2.␠[ ] Earth. * * @example - * {"name": "not-ok.md", "config": "mixed", "label": "input"} + * {"config": "tab", "gfm": true, "name": "gfm.md"} * - * *␠␠␠List item. + * *␠␠␠[x] Mercury. + * + * 1.␠␠[ ] Venus. + * + * 2.␠␠[ ] Earth. * * @example - * {"name": "not-ok.md", "config": "mixed", "label": "output"} + * {"config": "mixed", "name": "loose-tight.md"} * - * 1:5: Incorrect list-item indent: remove 2 spaces + * Loose lists have blank lines between items: * - * @example - * {"name": "not-ok.md", "config": "💩", "label": "output", "positionless": true} + * *␠␠␠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. + } else if (options === 'tab-size') { + file.fail( + 'Unexpected value `' + options + "` for `options`, expected `'tab'`" + ) + } else if (options === 'mixed' || options === 'one' || options === 'tab') { + expected = options + } else { + file.fail( + 'Unexpected value `' + + options + + "` for `options`, expected `'mixed'`, `'one'`, or `'tab'`" ) } - // @ts-expect-error: old name. - if (option === 'tab-size') { - file.fail( - 'Incorrect list-item indent style `' + option + "`: use `'tab'` instead" - ) - } + visitParents(tree, 'list', function (list, parents) { + let loose = list.spread - if (option !== 'mixed' && option !== 'one' && option !== 'tab') { - file.fail( - 'Incorrect list-item indent style `' + - option + - "`: use either `'mixed'`, `'one'`, or `'tab'`" - ) - } + if (!loose) { + for (const item of list.children) { + if (item.spread) { + loose = true + break + } + } + } - visit(tree, 'list', function (node) { - const spread = node.spread - let index = -1 - - 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) - const bulletSize = marker.replace(/\s+$/, '').length + // GFM tasklist. + const checkboxIndex = slice.indexOf('[') + if (checkboxIndex !== -1) slice = slice.slice(0, checkboxIndex) - const style = - option === 'tab' || (option === 'mixed' && spread) - ? Math.ceil(bulletSize / 4) * 4 - : bulletSize + 1 + const actualIndent = slice.length - if (marker.length !== style) { - const diff = style - marker.length - const abs = Math.abs(diff) + // 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 expectedSpaces = expectedIndent - end + const actualSpaces = actualIndent - end + + 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 0023f11..202aad5 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 cb51029..0d40d1b 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,113 +230,26 @@ 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` - -When configured with `'one'`. - -###### In - -```markdown -*␠List item. - -Paragraph. - -11.␠List item - -Paragraph. - -*␠List -␠␠item. - -*␠List -␠␠item. -``` - -###### Out - -No messages. - -##### `ok.md` - -When configured with `'tab'`. - -###### In - -```markdown -*␠␠␠List -␠␠␠␠item. - -Paragraph. - -11.␠List -␠␠␠␠item. - -Paragraph. - -*␠␠␠List -␠␠␠␠item. - -*␠␠␠List -␠␠␠␠item. -``` - -###### Out - -No messages. - -##### `not-ok.md` - -When configured with `'one'`. - -###### In - -```markdown -*␠␠␠List -␠␠␠␠item. -``` - -###### Out - -```text -1:5: Incorrect list-item indent: remove 2 spaces -``` - -##### `not-ok.md` - -When configured with `'tab'`. - -###### In - -```markdown -*␠List -␠␠item. -``` - -###### Out - -```text -1:3: Incorrect list-item indent: add 2 spaces -``` - ##### `not-ok.md` When configured with `'mixed'`. @@ -345,25 +257,250 @@ When configured with `'mixed'`. ###### 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 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 `'one'`. + +###### 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 `'one'`. + +###### 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 + +```text +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'`. + +###### 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 + +```text +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 `'💩'`. +When configured with `'🌍'`. ###### Out ```text -1:1: Incorrect list-item indent style `💩`: use either `'mixed'`, `'one'`, or `'tab'` +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 +*␠[x] Mercury. + +1.␠␠[ ] Venus. + +2.␠␠[ ] Earth. +``` + +###### Out + +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. +``` + +###### Out + +No messages. + +##### `gfm.md` + +When configured with `'tab'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +*␠␠␠[x] Mercury. + +1.␠␠[ ] Venus. + +2.␠␠[ ] Earth. +``` + +###### Out + +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 6ca1007..923fcb3 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: + * * Mercury. + * * Venus. * - * - item 1 - * - item 2 - * - item 3 + * + Mercury and + * Venus. * - * A loose list: - * - * - Wrapped - * item - * - * - item 2 - * - * - item 3 + * + Earth. * * @example - * {"name": "not-ok.md", "label": "input"} + * {"config": {"checkBlanks": true}, "name": "ok-check-blanks.md"} * - * 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. * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "input", "name": "not-ok.md"} * - * 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 + * * Mercury. + * + * * Venus. + * + * + Mercury and + * Venus. + * + Earth. + * + * * Mercury. + * + * Mercury is the first planet from the Sun and the smallest in the Solar + * System. + * * Earth. + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 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 * * @example - * {"name": "ok.md", "config": {"checkBlanks": true}} + * {"config": {"checkBlanks": true}, "label": "input", "name": "not-ok-blank.md"} * - * A tight list: + * * Mercury. * - * - item 1 - * - item 1.A - * - item 2 - * > Block quote + * * Venus. * - * A loose list: + * + Mercury and + * Venus. * - * - item 1 + * + Earth. * - * - item 1.A - * - * - 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"} + * {"config": {"checkBlanks": true}, "label": "output", "name": "not-ok-blank.md"} * - * A tight list: - * - * - item 1 - * - * - item 1.A - * - item 2 - * - * > Block quote - * - item 3 - * - * A loose list: - * - * - item 1 - * - item 1.A - * - * - item 2 - * > Block quote - * - * @example - * {"name": "not-ok.md", "config": {"checkBlanks": true}, "label": "output"} - * - * 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 - while (++index < node.children.length) { - const spaced = infer(node.children[index]) + for (const item of list.children) { + /** @type {boolean | null | undefined} */ + let spaced = false + + 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 && start) { - const spaced = start.line - previousEnd.line > 1 + if (previousEnd && itemStart) { + const actual = itemStart.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 7110006..4cdd812 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 8ce49cd..cf42477 100644 --- a/packages/remark-lint-list-item-spacing/readme.md +++ b/packages/remark-lint-list-item-spacing/readme.md @@ -174,20 +174,35 @@ all items must be loose. ###### In ```markdown -A tight list: +* Mercury. +* Venus. -- item 1 -- item 2 -- item 3 ++ Mercury and + Venus. -A loose list: ++ Earth. +``` -- Wrapped - item +###### Out -- item 2 +No messages. -- item 3 +##### `ok-check-blanks.md` + +When configured with `{ checkBlanks: true }`. + +###### In + +```markdown +* Mercury. +* Venus. + ++ Mercury + + Mercury is the first planet from the Sun and the smallest in the Solar + System. + ++ Earth. ``` ###### Out @@ -199,92 +214,58 @@ No messages. ###### In ```markdown -A tight list: +* Mercury. -- Wrapped - item -- item 2 -- item 3 +* Venus. -A loose list: ++ Mercury and + Venus. ++ Earth. -- item 1 +* Mercury. -- item 2 - -- item 3 + Mercury is the first planet from the Sun and the smallest in the Solar + System. +* 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 +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 ``` -##### `ok.md` +##### `not-ok-blank.md` When configured with `{ checkBlanks: true }`. ###### In ```markdown -A tight list: +* Mercury. -- item 1 - - item 1.A -- item 2 - > Block quote +* Venus. -A loose list: ++ Mercury and + Venus. -- item 1 ++ Earth. - - item 1.A +* Mercury. -- item 2 - - > Block quote -``` - -###### Out - -No messages. - -##### `not-ok.md` - -When configured with `{ checkBlanks: true }`. - -###### In - -```markdown -A tight list: - -- item 1 - - - item 1.A -- item 2 - - > Block quote -- item 3 - -A loose list: - -- item 1 - - item 1.A - -- 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 f4bfa14..b4395ab 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:42: Use headings shorter than `30` + * 1:1-1:50: Unexpected `40` characters in heading, expected at most `30` characters + * + * @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 0a2b8c7..301636a 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 1ecfc84..68cf81e 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:42: Use headings shorter than `30` +1:1-1:50: Unexpected `40` characters in heading, expected at most `30` characters +``` + +##### `not-ok.md` + +When configured with `'🌍'`. + +###### Out + +```text +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 1c2ba7b..f9d937c 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} - * - * This line is simply not toooooooooooooooooooooooooooooooooooooooooooo - * long. - * - * This is also fine: - * - * - * - * `alphaBravoCharlieDeltaEchoFoxtrotGolfHotelIndiaJuliettKiloLimaMikeNovemberOscarPapaQuebec.romeo()` - * - * [foo](http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables) - * - * - * - * ![foo](http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables) - * - * | An | exception | is | line | length | in | long | tables | because | those | can’t | just | - * | -- | --------- | -- | ---- | ------ | -- | ---- | ------ | ------- | ----- | ----- | ---- | - * | be | helped | | | | | | | | | | . | - * - *

alpha bravo charlie delta echo foxtrot golf

- * - * The following is also fine (note the `.`), because there is no whitespace. - * - * . - * - * In addition, definitions are also fine: - * - * [foo]: * * @example - * {"name": "not-ok.md", "config": 80, "label": "input", "positionless": true} + * {"name": "ok.md", "positionless": true} * - * This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo - * long. + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury mercury + * mercury. * - * Just like thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis one. + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury `mercury()`. * - * And this one is also very wrong: because the link starts aaaaaaafter the column: + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury . * - * and such. + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury [mercury](http://localhost). * - * And this one is also very wrong: because the code starts aaaaaaafter the column: `alpha.bravo()` + * Mercury mercury mercury mercury mercury mercury mercury mercury mercury ![mercury](http://localhost). * - * `alphaBravoCharlieDeltaEchoFoxtrotGolfHotelIndiaJuliettKiloLimaMikeNovemberOscar.papa()` and such. + *
Mercury mercury mercury mercury mercury mercury mercury mercury mercury
+ * + * [foo]: http://localhost/mercury/mercury/mercury/mercury/mercury/mercury/mercury/mercury * * @example - * {"name": "not-ok.md", "config": 80, "label": "output", "positionless": true} + * {"config": 20, "label": "input", "name": "not-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 + * Mercury mercury mercury + * mercury. + * + * Mercury mercury mercury `mercury()`. + * + * Mercury mercury mercury . + * + * Mercury mercury mercury [m](example.com). + * + * Mercury mercury mercury ![m](example.com). + * + * `mercury()` mercury mercury mercury. + * + * mercury. + * + * [m](example.com) mercury. + * + * ![m](example.com) mercury. + * + * Mercury mercury ![m](example.com) mercury. * * @example - * {"name": "ok-mixed-line-endings.md", "config": 10, "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 + * + * @example + * {"config": 20, "frontmatter": true, "name": "ok.md", "positionless": true} + * + * --- + * description: Mercury mercury mercury mercury. + * --- + * + * @example + * {"config": 20, "gfm": true, "name": "ok.md", "positionless": true} + * + * | Mercury | Mercury | Mercury | + * | ------- | ------- | ------- | + * + * @example + * {"config": 20, "math": true, "name": "ok.md", "positionless": true} + * + * $$ + * L = \frac{1}{2} \rho v^2 S C_L + * $$ + * + * @example + * {"config": 20, "mdx": true, "name": "ok.md", "positionless": true} + * + * export const description = 'Mercury mercury mercury mercury.' + * + * {description} + * + * @example + * {"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: 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 + * + * @example + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} + * + * 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 - // Allow nodes that cannot be wrapped. - visit(tree, function (node) { + if (options === null || options === undefined) { + // Empty. + } else if (typeof options === 'number') { + expected = options + } else { + file.fail( + 'Unexpected value `' + options + '` for `options`, expected `number`' + ) + } + + // 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++] = '' + } } + + return SKIP } - }) - - // Allow text spans to cross the border. - visit(tree, function (node, index, parent) { - const final = pointEnd(node) - const initial = pointStart(node) + // 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) + + 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 + } + + let line = start.line - 1 + while (line < end.line) { + lines[line++] = '' + } } - - 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 - } - - allowList(initial.line - 1, final.line) } }) @@ -210,29 +264,25 @@ const remarkLintMaximumLineLength = lintRule( let index = -1 while (++index < lines.length) { - const lineLength = lines[index].length + const actualBytes = lines[index].length + const actualCharacters = Array.from(lines[index]).length + const difference = actualCharacters - expected - if (lineLength > option) { - file.message('Line must be at most ' + option + ' characters', { - line: index + 1, - column: lineLength + 1 - }) - } - } - - /** - * 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 cf95306..fb2f141 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 f04ec3b..3369e89 100644 --- a/packages/remark-lint-maximum-line-length/readme.md +++ b/packages/remark-lint-maximum-line-length/readme.md @@ -151,38 +151,21 @@ Whether to wrap prose or not is a stylistic choice. ###### In -> 👉 **Note**: this example uses -> GFM ([`remark-gfm`][github-remark-gfm]). - ```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 mercury mercury mercury mercury mercury mercury `mercury()`. - +Mercury mercury mercury mercury mercury mercury mercury mercury mercury . -`alphaBravoCharlieDeltaEchoFoxtrotGolfHotelIndiaJuliettKiloLimaMikeNovemberOscarPapaQuebec.romeo()` +Mercury mercury mercury mercury mercury mercury mercury mercury mercury [mercury](http://localhost). -[foo](http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables) +Mercury mercury mercury mercury mercury mercury mercury mercury mercury ![mercury](http://localhost). - +
Mercury 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) - -| An | exception | is | line | length | in | long | tables | because | those | can’t | just | -| -- | --------- | -- | ---- | ------ | -- | ---- | ------ | ------- | ----- | ----- | ---- | -| be | helped | | | | | | | | | | . | - -

alpha bravo charlie delta echo foxtrot golf

- -The following is also fine (note the `.`), because there is no whitespace. - -. - -In addition, definitions are also fine: - -[foo]: +[foo]: http://localhost/mercury/mercury/mercury/mercury/mercury/mercury/mercury/mercury ``` ###### Out @@ -191,35 +174,123 @@ No messages. ##### `not-ok.md` -When configured with `80`. +When configured with `20`. ###### In ```markdown -This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo -long. +Mercury mercury mercury +mercury. -Just like thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis one. +Mercury mercury mercury `mercury()`. -And this one is also very wrong: because the link starts aaaaaaafter the column: +Mercury mercury mercury . - and such. +Mercury mercury mercury [m](example.com). -And this one is also very wrong: because the code starts aaaaaaafter the column: `alpha.bravo()` +Mercury mercury mercury ![m](example.com). -`alphaBravoCharlieDeltaEchoFoxtrotGolfHotelIndiaJuliettKiloLimaMikeNovemberOscar.papa()` and such. +`mercury()` mercury mercury mercury. + + mercury. + +[m](example.com) mercury. + +![m](example.com) mercury. + +Mercury mercury ![m](example.com) mercury. ``` ###### 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 +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 ``` +##### `ok.md` + +When configured with `20`. + +###### In + +> 👉 **Note**: this example uses +> frontmatter ([`remark-frontmatter`][github-remark-frontmatter]). + +```markdown +--- +description: Mercury mercury mercury mercury. +--- +``` + +###### Out + +No messages. + +##### `ok.md` + +When configured with `20`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| Mercury | Mercury | Mercury | +| ------- | ------- | ------- | +``` + +###### Out + +No messages. + +##### `ok.md` + +When configured with `20`. + +###### In + +> 👉 **Note**: this example uses +> math ([`remark-math`][github-remark-math]). + +```markdown +$$ +L = \frac{1}{2} \rho v^2 S C_L +$$ +``` + +###### Out + +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 09c4904..fef5550 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 - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * > Foo… - * …bar… - * > …baz. + * > Mercury, + * Venus, + * > and Earth. + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 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"} * - * >␉Foo… - * ␉…bar… - * …baz. + * * > Mercury and + * Venus. * + * > * Mercury and + * Venus. + * + * * > * 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 + // Only paragraphs can be lazy. + visitParents(tree, 'paragraph', function (node, parents) { + let expected = 0 - while (++index < node.children.length) { - const child = node.children[index] - const start = pointStart(child) - const end = pointEnd(child) + 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 (child.type === 'paragraph' && start && end) { - const column = start.column - let line = start.line + if (!expected) return SKIP - // Skip past the first line. - while (++line <= end.line) { - const offset = loc.toOffset({line, column}) + const end = pointEnd(node) + const start = pointStart(node) - if ( - typeof offset !== 'number' || - />[\t ]+$/.test(value.slice(offset - 5, offset)) - ) { - continue - } + if (!end || !start) return SKIP - // Roughly here. - file.message('Missing marker in block quote', { - line, - column: column - 2 - }) + 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 4ebe09c..8f9d64f 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": "^6.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 ec9d1c0..56affb3 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:1: Missing marker in block quote -3:1: Missing marker in block quote +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: 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 d8121b0..2e84cb5 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"} * - * Foo…␊␊␊…Bar␊␊␊ + * > 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 - * {"name": "not-ok.md", "label": "output"} + * {"directive": true, "label": "input", "name": "directive.md"} * - * 4:1: Remove 1 line before node - * 4:5: Remove 2 lines after node + * :::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. + * + * *** + * + * * + * Mercury. + * @example + * {"label": "output", "name": "list-item.md"} + * + * 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) - // Compare between each child. - let index = -1 - - while (++index < node.children.length) { - const previous = node.children[index - 1] - const child = node.children[index] - const previousEnd = pointEnd(previous) - const childStart = pointStart(child) - - if (previous && previousEnd && childStart) { - compare(previousEnd, childStart, 2) - } - } - - 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) + 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 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 + const next = siblings[index + 1] + const end = pointEnd(node) + const nextStart = pointStart(next) - if (lines > 0) { - file.message( - 'Remove ' + - lines + - ' ' + - plural('line', Math.abs(lines)) + - ' ' + - (diff > 0 ? 'before' : 'after') + - ' node', - end - ) + // 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 (difference > 0) { + const actual = difference + 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} + ) + } } - } + + 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 b45ecd4..8198a38 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 69ef2c4..f64d0a4 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. -##### `empty-document.md` - -###### Out - -No messages. - ##### `not-ok.md` ###### In ```markdown -Foo…␊␊␊…Bar␊␊␊ +# Planets + + +Mercury. + + + +Venus. ``` ###### Out ```text -4:1: Remove 1 line before node -4:5: Remove 2 lines after node +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. + +##### `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 +:::mercury +Venus. + + +Earth. +::: +``` + +###### Out + +```text +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 5366738..70438c3 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 (place && start && node.url) { - const url = String(node.url).toUpperCase() - const duplicate = map.get(url) + if (node.position && node.url) { + const urlNormal = String(node.url).toUpperCase() + const duplicateAncestors = map.get(urlNormal) + + 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 2a88b46..d5aea60 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 d6f17a4..f78b57c 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 8966e72..510f394 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 9bcc715..68124ad 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 2917bf6..a10bc72 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 2c5a5f4..7346e5f 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"} * - * ## Foxtrot + * # Planets * - * ### Golf + * ## Mars * - * ### Golf + * ### Discovery + * + * ### 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 9b8399c..beebad9 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 751697a..c14cb25 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 -### Golf +## Mars -### Golf +### Discovery + +### 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 f54e2c6..36c6409 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 + * # Mercury * - * ## Foo - * - * ## [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) + const ancestors = [...parents, node] + const value = toString(node).toLowerCase() + const duplicateAncestors = map.get(value) - if (place && start) { - const value = toString(node).toLowerCase() - const duplicate = map.get(value) + if (node.position && duplicateAncestors) { + const duplicate = duplicateAncestors.at(-1) + assert(duplicate) // Always defined. - if (duplicate) { - file.message( - 'Do not use headings with similar content (' + duplicate + ')', - node - ) - } - - 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 ccf97af..40ee249 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 29c6eb9..15edf41 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 02f8292..a5668b3 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"} + * {"label": "input", "name": "not-ok.md"} * - * *Foo* + * **Mercury** * - * Bar. + * **Mercury** is the first planet from the Sun and the smallest in the Solar + * System. * - * __Qux__ - * - * 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 0105106..f251a60 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 9d3a2d7..a8b35a5 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 941d8b5..7dc7c21 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). + * [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/ * * @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 36a6bea..4d7a864 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 52c96f8..30c48bc 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 18302b8..4fa8448 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} + * {"label": "output", "name": "a-title.md", "positionless": true} * - * 1:1: Do not start file names with `a` + * 1:1: Unexpected file name starting with `a`, remove it * * @example - * {"name": "the-title.md", "label": "output", "positionless": true} + * {"label": "output", "name": "the-title.md", "positionless": true} * - * 1:1: Do not start file names with `the` + * 1:1: Unexpected file name starting with `the`, remove it * * @example - * {"name": "teh-title.md", "label": "output", "positionless": true} + * {"label": "output", "name": "an-article.md", "positionless": true} * - * 1:1: Do not start file names with `teh` - * - * @example - * {"name": "an-article.md", "label": "output", "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 cddb546..88fff6c 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 39067d5..c40aebd 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 89d3bd1..1c30ec5 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 dc42679..11c8d78 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": "plug-ins.md"} * * @example - * {"name": "plugins.md"} + * {"name": "mercury-and-venus.md"} * * @example - * {"name": "plug_ins.md", "label": "output", "positionless": true} - * - * 1:1: Do not use `_` in a file name + * {"name": "mercury.md"} * * @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": "\\.a-z0-9", "label": "output", "name": "Readme.md", "positionless": true} * - * 1:1: Do not use ` ` in a file name + * 1:1: Unexpected character `R` in file name + * + * @example + * {"config": {"source": "[^\\.a-z0-9]"}, "label": "output", "name": "mercury_and_venus.md", "positionless": true} + * + * 1:1: Unexpected character `_` in file name + * + * @example + * {"label": "output", "name": "mercury and venus.md", "positionless": true} + * + * 1:1: Unexpected character ` ` in file name + * + * @example + * {"config": 1, "label": "output", "name": "not-ok-options.md", "positionless": true} + * + * 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 0001581..ecc2297 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: Do not use ` ` in a file name +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: 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 024bbab..58c4649 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"} * * @example - * {"name": "readme.md"} + * {"name": "MERCURY.md"} * * @example - * {"name": "Readme.md", "label": "output", "positionless": true} + * {"name": "mercury.md"} * - * 1:1: Do not mix casing in file names + * @example + * {"label": "output", "name": "Mercury.md", "positionless": true} + * + * 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 0d6ac87..8c8d85f 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 392ad06..121e9a2 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"} * * @example - * {"name": "-readme.md", "label": "output", "positionless": true} - * - * 1:1: Do not use initial or final dashes in a file name + * {"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 + * {"label": "output", "name": "venus-.md", "positionless": true} + * + * 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 33fe34f..3f360b1 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 6404f02..6bbdc9b 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 + * ===== + * + * ␠Jupiter + * -------- * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * #␠␠Foo + * #␠␠Mercury * - * ## Bar␠␠## + * ##␠Venus␠␠## * - * ##␠␠Baz + * ␠␠##␠␠␠Earth + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 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": "not-ok.md", "label": "output"} - * - * 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 - * - * @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') + const end = pointEnd(node) - 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]) - ) - } + 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) + 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 - /* c8 ignore next -- we get here if we have offsets. */ - if (!final || !end) return + while (value.charCodeAt(index) === 35 /* `#` */) { + index++ + found = true + continue + } - const diff = end.column - final.column - 1 - node.depth + const from = index - if (diff) { - file.message( - 'Remove ' + - diff + - ' ' + - plural('space', diff) + - ' after this heading’s content', - final - ) - } + 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 792fdcf..9b594f4 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 807a0ee..34f153a 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 70e88f1..578cd67 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"} + * {"label": "input", "name": "not-ok.md"} * - * ␠␠␠# Hello world + * ␠␠␠# Mercury * - * ␠Foo - * ----- + * ␠Venus + * ------ * - * ␠# Hello world # - * - * ␠␠␠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 d2b7484..8db3af9 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 e40178b..c8c1e99 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 d4b3640..8f8232a 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"} * - * ####### Charlie + * ####### Venus * - * Delta. + * Mercury. * + * ######## 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 (place) { - 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 (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 c3345b6..164f414 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 00f9b51..c8b717f 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 c744b42..3fdb522 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: + * + * # Venus? + * + * # Earth! + * + * # Mars, + * + * # Jupiter; + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 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 * * @example - * {"name": "not-ok.md", "label": "input"} + * {"config": ",;:!?", "name": "ok.md"} * - * # Hello: - * - * # Hello? - * - * # Hello! - * - * # Hello, - * - * # Hello; + * # Mercury… * * @example - * {"name": "not-ok.md", "label": "output"} + * {"config": {"source": "[^A-Za-z0-9]"}, "label": "input", "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 + * # Mercury! + * @example + * {"config": {"source": "[^A-Za-z0-9]"}, "label": "output", "name": "regex.md"} + * + * 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) + const tail = Array.from(toString(node)).at(-1) - if (place) { - 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 ba2a84b..79dc9af 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": "^6.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 f291285..328a1d8 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,21 +141,7 @@ Transform ([`Transformer` from `unified`][github-unified-transformer]). ###### In ```markdown -# Hello -``` - -###### Out - -No messages. - -##### `ok.md` - -When configured with `',;:!?'`. - -###### In - -```markdown -# Hello… +# Mercury ``` ###### Out @@ -167,28 +153,58 @@ No messages. ###### In ```markdown -# Hello: +# Mercury: -# Hello? +# Venus? -# Hello! +# Earth! -# Hello, +# Mars, -# Hello; +# Jupiter; ``` ###### 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 +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 ``` -##### `mdx.mdx` +##### `ok.md` + +When configured with `',;:!?'`. + +###### In + +```markdown +# Mercury… +``` + +###### Out + +No messages. + +##### `regex.md` + +When configured with `{ source: '[^A-Za-z0-9]' }`. + +###### In + +```markdown +# Mercury! +``` + +###### Out + +```text +1:1-1:11: Unexpected character `!` at end of heading, remove it +``` + +##### `example.mdx` ###### In @@ -196,15 +212,23 @@ No messages. > MDX ([`remark-mdx`][github-remark-mdx]). ```mdx -MDX is supported too. - -

Hi?

+

Mercury?

``` ###### Out ```text -3:1-3:13: Don’t add a trailing `?` to headings +1:1-1:18: Unexpected character `?` at end of heading, remove it +``` + +##### `not-ok-options.md` + +When configured with `1`. + +###### Out + +```text +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 1e49386..ddb650d 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 - * {"name": "not-ok.md", "label": "output"} + * {"config": {"allowComments": false}, "label": "input", "name": "not-ok.md"} * - * 1:1-1:15: Do not use HTML in markdown + * + * @example + * {"config": {"allowComments": false}, "label": "output", "name": "not-ok.md"} + * + * 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:15: Do not use HTML in markdown +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: 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 aefd2b5..fd71c75 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"} * - * + * @example + * {"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 821efb5..3adddc9 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 c42aaa9..e334621 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 8e95f5d..82a4aa9 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. - * - * @example - * {"name": "not-ok.md", "label": "input"} - * - * # Foo - * ## Bar - * - * - Paragraph - * + List. - * - * Paragraph. - * - * @example - * {"name": "not-ok.md", "label": "output"} - * - * 2:1-2:7: Missing blank line before block node - * 5:3-5:10: Missing blank line before block node - * - * @example - * {"name": "tight.md", "config": {"exceptTightLists": true}, "label": "input"} - * - * # Foo - * ## Bar - * - * - Paragraph - * + List. - * - * Paragraph. - * - * @example - * {"name": "tight.md", "config": {"exceptTightLists": true}, "label": "output"} - * - * 2:1-2:7: Missing blank line before block node - * - * @example - * {"name": "containers.md", "label": "input"} - * - * > # Alpha + * > # Jupiter * > - * > Bravo. + * > Saturn. * - * - charlie. - * - delta. - * - * + # Echo - * Foxtrot. * @example - * {"name": "containers.md", "label": "output"} + * {"label": "input", "name": "not-ok.md"} * - * 9:3-9:11: Missing blank line before block node + * # Mercury + * ## Venus + * + * * Earth + * * Mars. + * + * > # Jupiter + * > Saturn. + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 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 + * {"config": {"exceptTightLists": true}, "name": "tight.md"} + * + * * Venus. + * + * * Mars. + * + * @example + * {"label": "input", "name": "containers.md"} + * + * > # Venus + * > + * > Mercury. + * + * - earth. + * - mars. + * + * * # Jupiter + * Saturn. + * @example + * {"label": "output", "name": "containers.md"} + * + * 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 6f29520..ffbc1fd 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 252d828..45e9e2a 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 +* Venus. -- Paragraph - + List. - -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] +| Planet | Diameter | +| ------- | -------- | +| Mercury | 4 880 km | -| Alpha | Bravo | -| ------- | ----- | -| Charlie | Delta | - -[^e]: Echo -[^f]: Foxtrot. +[^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 aff1004..1378987 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} - * - * # Foo - * - * ## Bar * * @example - * {"name": "not-ok.md", "config": 1, "label": "input"} + * {"name": "ok.md"} * - * # Foo + * # Mercury * - * # Bar + * ## Venus * * @example - * {"name": "not-ok.md", "config": 1, "label": "output"} + * {"label": "input", "name": "not-ok.md"} * - * 3:1-3:6: Don’t use multiple top level headings (1:1) + * # Venus + * + * # Mercury + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 3:1-3:10: Unexpected duplicate toplevel heading, exected a single heading with rank `1` + * + * @example + * {"config": 2, "label": "input", "name": "not-ok.md"} + * + * ## 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 a1241fc..bb42ca0 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": "^6.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 2543b47..c35bfca 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 -# Foo +# Venus -# Bar +# Mercury ``` ###### Out ```text -3:1-3:6: Don’t use multiple top level headings (1:1) +3:1-3:10: Unexpected duplicate toplevel heading, exected a single heading with rank `1` +``` + +##### `not-ok.md` + +When configured with `2`. + +###### In + +```markdown +## Venus + +## Mercury +``` + +###### Out + +```text +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 62c9425..c46e310 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 + * 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. * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * ␠Alpha + * ␠Mercury. * - * Bravo - * ␠Charlie. + * Venus and + * ␠␠**Earth**. * - * * Delta - * ␠Echo. + * * Mars and + * ␠␠Jupiter. * - * > Foxtrot - * > ␠Golf. + * > Saturn and + * > ␠Uranus. * - * `hotel()` - * ␠india. + * * Neptune + * and + * ␠␠Pluto. * - * - `juliett()` - * ␠kilo. - * - * ␠![lima]() mike - * - * * november - * oscar - * ␠papa. + * > Ceres + * and + * > ␠Makemake. * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 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 */ /** * @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) - visit(tree, 'paragraph', function (node, _, parent) { - const end = pointEnd(node)?.line - let line = pointStart(node)?.line + // 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 + + 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 + + // Skip past the first line if we’re the first child of a list item. + if (parentHead === node) { + line++ + } } } - // In a parent we don’t know, exit. - if (!column || !line || !end) { - return - } + /* c8 ignore next -- unknown parent. */ + if (!column) return - while (line <= end) { - let offset = loc.toOffset({line, column}) - const lineColumn = offset + while (line <= end.line) { + let index = locations.toOffset({line, column}) - /* c8 ignore next 3 -- we get here if we have offsets. */ - if (typeof lineColumn !== 'number' || typeof offset !== 'number') { - continue + /* 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) } - while (/[ \t]/.test(value.charAt(offset - 1))) { - offset-- - } + if ( + code === 10 /* `\n` */ || + code === 13 /* `\r` */ || + code === 62 /* `>` */ || + Number.isNaN(code) + ) { + // Now check superfluous indent. + let actual = expected - // 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 + code = value.charCodeAt(actual) - while (/[ \t]/.test(value.charAt(offset))) { - offset++ + 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 bf10d9e..8b69226 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 a1a4970..c33db6c 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 +␠Mercury. -Bravo -␠Charlie. +Venus and +␠␠**Earth**. -* Delta - ␠Echo. +* Mars and + ␠␠Jupiter. -> Foxtrot -> ␠Golf. +> Saturn and +> ␠Uranus. -`hotel()` -␠india. +* Neptune +and + ␠␠Pluto. -- `juliett()` - ␠kilo. - -␠![lima]() mike - -* 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 08c4d4c..024cd56 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"} * - * [Charlie](delta). + * [**Mercury**](mercury) is the first planet from the sun. * - * [delta]: https://example.com + * [mercury]: https://example.com/mercury/ + * @example + * {"label": "output", "name": "not-ok.md"} + * + * 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()) - }) - - visit(tree, function (node) { - const place = position(node) - - 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 - ) + 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]) } }) + + 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' ? '!' : '' + + file.message( + '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 714852d..11d8f04 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": "^6.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 3b6d03c..1a8fcd5 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 4dcc3ab..8e558b8 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 616b17b..8a7bd4c 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 05ffc96..0512237 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 9abafe9..b3915cd 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"} + * {"label": "input", "name": "not-ok.md"} * - * ![foo] - * - * [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 72181e9..dae7694 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 fd98948..db044a3 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 6fd1b48..65d91e8 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"} + * {"label": "input", "name": "not-ok.md"} * - * [foo] - * - * [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 8cb5f4d..714680a 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 7490836..7dc92a1 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 603cab0..48d7a4c 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} + * {"gfm": true, "label": "input", "name": "not-ok.md"} * - * Paragraph. - * - * ␠␠␠| 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) + + 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 (!start || !end) return - - let line = start.line - - 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 + } else if (parent.type === 'blockquote') { + const parentStart = pointStart(parent) - /* 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++ + 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 + + // 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) } - const lineColumn = offset - while (/[ \t]/.test(value.charAt(offset - 1))) { - offset-- - } + if ( + code === 10 /* `\n` */ || + code === 13 /* `\r` */ || + code === 62 /* `>` */ || + Number.isNaN(code) + ) { + // Now check superfluous indent. + let actual = expected - if (!offset || /[\r\n>]/.test(value.charAt(offset - 1))) { - offset = lineColumn + 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 6d5c8be..bbfd20f 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 4d38992..2feb35d 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 c2f2d16..f2d3f7b 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} + * {"label": "input", "name": "not-ok.md", "positionless": true} * - * ␉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. - * - * And this is a tab as the last character.␉ + * ␉mercury() * + * Venus␉and Earth. * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 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 */ /** @@ -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 4c7c9b7..ed795a5 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. +␉mercury() -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. - -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 da0a103..3d43bd0 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][] + * [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 - * - * @example - * {"config": {"allow": ["…"]}, "name": "ok-allow.md"} - * - * > Eliding a portion of a quoted passage […] is acceptable. - * - * @example - * {"config": {"allow": ["a", {"source": "^b\\."}]}, "name": "ok-allow-source.md"} - * - * [foo][b.c] - * - * [bar][a] - * - * 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"} * - * 1:1-1:11: Found reference to undefined definition - * 3:1-3:9: Found reference to undefined definition + * [Mercury][] is the first planet from the Sun and the smallest in the Solar + * System. + * + * [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 defined = new Set() + const definitionIdentifiers = new Set() + /** @type {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 = [] + 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. - 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 + 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) - if (next === 91 || next === 93) { - index++ - } - } - // Opening bracket. - else if (code === 91) { - ranges.push([lines[lineIndex][0] + index]) - index++ - } - // Close bracket. - else if (code === 93) { - // No opening. - if (ranges.length === 0) { - index++ - } else if (line.charCodeAt(index + 1) === 91) { - 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) - // Collapsed or full. - let range = ranges.pop() + index++ - // 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) - } - } - } - // Anything else. - else { + if (next === 91 /* `[` */ || next === 93 /* `]` */) { index++ } } - } - }) + // Close bracket. + else if (code === 93 /* `]` */) { + const bracketInfo = ranges.at(-1) - 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}) + // No opening, ignore. + if (!bracketInfo) { + index++ + } + // `][`. + 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++ + } + // `]` with earlier `[`. + else { + index++ + bracketInfo[1].push(lineStart + index) + handleRange(bracketInfo) + ranges.pop() + } + } + // 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) || - regexes.some(function (regex) { - return regex.test(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(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 dbd5769..3f4f8cd 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 5ed52d9..e14a1c3 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,48 +177,17 @@ 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 -``` - -###### Out - -No messages. - -##### `ok-allow.md` - -When configured with `{ allow: [ '…' ] }`. - -###### In - -```markdown -> Eliding a portion of a quoted passage […] is acceptable. -``` - -###### Out - -No messages. - -##### `ok-allow-source.md` - -When configured with `{ allow: [ 'a', { source: '^b\\.' } ] }`. - -###### In - -```markdown -[foo][b.c] - -[bar][a] - -Matching is case-insensitive: [bar][B.C] +[mercury]: https://example.com/mercury/ ``` ###### Out @@ -228,58 +199,75 @@ No messages. ###### In ```markdown -[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. ``` ###### 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 +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-source.md` +##### `ok-allow.md` -When configured with `{ allow: [ 'a', { source: '^b\\.' } ] }`. +When configured with `{ allow: [ '…' ] }`. ###### In ```markdown -[foo][a.c] - -[bar][b] +Mercury is the first planet from the Sun and the smallest in the Solar +System. […] ``` ###### Out -```text -1:1-1:11: Found reference to undefined definition -3:1-3:9: Found reference to undefined definition +No messages. + +##### `source.md` + +When configured with `{ allow: [ { source: '^mer' }, 'venus' ] }`. + +###### In + +```markdown +[Mercury][] is the first planet from the Sun and the smallest in the Solar +System. + +[Venus][] is the second planet from the Sun. ``` +###### Out + +No messages. + ##### `gfm.md` ###### In @@ -288,15 +276,40 @@ When configured with `{ allow: [ 'a', { source: '^b\\.' } ] }`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -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. ``` ###### Out ```text -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 +``` + +##### `allow-shortcut-link.md` + +When configured with `{ allowShortcutLink: true }`. + +###### In + +```markdown +[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. +``` + +###### Out + +```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 ``` ## 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 a12815b..6a20b9e 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"} * - * ![alpha][alpha] - * ![Bravo][bravo] - * ![charlie][Charlie] + * ![Mercury][mercury]. * - * [alpha]: a - * [bravo]: b - * [charlie]: c + * [mercury]: /mercury.png * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.md"} * - * 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][]`) + * + * @example + * {"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 + * + * @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 c15db82..9c74fa0 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 2786f43..6df8496 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 c4d324a..5ad4808 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]. * - * This only works if the link text is a `text` node: - * [`echo`][] - * [*foxtrot*][] - * - * [alpha]: a - * [bravo]: b - * [delta]: d - * [`echo`]: e - * [*foxtrot*]: f + * [mercury]: https://example.com/mercury/ + * [venus-url]: https://example.com/venus/ * * @example - * {"name": "not-ok.md", "label": "input"} + * {"label": "input", "name": "not-ok.md"} * - * [alpha][alpha] - * [Bravo][bravo] - * [charlie][Charlie] + * [Mercury][mercury]. * - * [alpha]: a - * [bravo]: b - * [charlie]: c + * [mercury]: https://example.com/mercury/ * * @example - * {"name": "not-ok.md", "label": "output"} + * {"label": "output", "name": "not-ok.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 + * 1:1-1:19: Unexpected full reference link (`[text][label]`) where the identifier can be inferred from the text, expected collapsed reference (`[text][]`) + * + * @example + * {"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`, 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 + * {"label": "output", "name": "escape.md"} + * + * 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 188cac7..2b970dd 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 9b67ec1..a0c956d 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] +[Mercury][] and [Venus][venus-url]. -This only works if the link text is a `text` node: -[`echo`][] -[*foxtrot*][] - -[alpha]: a -[bravo]: b -[delta]: d -[`echo`]: e -[*foxtrot*]: f +[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]. -[alpha]: a -[bravo]: b -[charlie]: c +[mercury]: https://example.com/mercury/ ``` ###### 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 +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 + +> 👉 **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 +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 875b8e5..6ed3ae2 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 1325058..b0ab1c3 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 8119f84..c0f9d09 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]. +Mercury[^mercury] is a planet. -[^x]: ok -[^y]: not ok +[^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 c063aeb..eb4b05d 100644 --- a/packages/remark-lint-ordered-list-marker-style/index.js +++ b/packages/remark-lint-ordered-list-marker-style/index.js @@ -25,16 +25,6 @@ * * Transform ([`Transformer` from `unified`][github-unified-transformer]). * - * ### `Marker` - * - * Marker (TypeScript type). - * - * ###### Type - * - * ```ts - * type Marker = '.' | ')' - * ``` - * * ### `Options` * * Configuration (TypeScript type). @@ -42,7 +32,17 @@ * ###### Type * * ```ts - * type Options = Marker | 'consistent' + * type Options = Style | 'consistent' + * ``` + * + * ### `Style` + * + * Style (TypeScript type). + * + * ###### Type + * + * ```ts + * 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 * + * * Venus * - * 1. Bar - * - * 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) + } - if (option === 'consistent') { - option = marker - } else if (marker !== option) { - file.message('Marker style should be `' + option + '`', child) + /* c8 ignore next 2 -- weird ASTs. */ + const actual = + code === 41 /* `)` */ ? ')' : code === 46 /* `.` */ ? '.' : undefined + + /* 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 3c732a8..e0ea168 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 ee1a643..1af0ace 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,16 +141,6 @@ Warn when ordered list markers are inconsistent. Transform ([`Transformer` from `unified`][github-unified-transformer]). -### `Marker` - -Marker (TypeScript type). - -###### Type - -```ts -type Marker = '.' | ')' -``` - ### `Options` Configuration (TypeScript type). @@ -158,7 +148,17 @@ Configuration (TypeScript type). ###### Type ```ts -type Options = Marker | 'consistent' +type Options = Style | 'consistent' +``` + +### `Style` + +Style (TypeScript type). + +###### Type + +```ts +type Style = '.' | ')' ``` ## Recommendation @@ -181,14 +181,11 @@ Pass `bulletOrdered: ')'` to always use parens. ###### In ```markdown -1. Foo +1. Mercury +* Venus -1. Bar - -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 dac7f7e..539c4ea 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). * - * * `'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` + * `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 = 'one' | 'ordered' | 'single' + * 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 + * + * ###### Type + * + * ```ts + * 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. Mercury + * 2. Venus * - * 1. Foo - * 2. Bar - * 3. Baz + * *** * - * Paragraph. + * 3. Earth + * 4. Mars * - * 3. Alpha - * 4. Bravo - * 5. Charlie + * *** * - * 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"} * - * Paragraph. + * 2:2: Unexpected ordered list item value `1`, expected `3` + * 7:2: Unexpected ordered list item value `1`, expected `2` * - * 0. Delta - * 0. Echo - * 0. Foxtrot + * @example + * {"config": "one", "name": "ok.md"} + * + * 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. Foo - * 2. Bar + * 1. Mercury + * 1. Venus + * + * *** + * + * 3. Earth + * 3. Mars + * + * *** + * + * 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 + * {"config": "one", "label": "output", "name": "not-ok.md"} + * + * 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": "input"} + * {"config": "ordered", "label": "input", "name": "not-ok.md"} * - * 2. Foo - * 1. Bar + * 1. Mercury + * 1. Venus + * + * *** + * + * 2. Mars + * 1. Jupiter + * @example + * {"config": "ordered", "label": "output", "name": "not-ok.md"} + * + * 2:2: Unexpected ordered list item value `1`, expected `2` + * 7:2: Unexpected ordered list item value `1`, expected `3` * * @example - * {"name": "also-not-ok.md", "config": "one", "label": "output"} + * {"config": "single", "label": "input", "name": "not-ok.md"} * - * 1:1-1:8: Marker should be `1`, was `2` + * 1. Mercury + * 2. Venus + * + * *** + * + * 2. Mars + * 1. Jupiter + * @example + * {"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` * * @example - * {"name": "not-ok.md", "config": "ordered", "label": "input"} + * {"name": "not-ok.md", "config": "🌍", "label": "output", "positionless": true} * - * 1. Foo - * 1. Bar - * - * @example - * {"name": "not-ok.md", "config": "ordered", "label": "output"} - * - * 2:1-2:8: Marker should be `2`, was `1` - * - * @example - * {"name": "not-ok.md", "config": "💩", "label": "output", "positionless": true} - * - * 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'` */ /** + * @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 + /** @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 (asciiDigit(code)) { + index++ + code = value.charCodeAt(index) + } + + counter = value.slice(start.offset, index) + } + + 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 start = Number(startValue) let index = -1 - while (++index < node.children.length) { - const child = node.children[index] - const end = pointStart(child.children[0]) - const start = pointStart(child) + while (++index < info.counters.length) { + const item = node.children[index] + const actual = info.counters[index] + if (!actual) continue - // Ignore generated nodes, first items. - if ( - !end || - !start || - typeof end.offset !== 'number' || - typeof start.offset !== 'number' || - (index === 0 && option !== 'one') - ) { - continue - } + const startPoint = pointStart(item) + assert(startPoint) // Always defined, we checked when we found items. + assert(typeof startPoint.offset === 'number') // Same. - // Increase the expected line number when in `ordered` mode. - if (option === 'ordered') { - expected++ - } - - const marker = Number( - value - .slice(start.offset, end.offset) - .replace(/[\s.)]/g, '') - .replace(/\[[x ]?]\s*$/i, '') + 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 25ff0f2..a9f3c0f 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 404ec8c..5706f8c 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). -* `'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` +`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 = 'one' | 'ordered' | 'single' +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 + +###### Type + +```ts +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 -1. Foo -2. Bar -3. Baz +*** -Paragraph. +3. Earth +4. Mars -3. Alpha -4. Bravo -5. Charlie +*** -Unordered lists are not affected by this rule. - -* Anton +* Jupiter ``` ###### Out No messages. +##### `ok-infer-single.md` + +###### In + +```markdown +2. Mercury +2. Venus + +*** + +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,33 @@ When configured with `'one'`. ###### In ```markdown -1. Foo -1. Bar -1. Baz +1. Mercury +1. Venus +``` -Paragraph. +###### Out -1. Alpha -1. Bravo -1. Charlie +No messages. + +##### `ok.md` + +When configured with `'ordered'`. + +###### In + +```markdown +1. Mercury +2. Venus + +*** + +3. Earth +4. Mars + +*** + +0. Jupiter +1. Saturn ``` ###### Out @@ -230,49 +297,18 @@ When configured with `'single'`. ###### In ```markdown -1. Foo -1. Bar -1. Baz +1. Mercury +1. Venus -Paragraph. +*** -3. Alpha -3. Bravo -3. Charlie +3. Earth +3. Mars -Paragraph. +*** -0. Delta -0. Echo -0. Foxtrot -``` - -###### Out - -No messages. - -##### `ok.md` - -When configured with `'ordered'`. - -###### In - -```markdown -1. Foo -2. Bar -3. Baz - -Paragraph. - -3. Alpha -4. Bravo -5. Charlie - -Paragraph. - -0. Delta -1. Echo -2. Foxtrot +0. Jupiter +0. Saturn ``` ###### Out @@ -286,31 +322,25 @@ 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` -``` - -##### `also-not-ok.md` - -When configured with `'one'`. - -###### In - -```markdown -2. Foo -1. Bar -``` - -###### Out - -```text -1:1-1: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` ``` ##### `not-ok.md` @@ -320,24 +350,53 @@ When configured with `'ordered'`. ###### In ```markdown -1. Foo -1. Bar +1. Mercury +1. Venus + +*** + +2. Mars +1. Jupiter ``` ###### Out ```text -2:1-2:8: Marker should be `2`, was `1` +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 `'💩'`. +When configured with `'single'`. + +###### In + +```markdown +1. Mercury +2. Venus + +*** + +2. Mars +1. Jupiter +``` ###### Out ```text -1:1: Incorrect ordered list item marker value `💩`: use either `'ordered'`, `'one'`, or `'single'` +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 `'🌍'`. + +###### Out + +```text +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 a61ba98..4d3c03e 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 + * {"label": "output", "name": "not-ok.md"} + * + * 3:1-3:6: Unexpected thematic rule `* * *`, expected `***` * * @example - * {"name": "not-ok.md", "label": "output"} + * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true} * - * 3:1-3:6: Rules should use `***` - * - * @example - * {"name": "not-ok.md", "label": "output", "config": "💩", "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 3c662a1..b15e1ff 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 876db72..bbc435c 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 7f86007..f308949 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 - * {"config": "~", "name": "not-ok.md", "label": "input", "gfm": true} + * {"gfm": true, "label": "input", "name": "not-ok.md"} * - * ~~foo~~ + * ~Mercury~Venus and ~~Earth~~Mars. + * @example + * {"gfm": true, "label": "output", "name": "not-ok.md"} + * + * 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} + * {"config": "~", "gfm": true, "label": "input", "name": "not-ok.md"} * - * ~~foo~~ + * ~~Mercury~~Venus. + * @example + * {"config": "~", "gfm": true, "label": "output", "name": "not-ok.md"} + * + * 1:1-1:12: Unexpected double tilde strikethrough sequences (`~~`), expected single tilde (`~`) * * @example - * {"config": "~~", "name": "not-ok.md", "label": "input", "gfm": true} + * {"config": "~~", "gfm": true, "name": "ok.md"} * - * ~foo~ + * ~~Mercury~~Venus. * * @example - * {"config": "~~", "name": "not-ok.md", "label": "output", "gfm": true} + * {"config": "~~", "gfm": true, "label": "input", "name": "not-ok.md"} * - * 1:1-1:6: Strikethrough should use `~~` as a marker + * ~Mercury~Venus. + * @example + * {"config": "~~", "gfm": true, "label": "output", "name": "not-ok.md"} + * + * 1:1-1:10: Unexpected single tilde strikethrough sequences (`~`), expected double tilde (`~~`) * * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} + * {"config": "🌍", "name": "not-ok.md", "label": "output", "positionless": true, "gfm": true} * - * ~~foo~~ - * ~bar~ - * - * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} - * - * 2:1-2:6: Strikethrough should use `~~` as a marker - * - * @example - * {"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 01fb850..3f3eb57 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 f07af21..bb682b8 100644 --- a/packages/remark-lint-strikethrough-marker/readme.md +++ b/packages/remark-lint-strikethrough-marker/readme.md @@ -171,6 +171,23 @@ It’s recommended to use two tildes. ## Examples +##### `not-ok.md` + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +~Mercury~Venus and ~~Earth~~Mars. +``` + +###### Out + +```text +1:20-1:29: Unexpected double tilde strikethrough sequences (`~~`), expected single tilde (`~`) +``` + ##### `ok.md` When configured with `'~'`. @@ -181,7 +198,7 @@ When configured with `'~'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -~foo~ +~Mercury~Venus. ``` ###### Out @@ -198,13 +215,13 @@ 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 +1:1-1:12: Unexpected double tilde strikethrough sequences (`~~`), expected single tilde (`~`) ``` ##### `ok.md` @@ -217,7 +234,7 @@ When configured with `'~~'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -~~foo~~ +~~Mercury~~Venus. ``` ###### Out @@ -234,41 +251,23 @@ 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 +1:1-1:10: Unexpected single tilde strikethrough sequences (`~`), expected double tilde (`~~`) ``` ##### `not-ok.md` -###### In - -> 👉 **Note**: this example uses -> GFM ([`remark-gfm`][github-remark-gfm]). - -```markdown -~~foo~~ -~bar~ -``` +When configured with `'🌍'`. ###### Out ```text -2:1-2:6: Strikethrough should use `~~` as a marker -``` - -##### `not-ok.md` - -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 5562520..54e5dc9 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 - * {"name": "ok.md"} - * - * **foo** and **bar**. * * @example - * {"name": "also-ok.md"} + * {"config": "*", "name": "ok-asterisk.md"} * - * __foo__ and __bar__. + * **Mercury**. * * @example - * {"name": "ok.md", "config": "*"} + * {"config": "*", "label": "input", "name": "not-ok-asterisk.md"} * - * **foo**. + * __Mercury__. * * @example - * {"name": "ok.md", "config": "_"} + * {"config": "*", "label": "output", "name": "not-ok-asterisk.md"} * - * __foo__. + * 1:1-1:12: Unexpected strong marker `_`, expected `*` * * @example - * {"name": "not-ok.md", "label": "input"} + * {"config": "_", "name": "ok-underscore.md"} * - * **foo** and __bar__. + * __Mercury__. * * @example - * {"name": "not-ok.md", "label": "output"} + * {"config": "_", "label": "input", "name": "not-ok-underscore.md"} * - * 1:13-1:20: Strong should use `*` as a marker + * **Mercury**. * * @example - * {"name": "not-ok.md", "label": "output", "config": "💩", "positionless": true} + * {"config": "_", "label": "output", "name": "not-ok-underscore.md"} * - * 1:1: Incorrect strong marker `💩`: use either `'consistent'`, `'*'`, or `'_'` + * 1:1-1:12: Unexpected strong marker `*`, expected `_` + * + * @example + * {"label": "input", "name": "not-ok-consistent.md"} + * + * **Mercury** and __Venus__. + * + * @example + * {"label": "output", "name": "not-ok-consistent.md"} + * + * 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) - if (option === 'consistent') { - option = marker - } else if (marker !== option) { - file.message('Strong should use `' + option + '` as a marker', node) + /* c8 ignore next -- should not happen. */ + if (actual !== '*' && actual !== '_') return + + 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 70961e5..1808f53 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 65a45ae..c2cc624 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` - -###### In - -```markdown -**foo** and **bar**. -``` - -###### Out - -No messages. - -##### `also-ok.md` - -###### In - -```markdown -__foo__ and __bar__. -``` - -###### Out - -No messages. - -##### `ok.md` +##### `ok-asterisk.md` When configured with `'*'`. ###### In ```markdown -**foo**. +**Mercury**. ``` ###### Out No messages. -##### `ok.md` +##### `not-ok-asterisk.md` + +When configured with `'*'`. + +###### In + +```markdown +__Mercury__. +``` + +###### Out + +```text +1:1-1:12: Unexpected strong marker `_`, expected `*` +``` + +##### `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** and __bar__. +**Mercury**. ``` ###### Out ```text -1:13-1:20: Strong should use `*` as a marker +1:1-1:12: Unexpected strong marker `*`, expected `_` +``` + +##### `not-ok-consistent.md` + +###### In + +```markdown +**Mercury** and __Venus__. +``` + +###### Out + +```text +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 8afbeef..5d9279d 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 - * {"name": "ok.md", "config": "padded", "gfm": true} - * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | * * @example - * {"name": "not-ok.md", "label": "input", "config": "padded", "gfm": true} + * {"config": "padded", "gfm": true, "name": "ok.md"} * - * | A | B | - * | :----|----: | - * | Alpha|Bravo | + * | Planet | Symbol | Satellites | Mean anomaly (°) | + * | ------- | :----- | :--------: | ---------------: | + * | Mercury | ☿ | None | 174 796 | * - * | C | D | - * | :----- | ---: | - * |Charlie | Delta| - * - * Too much padding isn’t good either: - * - * | E | F | G | H | - * | :---- | -------- | :----: | -----: | - * | Echo | Foxtrot | Golf | Hotel | + * | Planet | Symbol | Satellites | Mean anomaly (°) | + * | - | :- | :-: | -: | + * | Venus | ♀ | None | 50 115 | * * @example - * {"name": "not-ok.md", "label": "output", "config": "padded", "gfm": true} + * {"config": "padded", "gfm": true, "label": "input", "name": "not-ok.md"} * - * 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 + * | 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": "compact", "gfm": true} + * {"config": "compact", "gfm": true, "name": "ok.md"} * - * |A |B | - * |-----|-----| - * |Alpha|Bravo| + * |Planet |Symbol|Satellites|Mean anomaly (°)| + * |-------|:-----|:--------:|---------------:| + * |Mercury|☿ | None | 174 796| + * + * |Planet|Symbol|Satellites|Mean anomaly (°)| + * |-|:-|:-:|-:| + * |Venus|♀|None|50 115| * * @example - * {"name": "not-ok.md", "label": "input", "config": "compact", "gfm": true} + * {"config": "compact", "gfm": true, "label": "input", "name": "not-ok.md"} * - * | A | B | - * | -----| -----| - * | Alpha| Bravo| + * | Planet | + * | -------| + * | Mercury| * - * |C | D| - * |:------|-----:| - * |Charlie|Delta | + * |Planet | + * |------ | + * |Venus | + * + * | 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 - * {"name": "not-ok.md", "label": "output", "config": "compact", "gfm": true} + * {"gfm": true, "name": "consistent-padded-ok.md"} * - * 3:5: Cell should be compact - * 3:12: Cell should be compact - * 7:15: Cell should be compact + * | Planet | + * | - | * * @example - * {"name": "ok-padded.md", "gfm": true} + * {"gfm": true, "label": "input", "name": "consistent-padded-nok.md"} * - * The default is `'consistent'`. + * | Planet| + * | - | + * @example + * {"gfm": true, "label": "output", "name": "consistent-padded-nok.md"} * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | - * - * | C | D | - * | ------- | ----- | - * | Charlie | Delta | + * 1:9: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space * * @example - * {"name": "not-ok-padded.md", "label": "input", "config": "consistent", "gfm": true} + * {"gfm": true, "name": "consistent-compact-ok.md"} * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | - * - * | C | D | - * | :----- | ----: | - * |Charlie | Delta | + * |Planet| + * |-| * * @example - * {"name": "not-ok-padded.md", "label": "output", "config": "consistent", "gfm": true} + * {"gfm": true, "label": "input", "name": "consistent-compact-nok.md"} * - * 7:2: Cell should be padded + * |Planet | + * |-| + * @example + * {"gfm": true, "label": "output", "name": "consistent-compact-nok.md"} + * + * 1:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space * * @example - * {"name": "ok-compact.md", "config": "consistent", "gfm": true} + * {"gfm": true, "name": "empty.md"} * - * |A |B | - * |-----|-----| - * |Alpha|Bravo| - * - * |C |D | - * |-------|-----| - * |Charlie|Delta| + * | | Satellites | + * | - | - | + * | Mercury | | * * @example - * {"name": "not-ok-compact.md", "label": "input", "config": "consistent", "gfm": true} + * {"gfm": true, "name": "missing-cells.md"} * - * |A |B | - * |-----|-----| - * |Alpha|Bravo| - * - * |C | D| - * |:------|-----:| - * |Charlie|Delta | + * | Planet | Symbol | Satellites | + * | - | - | - | + * | Mercury | + * | Venus | ♀ | + * | Earth | 🜨 and ♁ | 1 | + * | Mars | ♂ | 2 | 19 412 | * * @example - * {"name": "not-ok-compact.md", "label": "output", "config": "consistent", "gfm": true} + * {"config": "padded", "gfm": true, "label": "input", "name": "missing-fences.md"} * - * 7:15: Cell should be compact + * ␠Planet|Symbol|Satellites + * ------:|:-----|---------- + * Mercury|☿ |0 + * + * 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.md", "label": "output", "config": "💩", "positionless": true, "gfm": true} + * {"config": "compact", "gfm": true, "label": "input", "name": "missing-fences.md"} * - * 1:1: Incorrect table cell padding style `💩`, expected `'padded'`, `'compact'`, or `'consistent'` + * 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": "empty.md", "label": "input", "config": "padded", "gfm": true} + * {"config": "compact", "gfm": true, "label": "input", "name": "trailing-spaces.md"} * - * + * Planet | Symbol␠ + * -: | -␠ + * Mercury | ☿␠␠ * - * | | Alpha | Bravo| - * | ------ | ----- | ---: | - * | Charlie| | Echo| + * | 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 - * {"name": "empty.md", "label": "output", "config": "padded", "gfm": true} + * {"config": "compact", "gfm": true, "label": "input", "name": "nothing.md"} * - * 3:25: Cell should be padded - * 5:10: Cell should be padded - * 5:25: Cell should be padded + * | | | | + * | - | - | - | + * | | | | + * @example + * {"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 * * @example - * {"name": "missing-body.md", "config": "padded", "gfm": true} + * {"config": "padded", "gfm": true, "label": "input", "name": "nothing.md"} * - * + * |||| + * |-|-|-| + * |||| + * @example + * {"config": "padded", "gfm": true, "label": "output", "name": "nothing.md"} * - * | Alpha | Bravo | Charlie | - * | ----- | ------- | ------- | - * | Delta | - * | Echo | Foxtrot | + * 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 + * {"config": "padded", "gfm": true, "label": "input", "name": "more-weirdness.md"} + * + * Mercury + * |- + * + * Venus + * -| + * @example + * {"config": "padded", "gfm": true, "label": "output", "name": "more-weirdness.md"} + * + * 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 + * {"config": "padded", "gfm": true, "label": "input", "name": "containers.md"} + * + * > | Mercury| + * > | - | + * + * * | Venus| + * | - | + * + * > * > | Earth| + * > > | - | + * @example + * {"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 + * + * @example + * {"config": "padded", "gfm": true, "label": "input", "name": "windows.md"} + * + * | Mercury|␍␊| --- |␍␊| None | + * @example + * {"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} + * + * 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 - - if ( - typeof cellStart !== 'number' || - typeof cellEnd !== 'number' || - typeof contentStart !== 'number' || - typeof contentEnd !== 'number' - ) { - continue - } - - 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 - ) + for (const entry of entries) { + if ( + entry.size && + (sizes[entry.column] === undefined || + entry.size.middle > sizes[entry.column]) + ) { + sizes[entry.column] = entry.size.middle } } - const style = - option === 'consistent' - ? entries[0] && (!entries[0].start || !entries[0].end) - ? 0 - : 1 - : option === 'padded' - ? 1 - : 0 - - index = -1 - - while (++index < entries.length) { - checkSide('start', entries[index], style, sizes) - checkSide('end', entries[index], style, sizes) + // Find the first cell that is the biggest in its column. + if (!expected) { + for (const info of entries) { + if ( + info.size && + info.size.middle && + info.size.middle === sizes[info.column] + ) { + 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' + } + ) + } + } } + /* c8 ignore next -- always a cell. */ + if (!expected) return + + 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] - - if (spacing === undefined || spacing === style) { + function checkSide(side, info, sizes) { + if (!info.size) { return } - let reason = 'Cell should be ' + const actual = info.size[side] - if (style === 0) { - // Ignore every cell except the biggest in the column. - if (size(cell) < sizes[column]) { - return + if (actual === undefined) { + return + } + + 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 + + // 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 + ) + }) } - reason += 'compact' - } else { - reason += 'padded' - - if (spacing > style) { - // May be right or center aligned. - if (size(cell) < sizes[column]) { - return - } - - reason += ' with 1 space, not ' + spacing + if (rowIndex === 0) { + const alignRow = inferAlignRow(ancestors, align) + if (alignRow) result.push(...alignRow) } } - file.message( - reason, - side === 'start' - ? pointStart(cell.children[0]) - : pointEnd(cell.children[cell.children.length - 1]) - ) + 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, + 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 ( + 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 7fc4831..5cbdd20 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 354d219..e4402f2 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 | +| Planet | +| -------| +| Mercury| -| C | D | -| :----- | ---: | -|Charlie | Delta| +|Planet | +|------ | +|Venus | -Too much padding isn’t good either: - -| 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,37 @@ When configured with `'compact'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| A | B | -| -----| -----| -| Alpha| Bravo| +| Planet | +| -------| +| Mercury| -|C | D| -|:------|-----:| -|Charlie|Delta | +|Planet | +|------ | +|Venus | + +| Planet | +| ------ | +| Venus | ``` ###### Out ```text -3:5: Cell should be compact -3:12: Cell should be compact -7:15: Cell should be compact +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 ``` -##### `ok-padded.md` +##### `consistent-padded-ok.md` ###### In @@ -292,24 +313,15 @@ When configured with `'compact'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -The default is `'consistent'`. - -| A | B | -| ----- | ----- | -| Alpha | Bravo | - -| C | D | -| ------- | ----- | -| Charlie | Delta | +| Planet | +| - | ``` ###### Out No messages. -##### `not-ok-padded.md` - -When configured with `'consistent'`. +##### `consistent-padded-nok.md` ###### In @@ -317,24 +329,17 @@ When configured with `'consistent'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -| A | B | -| ----- | ----- | -| Alpha | Bravo | - -| C | D | -| :----- | ----: | -|Charlie | Delta | +| Planet| +| - | ``` ###### Out ```text -7:2: Cell should be padded +1:9: Unexpected `0` spaces between cell content and edge, expected `1` space, add `1` space ``` -##### `ok-compact.md` - -When configured with `'consistent'`. +##### `consistent-compact-ok.md` ###### In @@ -342,22 +347,15 @@ When configured with `'consistent'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -|A |B | -|-----|-----| -|Alpha|Bravo| - -|C |D | -|-------|-----| -|Charlie|Delta| +|Planet| +|-| ``` ###### Out No messages. -##### `not-ok-compact.md` - -When configured with `'consistent'`. +##### `consistent-compact-nok.md` ###### In @@ -365,78 +363,321 @@ When configured with `'consistent'`. > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown -|A |B | -|-----|-----| -|Alpha|Bravo| - -|C | D| -|:------|-----:| -|Charlie|Delta | +|Planet | +|-| ``` ###### Out ```text -7:15: Cell should be compact -``` - -##### `not-ok.md` - -When configured with `'💩'`. - -###### Out - -```text -1:1: Incorrect table cell padding style `💩`, expected `'padded'`, `'compact'`, or `'consistent'` +1:9: Unexpected `1` space between cell content and edge, expected `0` spaces, remove `1` space ``` ##### `empty.md` -When configured with `'padded'`. - ###### In > 👉 **Note**: this example uses > GFM ([`remark-gfm`][github-remark-gfm]). ```markdown - - -| | Alpha | Bravo| -| ------ | ----- | ---: | -| Charlie| | Echo| -``` - -###### Out - -```text -3:25: Cell should be padded -5:10: Cell should be padded -5:25: Cell should be padded -``` - -##### `missing-body.md` - -When configured with `'padded'`. - -###### In - -> 👉 **Note**: this example uses -> GFM ([`remark-gfm`][github-remark-gfm]). - -```markdown - - -| Alpha | Bravo | Charlie | -| ----- | ------- | ------- | -| Delta | -| Echo | Foxtrot | +| | Satellites | +| - | - | +| Mercury | | ``` ###### Out No messages. +##### `missing-cells.md` + +###### In + +> 👉 **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. + +##### `missing-fences.md` + +When configured with `'padded'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +␠Planet|Symbol|Satellites +------:|:-----|---------- +Mercury|☿ |0 + +Planet|Symbol +-----:|------ +␠Venus|♀ +``` + +###### Out + +```text +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 +``` + +##### `missing-fences.md` + +When configured with `'compact'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Planet | Symbol | Satellites +-: | - | - +Mercury | ☿ | 0 + +Planet | Symbol +-----: | ------ +␠Venus | ♀ +``` + +###### Out + +```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 +``` + +##### `trailing-spaces.md` + +When configured with `'compact'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Planet | Symbol␠ +-: | -␠ +Mercury | ☿␠␠ + +| Planet | Symbol |␠ +| ------ | ------ |␠ +| Venus | ♀ |␠␠ +``` + +###### Out + +```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 +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 +``` + +##### `nothing.md` + +When configured with `'compact'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +| | | | +| - | - | - | +| | | | +``` + +###### Out + +```text +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 +``` + +##### `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'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +Mercury +|- + +Venus +-| +``` + +###### Out + +```text +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 +``` + +##### `containers.md` + +When configured with `'padded'`. + +###### In + +> 👉 **Note**: this example uses +> GFM ([`remark-gfm`][github-remark-gfm]). + +```markdown +> | Mercury| +> | - | + +* | Venus| + | - | + +> * > | Earth| +> > | - | +``` + +###### Out + +```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 Projects maintained by the unified collective are compatible with maintained diff --git a/packages/remark-lint-table-pipe-alignment/index.js b/packages/remark-lint-table-pipe-alignment/index.js index aa7b5ac..9e5a7dc 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 - * {"name": "ok.md", "gfm": true} - * - * | A | B | - * | ----- | ----- | - * | Alpha | Bravo | * * @example - * {"name": "not-ok.md", "label": "input", "gfm": true} + * {"gfm": true, "name": "ok.md"} * - * | A | B | - * | -- | -- | - * | Alpha | Bravo | + * | Planet | Mean anomaly (°) | + * | ------- | ---------------: | + * | Mercury | 174 796 | + * + * |Planet|Mean anomaly (°)| + * |------|---------------:| + * |Venus | 50 115 | * * @example - * {"name": "not-ok.md", "label": "output", "gfm": true} + * {"gfm": true, "label": "input", "name": "not-ok.md"} * - * 3:9-3:10: Misaligned table fence - * 3:17-3:18: Misaligned table fence + * | Planet | Mean anomaly (°) | + * | - | -: | + * | Mercury | 174 796 | * * @example - * {"name": "ok-empty-columns.md", "gfm": true} + * {"gfm": true, "label": "output", "name": "not-ok.md"} * - * | | B | | - * |-| ----- | - | - * | | Bravo | | + * 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 - * {"name": "ok-empty-cells.md", "gfm": true} + * {"gfm": true, "name": "empty.md"} * - * | | | | - * | - | --- | ------- | - * | A | Bra | Charlie | + * | | 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 + * {"gfm": true, "label": "input", "name": "trailing-spaces.md"} + * + * | 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 + * {"gfm": true, "label": "input", "name": "more-weirdness.md"} + * + * Mercury + * |- + * + * Venus + * -| + * @example + * {"gfm": true, "label": "output", "name": "more-weirdness.md"} + * + * 5:2: Unexpected unaligned cell, expected aligned pipes, add `4` spaces (or add `-` to pad alignment row cells) + * + * @example + * {"gfm": true, "label": "input", "name": "containers.md"} + * + * > | 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 + * {"gfm": true, "label": "input", "name": "windows.md"} + * + * | 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 = [] - while (++index < node.children.length) { - const row = node.children[index] - const begin = pointStart(row) - let column = -2 // Start without a first cell. + 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 - 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 - - if ( - typeof initial !== 'number' || - typeof final !== 'number' || - typeof begin?.offset !== 'number' - ) { - continue - } - - if (cell && cell.children.length === 0) initial++ - if (next && next.children.length === 0) final-- - - const fence = value.slice(initial, final) - const pos = initial + fence.indexOf('|') - begin.offset + 1 - - // 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} - }) + 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 }) + + /** + * @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 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 ( + 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 + } + } + } + } } ) diff --git a/packages/remark-lint-table-pipe-alignment/package.json b/packages/remark-lint-table-pipe-alignment/package.json index a861df4..15ff773 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 31c8dc3..ea6fb66 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 f98147e..785a8a9 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 9bd472f..ecf09f8 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 2bf9e48..93f0745 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 c822311..aba18b1 100644 --- a/packages/remark-lint-unordered-list-marker-style/index.js +++ b/packages/remark-lint-unordered-list-marker-style/index.js @@ -25,16 +25,6 @@ * * Transform ([`Transformer` from `unified`][github-unified-transformer]). * - * ### `Marker` - * - * Marker (TypeScript type). - * - * ###### Type - * - * ```ts - * type Marker = '*' | '+' | '-' - * ``` - * * ### `Options` * * Configuration (TypeScript type). @@ -42,7 +32,17 @@ * ###### Type * * ```ts - * type Options = Marker | 'consistent' + * type Options = Style | 'consistent' + * ``` + * + * ### `Style` + * + * Style (TypeScript type). + * + * ###### Type + * + * ```ts + * 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. + * * Mercury * - * * Foo - * * Bar - * * Baz + * 1. Venus * - * Ordered lists are not affected. - * - * 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 - if (option !== 'consistent' && !markers.has(option)) { + console.log('check:', file.path) + + 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 + + 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} ) - - if (option === 'consistent') { - option = marker - } else if (marker !== option) { - file.message('Marker style should be `' + option + '`', child) - } } + } 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 f542044..bc8c94c 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 95bf37c..d7705e6 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,16 +139,6 @@ Warn when unordered list markers are inconsistent. Transform ([`Transformer` from `unified`][github-unified-transformer]). -### `Marker` - -Marker (TypeScript type). - -###### Type - -```ts -type Marker = '*' | '+' | '-' -``` - ### `Options` Configuration (TypeScript type). @@ -156,7 +146,17 @@ Configuration (TypeScript type). ###### Type ```ts -type Options = Marker | 'consistent' +type Options = Style | 'consistent' +``` + +### `Style` + +Style (TypeScript type). + +###### Type + +```ts +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. +* Mercury -* Foo -* Bar -* Baz +1. Venus -Ordered lists are not affected. - -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 bc61cac..57a6aeb 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 5432079..da09d71 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 366be95..cbc5715 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)