Add better messages, rewrite and improve rules

This commit is contained in:
Titus Wormer 2024-01-19 17:29:44 +01:00
parent ccea69188b
commit 45aeac273a
No known key found for this signature in database
GPG Key ID: E6E581152ED04E2E
192 changed files with 9167 additions and 4850 deletions

View File

@ -133,6 +133,7 @@
"remark-cli": "^12.0.0", "remark-cli": "^12.0.0",
"remark-comment-config": "^8.0.0", "remark-comment-config": "^8.0.0",
"remark-directive": "^3.0.0", "remark-directive": "^3.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"remark-github": "^12.0.0", "remark-github": "^12.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",

View File

@ -65,41 +65,52 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
* @example
* {"name": "ok.md", "config": 4}
*
* > Hello
*
* Paragraph.
*
* > World
* @example
* {"name": "ok.md", "config": 2}
*
* > Hello
*
* Paragraph.
*
* > World
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"config": 2, "name": "ok-2.md"}
* *
* > Hello * > Mercury.
* *
* Paragraph. * Venus.
* *
* > World * > Earth.
*
* Paragraph.
*
* > World
* *
* @example * @example
* {"name": "not-ok.md", "label": "output"} * {"config": 4, "name": "ok-4.md"}
* *
* 5:5: Remove 1 space between block quote and content * > Mercury.
* 9:3: Add 1 space between block quote and content *
* 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 pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart} from 'unist-util-position' import {pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit' import {visitParents} from 'unist-util-visit-parents'
const remarkLintBlockquoteIndentation = lintRule( const remarkLintBlockquoteIndentation = lintRule(
{ {
@ -130,33 +141,53 @@ const remarkLintBlockquoteIndentation = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file, options) { 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 start = pointStart(node)
const head = pointStart(node.children[0]) const headStart = pointStart(node.children[0])
if (head && start) { if (headStart && start) {
const count = head.column - start.column const actual = headStart.column - start.column
if (option === 'consistent') { if (expected) {
option = count const difference = expected - actual
} else { const differenceAbsolute = Math.abs(difference)
const diff = option - count
if (diff !== 0) {
const abs = Math.abs(diff)
if (difference !== 0) {
file.message( file.message(
(diff > 0 ? 'Add' : 'Remove') + 'Unexpected `' +
' ' + actual +
abs + '` ' +
' ' + pluralize('space', actual) +
pluralize('space', abs) + ' between block quote marker and content, expected `' +
' between block quote and content', expected +
head '` ' +
pluralize('space', expected) +
', ' +
(difference > 0 ? 'add' : 'remove') +
' `' +
differenceAbsolute +
'` ' +
pluralize('space', differenceAbsolute),
{ancestors: [...parents, node], place: headStart}
) )
} }
} else {
expected = actual
} }
} }
}) })

View File

@ -36,7 +36,7 @@
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0" "unist-util-visit-parents": "^6.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -171,36 +171,48 @@ Due to this, its recommended to configure this rule with `2`.
## Examples ## Examples
##### `ok.md` ##### `ok-2.md`
When configured with `4`.
###### In
```markdown
> Hello
Paragraph.
> World
```
###### Out
No messages.
##### `ok.md`
When configured with `2`. When configured with `2`.
###### In ###### In
```markdown ```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 ###### Out
@ -212,22 +224,32 @@ No messages.
###### In ###### In
```markdown ```markdown
> Hello > Mercury.
Paragraph. Venus.
> World > Earth.
Paragraph. Mars.
> World > Jupiter
``` ```
###### Out ###### Out
```text ```text
5:5: Remove 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: Add 1 space between block quote and content 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 ## Compatibility

View File

@ -10,6 +10,8 @@
* *
* You can use this package to check that the style of GFM tasklists is * You can use this package to check that the style of GFM tasklists is
* consistent. * consistent.
* Task lists are a GFM feature enabled with
* [`remark-gfm`][github-remark-gfm].
* *
* ## API * ## API
* *
@ -63,6 +65,7 @@
* [api-options]: #options * [api-options]: #options
* [api-remark-lint-checkbox-character-style]: #unifieduseremarklintcheckboxcharacterstyle-options * [api-remark-lint-checkbox-character-style]: #unifieduseremarklintcheckboxcharacterstyle-options
* [api-styles]: #styles * [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-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify
* [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer * [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer
* *
@ -70,55 +73,60 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
* @example
* {"name": "ok.md", "config": {"checked": "x"}, "gfm": true}
*
* - [x] List item
* - [x] List item
* *
* @example * @example
* {"name": "ok.md", "config": {"checked": "X"}, "gfm": true} * {"config": {"checked": "x"}, "gfm": true, "name": "ok-x.md"}
* *
* - [X] List item * - [x] Mercury.
* - [X] List item * - [x] Venus.
* *
* @example * @example
* {"name": "ok.md", "config": {"unchecked": " "}, "gfm": true} * {"config": {"checked": "X"}, "gfm": true, "name": "ok-x-upper.md"}
* *
* - [ ] List item * - [X] Mercury.
* - [ ] List item * - [X] Venus.
*
* @example
* {"config": {"unchecked": " "}, "gfm": true, "name": "ok-space.md"}
*
* - [ ] Mercury.
* - [ ] Venus.
* - [ ] * - [ ]
* - [ ] * - [ ]
* *
* @example * @example
* {"name": "ok.md", "config": {"unchecked": "\t"}, "gfm": true} * {"config": {"unchecked": "\t"}, "gfm": true, "name": "ok-tab.md"}
* *
* - [] List item * - [] Mercury.
* - [] List item * - [] Venus.
* *
* @example * @example
* {"name": "not-ok.md", "label": "input", "gfm": true} * {"label": "input", "gfm": true, "name": "not-ok-default.md"}
* *
* - [x] List item * - [x] Mercury.
* - [X] List item * - [X] Venus.
* - [ ] List item * - [ ] Earth.
* - [] List item * - [] 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 * @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 * 1:1: Unexpected value `🌍` for `options`, expected an object or `'consistent'`
* 4:5: Unchecked checkboxes should use ` ` as a marker
* *
* @example * @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 * @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 {lintRule} from 'unified-lint-rule'
import {pointStart} from 'unist-util-position' 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( const remarkLintCheckboxCharacterStyle = lintRule(
{ {
@ -156,77 +165,115 @@ const remarkLintCheckboxCharacterStyle = lintRule(
*/ */
function (tree, file, options) { function (tree, file, options) {
const value = String(file) const value = String(file)
/** @type {'X' | 'x' | 'consistent'} */ /** @type {'X' | 'x' | undefined} */
let checked = 'consistent' let checkedExpected
/** @type {'\x09' | ' ' | 'consistent'} */ /** @type {VFileMessage | undefined} */
let unchecked = 'consistent' let checkedConsistentCause
/** @type {'\t' | ' ' | undefined} */
let uncheckedExpected
/** @type {VFileMessage | undefined} */
let uncheckedConsistentCause
if (options && typeof options === 'object') { if (options === null || options === undefined || options === 'consistent') {
checked = options.checked || 'consistent' // Empty.
unchecked = options.unchecked || 'consistent' } 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( file.fail(
'Incorrect unchecked checkbox marker `' + 'Unexpected value `' +
unchecked + options +
"`: use either `'\\t'`, or `' '`" "` for `options`, expected an object or `'consistent'`"
) )
} }
if (checked !== 'consistent' && checked !== 'x' && checked !== 'X') { visitParents(tree, 'listItem', function (node, parents) {
file.fail(
'Incorrect checked checkbox marker `' +
checked +
"`: use either `'x'`, or `'X'`"
)
}
visit(tree, 'listItem', function (node) {
const head = node.children[0] const head = node.children[0]
const point = pointStart(head) const headStart = pointStart(head)
// Exit early for items without checkbox. // 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 ( if (
!point ||
!head || !head ||
!headStart ||
typeof node.checked !== 'boolean' || typeof node.checked !== 'boolean' ||
typeof point.offset !== 'number' typeof headStart.offset !== 'number'
) { ) {
return return
} }
// Move back to before `] `. // Move back to before `] `.
point.offset -= 2 headStart.offset -= 2
point.column -= 2 headStart.column -= 2
// Assume we start with a checkbox, because well, `checked` is set. // Assume we start with a checkbox, because well, `checked` is set.
const match = /\[([\t Xx])]/.exec( 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 dont crash if there actually isnt /* c8 ignore next 2 -- failsafe so we dont crash if there actually isnt
* a checkbox. */ * a checkbox. */
if (!match) return 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) { if (node.checked) {
// @ts-expect-error: valid marker. checkedExpected = /** @type {'X' | 'x'} */ (actual)
checked = match[1] checkedConsistentCause = cause
} else { } else {
// @ts-expect-error: valid marker. uncheckedExpected = /** @type {'\t' | ' '} */ (actual)
unchecked = match[1] uncheckedConsistentCause = cause
} }
} else if (match[1] !== style) { } else if (actual !== expected) {
file.message( file.message(
(node.checked ? 'Checked' : 'Unchecked') + 'Unexpected ' +
' checkboxes should use `' + (node.checked ? '' : 'un') +
style + 'checked checkbox value `' +
'` as a marker', actualDisplay +
point '`, expected `' +
expectedDisplay +
'`',
{
ancestors: [...parents, node],
cause: node.checked
? checkedConsistentCause
: uncheckedConsistentCause,
place: headStart
}
) )
} }
}) })

View File

@ -36,7 +36,8 @@
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -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 You can use this package to check that the style of GFM tasklists is
consistent. consistent.
Task lists are a GFM feature enabled with
[`remark-gfm`][github-remark-gfm].
## Presets ## Presets
@ -176,7 +178,7 @@ using `'x'` (lowercase X) and unchecked checkboxes using `'␠'` (a space).
## Examples ## Examples
##### `ok.md` ##### `ok-x.md`
When configured with `{ checked: 'x' }`. When configured with `{ checked: 'x' }`.
@ -186,15 +188,15 @@ When configured with `{ checked: 'x' }`.
> GFM ([`remark-gfm`][github-remark-gfm]). > GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
- [x] List item - [x] Mercury.
- [x] List item - [x] Venus.
``` ```
###### Out ###### Out
No messages. No messages.
##### `ok.md` ##### `ok-x-upper.md`
When configured with `{ checked: 'X' }`. When configured with `{ checked: 'X' }`.
@ -204,15 +206,15 @@ When configured with `{ checked: 'X' }`.
> GFM ([`remark-gfm`][github-remark-gfm]). > GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
- [X] List item - [X] Mercury.
- [X] List item - [X] Venus.
``` ```
###### Out ###### Out
No messages. No messages.
##### `ok.md` ##### `ok-space.md`
When configured with `{ unchecked: ' ' }`. When configured with `{ unchecked: ' ' }`.
@ -222,8 +224,8 @@ When configured with `{ unchecked: ' ' }`.
> GFM ([`remark-gfm`][github-remark-gfm]). > GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
- [ ] List item - [ ] Mercury.
- [ ] List item - [ ] Venus.
- [ ]␠␠ - [ ]␠␠
- [ ] - [ ]
``` ```
@ -232,7 +234,7 @@ When configured with `{ unchecked: ' ' }`.
No messages. No messages.
##### `ok.md` ##### `ok-tab.md`
When configured with `{ unchecked: '\t' }`. When configured with `{ unchecked: '\t' }`.
@ -242,15 +244,15 @@ When configured with `{ unchecked: '\t' }`.
> GFM ([`remark-gfm`][github-remark-gfm]). > GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
- [␉] List item - [␉] Mercury.
- [␉] List item - [␉] Venus.
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `not-ok-default.md`
###### In ###### In
@ -258,37 +260,47 @@ No messages.
> GFM ([`remark-gfm`][github-remark-gfm]). > GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
- [x] List item - [x] Mercury.
- [X] List item - [X] Venus.
- [ ] List item - [ ] Earth.
- [␉] List item - [␉] Mars.
``` ```
###### Out ###### Out
```text ```text
2:5: Checked checkboxes should use `x` as a marker 2:5: Unexpected checked checkbox value `X`, expected `x`
4:5: Unchecked checkboxes should use ` ` as a marker 4:5: Unexpected unchecked checkbox value `\t`, expected ` `
``` ```
##### `not-ok.md` ##### `not-ok-option.md`
When configured with `{ unchecked: '💩' }`. When configured with `'🌍'`.
###### Out ###### Out
```text ```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 ###### Out
```text ```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 ## Compatibility

View File

@ -55,38 +55,48 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
* @example
* {"name": "ok.md", "gfm": true}
*
* - [ ] List item
* + [x] List Item
* * [X] List item
* - [ ] List item
* *
* @example * @example
* {"name": "not-ok.md", "label": "input", "gfm": true} * {"gfm": true, "name": "ok.md"}
* *
* - [ ] List item * - [ ] Mercury.
* + [x] List item * + [x] Venus.
* * [X] List item * * [X] Earth.
* - [ ] List item * - [ ] Mars.
* *
* @example * @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 * - [ ] Mercury.
* 3:7-3:9: Checkboxes should be followed by a single character * + [x] Venus.
* 4:7-4:10: Checkboxes should be followed by a single character * * [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 * @typedef {import('mdast').Root} Root
*/ */
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart} from 'unist-util-position' import {pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit' import {visitParents} from 'unist-util-visit-parents'
import {location} from 'vfile-location'
const remarkLintCheckboxContentIndent = lintRule( const remarkLintCheckboxContentIndent = lintRule(
{ {
@ -101,45 +111,59 @@ const remarkLintCheckboxContentIndent = lintRule(
*/ */
function (tree, file) { function (tree, file) {
const value = String(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 head = node.children[0]
const point = pointStart(head) const headStart = pointStart(head)
// Exit early for items without checkbox. // 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 ( if (
!point ||
!head || !head ||
!headStart ||
typeof node.checked !== 'boolean' || typeof node.checked !== 'boolean' ||
typeof point.offset !== 'number' typeof headStart.offset !== 'number'
) { ) {
return 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( 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 dont crash if there actually isnt a checkbox. */ /* c8 ignore next -- make sure we dont crash if there actually isnt a checkbox. */
if (!match) return if (!match) return
// Move past checkbox. // Move past checkbox.
const initial = point.offset let final = headStart.offset
let final = initial 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 size = final - headStart.offset
const start = loc.toPoint(initial)
const end = loc.toPoint(final)
if (size) {
file.message( file.message(
'Checkboxes should be followed by a single character', 'Unexpected `' +
/* c8 ignore next -- we get here if we have offsets. */ (size + 1) +
start && end ? {start, end} : undefined '` ' +
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
}
}
) )
} }
}) })

View File

@ -34,10 +34,10 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {
@ -49,7 +49,8 @@
"xo": { "xo": {
"prettier": true, "prettier": true,
"rules": { "rules": {
"capitalized-comments": "off" "capitalized-comments": "off",
"unicorn/prefer-code-point": "off"
} }
} }
} }

View File

@ -163,10 +163,10 @@ content after them with a single space between.
> GFM ([`remark-gfm`][github-remark-gfm]). > GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
- [ ] List item - [ ] Mercury.
+ [x] List Item + [x] Venus.
* [X] List item * [X] Earth.
- [ ] List item - [ ] Mars.
``` ```
###### Out ###### Out
@ -181,18 +181,36 @@ No messages.
> GFM ([`remark-gfm`][github-remark-gfm]). > GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
- [ ] List item - [ ] Mercury.
+ [x] List item + [x] Venus.
* [X] List item * [X] Earth.
- [ ] List item - [ ] Mars.
``` ```
###### Out ###### Out
```text ```text
2:7-2:8: Checkboxes should be followed by a single character 2:8: Unexpected `2` spaces between checkbox and content, expected `1` space, remove `1` space
3:7-3:9: Checkboxes should be followed by a single character 3:9: Unexpected `3` spaces between checkbox and content, expected `1` space, remove `2` spaces
4:7-4:10: Checkboxes should be followed by a single character 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 ## Compatibility

View File

@ -75,81 +75,79 @@
* @license MIT * @license MIT
* *
* @example * @example
* {"config": "indented", "name": "ok.md"} * {"config": "indented", "name": "ok-indented.md"}
* *
* alpha() * venus()
* *
* Paragraph. * Mercury.
* *
* bravo() * earth()
* *
* @example * @example
* {"config": "indented", "name": "not-ok.md", "label": "input"} * {"config": "fenced", "name": "ok-fenced.md"}
* *
* ``` * ```
* alpha() * venus()
* ``` * ```
* *
* Paragraph. * Mercury.
* *
* ``` * ```
* bravo() * earth()
* ``` * ```
* *
* @example * @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 * venus()
* 7:1-9:4: Code blocks should be indented
* *
* @example * Mercury.
* {"config": "fenced", "name": "ok.md"}
* *
* ``` * ```
* alpha() * earth()
* ``` * ```
* @example
* {"label": "output", "name": "not-ok-consistent.md"}
* *
* Paragraph. * 5:1-7:4: Unexpected fenced code block, expected indented code blocks
*
* ```
* bravo()
* ```
* *
* @example * @example
* {"config": "fenced", "name": "not-ok-fenced.md", "label": "input"} * {"config": "indented", "label": "input", "name": "not-ok-indented.md"}
*
* 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.
* *
* ``` * ```
* bravo() * venus()
* ``` * ```
* *
* @example * Mercury.
* {"name": "not-ok-consistent.md", "label": "output"}
* *
* 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 * @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 {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position' 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( const remarkLintCodeBlockStyle = lintRule(
{ {
@ -182,22 +181,25 @@ const remarkLintCodeBlockStyle = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file, options) { function (tree, file, options) {
let option = options || 'consistent'
const value = String(file) const value = String(file)
/** @type {VFileMessage | undefined} */
let cause
/** @type {Style | undefined} */
let expected
if ( if (options === null || options === undefined || options === 'consistent') {
option !== 'consistent' && // Empty.
option !== 'indented' && } else if (options === 'indented' || options === 'fenced') {
option !== 'fenced' expected = options
) { } else {
file.fail( file.fail(
'Incorrect code block style `' + 'Unexpected value `' +
option + options +
"`: use either `'consistent'`, `'fenced'`, or `'indented'`" "` for `options`, expected `'fenced'`, `'indented'`, or `'consistent'`"
) )
} }
visit(tree, 'code', function (node) { visitParents(tree, 'code', function (node, parents) {
const end = pointEnd(node) const end = pointEnd(node)
const start = pointStart(node) const start = pointStart(node)
@ -210,16 +212,35 @@ const remarkLintCodeBlockStyle = lintRule(
return return
} }
const current = const actual =
node.lang || node.lang || /^ {0,3}([`~])/.test(value.slice(start.offset, end.offset))
/^\s*([~`])\1{2,}/.test(value.slice(start.offset, end.offset))
? 'fenced' ? 'fenced'
: 'indented' : 'indented'
if (option === 'consistent') { if (expected) {
option = current if (expected !== actual) {
} else if (option !== current) { file.message(
file.message('Code blocks should be ' + option, node) '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'
}
)
} }
}) })
} }

View File

@ -34,7 +34,8 @@
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -180,50 +180,25 @@ language and as indented code otherwise.
## Examples ## Examples
##### `ok.md` ##### `ok-indented.md`
When configured with `'indented'`. When configured with `'indented'`.
###### In ###### In
```markdown ```markdown
alpha() venus()
Paragraph. Mercury.
bravo() earth()
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `ok-fenced.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`
When configured with `'fenced'`. When configured with `'fenced'`.
@ -231,13 +206,13 @@ When configured with `'fenced'`.
````markdown ````markdown
``` ```
alpha() venus()
``` ```
Paragraph. Mercury.
``` ```
bravo() earth()
``` ```
```` ````
@ -245,6 +220,51 @@ bravo()
No messages. 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` ##### `not-ok-fenced.md`
When configured with `'fenced'`. When configured with `'fenced'`.
@ -252,48 +272,28 @@ When configured with `'fenced'`.
###### In ###### In
```markdown ```markdown
alpha() venus()
Paragraph. Mercury.
bravo() earth()
``` ```
###### Out ###### Out
```text ```text
1:1-1:12: Code blocks should be fenced 1:1-1:12: Unexpected indented code block, expected fenced code blocks
5:1-5:12: Code blocks should be fenced 5:1-5:12: Unexpected indented code block, expected fenced code blocks
``` ```
##### `not-ok-consistent.md` ##### `not-ok-options.md`
###### In When configured with `'🌍'`.
````markdown
alpha()
Paragraph.
```
bravo()
```
````
###### Out ###### Out
```text ```text
5:1-7:4: Code blocks should be indented 1:1: Unexpected value `🌍` for `options`, expected `'fenced'`, `'indented'`, or `'consistent'`
```
##### `not-ok-incorrect.md`
When configured with `'💩'`.
###### Out
```text
1:1: Incorrect code block style `💩`: use either `'consistent'`, `'fenced'`, or `'indented'`
``` ```
## Compatibility ## Compatibility

View File

@ -37,30 +37,31 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* [example]: http://example.com "Example Domain" * [mercury]: http://example.com "Mercury"
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "not-ok.md"}
*
* [Example]: http://example.com "Example Domain"
* *
* [Mercury]: http://example.com "Mercury"
* @example * @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 * @example
* {"gfm": true, "label": "input", "name": "gfm.md"} * {"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 * @example
* {"gfm": true, "label": "output", "name": "gfm.md"} * {"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 {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position' import {visitParents} from 'unist-util-visit-parents'
import {visit} from 'unist-util-visit'
const label = /^\s*\[((?:\\[\s\S]|[^[\]])+)]/
const remarkLintDefinitionCase = lintRule( const remarkLintDefinitionCase = lintRule(
{ {
@ -85,28 +83,19 @@ const remarkLintDefinitionCase = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
const value = String(file) visitParents(tree, function (node, parents) {
if (
visit(tree, function (node) { (node.type === 'definition' || node.type === 'footnoteDefinition') &&
if (node.type === 'definition' || node.type === 'footnoteDefinition') { node.position &&
const end = pointEnd(node) node.label &&
const start = pointStart(node) node.label !== node.label.toLowerCase()
) {
if ( file.message(
end && 'Unexpected uppercase characters in ' +
start && (node.type === 'definition' ? '' : 'footnote ') +
typeof end.offset === 'number' && 'definition label, expected lowercase',
typeof start.offset === 'number' {ancestors: [...parents, node], place: node.position}
) { )
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
)
}
}
} }
}) })
} }

View File

@ -33,8 +33,7 @@
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -146,7 +146,7 @@ Due to this, its recommended to use lowercase and turn this rule on.
###### In ###### In
```markdown ```markdown
[example]: http://example.com "Example Domain" [mercury]: http://example.com "Mercury"
``` ```
###### Out ###### Out
@ -158,13 +158,13 @@ No messages.
###### In ###### In
```markdown ```markdown
[Example]: http://example.com "Example Domain" [Mercury]: http://example.com "Mercury"
``` ```
###### Out ###### Out
```text ```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` ##### `gfm.md`
@ -175,13 +175,15 @@ No messages.
> GFM ([`remark-gfm`][github-remark-gfm]). > GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```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 ###### Out
```text ```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 ## Compatibility

View File

@ -41,31 +41,41 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* [example domain]: http://example.com "Example Domain" * [planet mercury]: http://example.com
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "not-ok-consecutive.md"}
* *
* [exampledomain]: http://example.com "Example Domain" * [planetmercury]: 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 * @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 * [planetmercury]: 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 * @typedef {import('mdast').Root} Root
*/ */
import {longestStreak} from 'longest-streak'
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart, pointEnd} from 'unist-util-position' import {visitParents} from 'unist-util-visit-parents'
import {visit} from 'unist-util-visit'
const label = /^\s*\[((?:\\[\s\S]|[^[\]])+)]/
const remarkLintDefinitionSpacing = lintRule( const remarkLintDefinitionSpacing = lintRule(
{ {
@ -79,27 +89,35 @@ const remarkLintDefinitionSpacing = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { 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 (size > 1) {
if (node.type === 'definition') { file.message(
const end = pointEnd(node) 'Unexpected `' +
const start = pointStart(node) size +
'` consecutive spaces in definition label, expected `1` space, remove `' +
(size - 1) +
'` ' +
pluralize('space', size - 1),
{ancestors: [...parents, node], place: node.position}
)
}
if ( /** @type {Array<string>} */
end && const disallowed = []
start && if (node.label.includes('\t')) disallowed.push('\\t')
typeof end.offset === 'number' && if (node.label.includes('\n')) disallowed.push('\\n')
typeof start.offset === 'number' if (node.label.includes('\r')) disallowed.push('\\r')
) {
const match = value.slice(start.offset, end.offset).match(label)
if (match && /[ \t\n]{2,}/.test(match[1])) { for (const disallow of disallowed) {
file.message( file.message(
'Do not use consecutive whitespace in definition labels', 'Unexpected non-space whitespace character `' +
node disallow +
) '` in definition label, expected `1` space, replace it',
} {ancestors: [...parents, node], place: node.position}
)
} }
} }
}) })

View File

@ -32,9 +32,10 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"longest-streak": "^3.0.0",
"pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -150,25 +150,41 @@ Due to this, its recommended to use one space and turn this rule on.
###### In ###### In
```markdown ```markdown
[example domain]: http://example.com "Example Domain" [planet mercury]: http://example.com
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `not-ok-consecutive.md`
###### In ###### In
```markdown ```markdown
[example␠␠␠␠domain]: http://example.com "Example Domain" [planet␠␠␠␠mercury]: http://example.com
``` ```
###### Out ###### Out
```text ```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 ## Compatibility

View File

@ -77,51 +77,51 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
* @example
* {"config": "*", "name": "ok.md"}
*
* *foo*
* *
* @example * @example
* {"config": "*", "name": "not-ok.md", "label": "input"} * {"config": "*", "name": "ok-asterisk.md"}
* *
* _foo_ * *Mercury*.
* *
* @example * @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 * @example
* {"config": "_", "name": "ok.md"} * {"config": "*", "label": "output", "name": "not-ok-asterisk.md"}
* *
* _foo_ * 1:1-1:10: Unexpected emphasis marker `_`, expected `*`
* *
* @example * @example
* {"config": "_", "name": "not-ok.md", "label": "input"} * {"config": "_", "name": "ok-underscore.md"}
* *
* *foo* * _Mercury_.
* *
* @example * @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 * @example
* {"name": "not-ok.md", "label": "input"} * {"config": "_", "label": "output", "name": "not-ok-underscore.md"}
* *
* *foo* * 1:1-1:10: Unexpected emphasis marker `*`, expected `_`
* _bar_
* *
* @example * @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 * @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 {lintRule} from 'unified-lint-rule'
import {pointStart} from 'unist-util-position' 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( const remarkLintEmphasisMarker = lintRule(
{ {
@ -155,26 +156,56 @@ const remarkLintEmphasisMarker = lintRule(
*/ */
function (tree, file, options) { function (tree, file, options) {
const value = String(file) 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( file.fail(
'Incorrect emphasis marker `' + 'Unexpected value `' +
option + options +
"`: use either `'consistent'`, `'*'`, or `'_'`" "` for `options`, expected `'*'`, `'_'`, or `'consistent'`"
) )
} }
visit(tree, 'emphasis', function (node) { visitParents(tree, 'emphasis', function (node, parents) {
const start = pointStart(node) const start = pointStart(node)
if (start && typeof start.offset === 'number') { if (start && typeof start.offset === 'number') {
const marker = /** @type {Marker} */ (value.charAt(start.offset)) const actual = value.charAt(start.offset)
if (option === 'consistent') { /* c8 ignore next -- should not happen. */
option = marker if (actual !== '*' && actual !== '_') return
} else if (marker !== option) {
file.message('Emphasis should use `' + option + '` as a marker', node) if (expected) {
if (actual !== expected) {
file.message(
'Unexpected emphasis marker `' +
actual +
'`, expected `' +
expected +
'`',
{ancestors: [...parents, node], cause, place: node.position}
)
}
} else {
expected = actual
cause = new VFileMessage(
"Emphasis marker style `'" +
actual +
"'` first defined for `'consistent'` here",
{
ancestors: [...parents, node],
place: node.position,
ruleId: 'emphasis-marker',
source: 'remark-lint'
}
)
} }
} }
}) })

View File

@ -34,7 +34,8 @@
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -184,89 +184,88 @@ Pass `emphasis: '_'` to always use underscores.
## Examples ## Examples
##### `ok.md` ##### `ok-asterisk.md`
When configured with `'*'`. When configured with `'*'`.
###### In ###### In
```markdown ```markdown
*foo* *Mercury*.
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `not-ok-asterisk.md`
When configured with `'*'`. When configured with `'*'`.
###### In ###### In
```markdown ```markdown
_foo_ _Mercury_.
``` ```
###### Out ###### Out
```text ```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 `'_'`. When configured with `'_'`.
###### In ###### In
```markdown ```markdown
_foo_ _Mercury_.
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `not-ok-underscore.md`
When configured with `'_'`. When configured with `'_'`.
###### In ###### In
```markdown ```markdown
*foo* *Mercury*.
``` ```
###### Out ###### Out
```text ```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 ###### In
```markdown ```markdown
*foo* *Mercury* and _Venus_.
_bar_
``` ```
###### Out ###### Out
```text ```text
2:1-2:6: Emphasis should use `*` as a marker 1:15-1:22: Unexpected emphasis marker `_`, expected `*`
``` ```
##### `not-ok.md` ##### `not-ok.md`
When configured with `'💩'`. When configured with `'🌍'`.
###### Out ###### Out
```text ```text
1:1: Incorrect emphasis marker `💩`: use either `'consistent'`, `'*'`, or `'_'` 1:1: Unexpected value `🌍` for `options`, expected `'*'`, `'_'`, or `'consistent'`
``` ```
## Compatibility ## Compatibility

View File

@ -59,66 +59,79 @@
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* ```alpha * ```markdown
* bravo() * # Mercury
* ``` * ```
* *
* @example * @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 * @example
* {"name": "not-ok.md", "label": "output"} * {"config": {"allowEmpty": false}, "label": "input", "name": "not-ok-allow-empty.md"}
*
* 1:1-3:4: Missing code language flag
*
* @example
* {"name": "ok.md", "config": {"allowEmpty": true}}
* *
* ``` * ```
* 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 * @example
* {"name": "not-ok.md", "config": {"allowEmpty": false}, "label": "input"} * {"config": {"flags":["markdown"]}, "name": "ok-options.md"}
* *
* ``` * ```markdown
* alpha() * # Mercury
* ``` * ```
* *
* @example * @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 * ```javascript
* * mercury()
* @example
* {"name": "ok.md", "config": ["alpha"]}
*
* ```alpha
* bravo()
* ``` * ```
* @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 * @example
* {"name": "ok.md", "config": {"flags":["alpha"]}} * {"config": ["javascript", "markdown", "mdx", "typescript"], "label": "input", "name": "not-ok-long-array.md"}
* *
* ```alpha * ```html
* bravo() * <h1>Mercury</h1>
* ``` * ```
* @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 * @example
* {"name": "not-ok.md", "config": ["charlie"], "label": "input"} * {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true}
* *
* ```alpha * 1:1: Unexpected value `🌍` for `options`, expected array or object
* bravo()
* ```
*
* @example
* {"name": "not-ok.md", "config": ["charlie"], "label": "output"}
*
* 1:1-3:4: Incorrect code language flag
*/ */
/** /**
@ -135,13 +148,15 @@
* other flags will result in a warning (optional). * other flags will result in a warning (optional).
*/ */
import {quotation} from 'quotation'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position' 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,}/ const fence = /^ {0,3}([~`])\1{2,}/
/** @type {ReadonlyArray<string>} */
const emptyFlags = [] const listFormat = new Intl.ListFormat('en', {type: 'disjunction'})
const listFormatUnit = new Intl.ListFormat('en', {type: 'unit'})
const remarkLintFencedCodeFlag = lintRule( const remarkLintFencedCodeFlag = lintRule(
{ {
@ -159,24 +174,45 @@ const remarkLintFencedCodeFlag = lintRule(
function (tree, file, options) { function (tree, file, options) {
const value = String(file) const value = String(file)
let allowEmpty = false let allowEmpty = false
let allowed = emptyFlags /** @type {ReadonlyArray<string> | 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` dont mix. // Note: casts because `isArray` and `readonly` dont mix.
if (Array.isArray(options)) { if (Array.isArray(options)) {
const flags = /** @type {ReadonlyArray<string>} */ (options) const flags = /** @type {ReadonlyArray<string>} */ (options)
allowed = flags allowed = flags
} else { } else {
const settings = /** @type {Options} */ (options) const settings = /** @type {Options} */ (options)
allowEmpty = Boolean(settings.allowEmpty) allowEmpty = settings.allowEmpty === true
if (settings.flags) { if (settings.flags) {
allowed = 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 end = pointEnd(node)
const start = pointStart(node) const start = pointStart(node)
@ -187,14 +223,24 @@ const remarkLintFencedCodeFlag = lintRule(
typeof start.offset === 'number' typeof start.offset === 'number'
) { ) {
if (node.lang) { if (node.lang) {
if (allowed.length > 0 && !allowed.includes(node.lang)) { if (allowed && !allowed.includes(node.lang)) {
file.message('Incorrect code language flag', node) 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) const slice = value.slice(start.offset, end.offset)
if (!allowEmpty && fence.test(slice)) { if (fence.test(slice)) {
file.message('Missing code language flag', node) file.message(
'Unexpected missing fenced code language flag in info string, expected ' +
allowedDisplay,
{ancestors: [...parents, node], place: node.position}
)
} }
} }
} }

View File

@ -34,9 +34,10 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"quotation": "^2.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0" "unist-util-visit-parents": "^6.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -165,8 +165,8 @@ Its recommended to instead use a certain flag for plain text (such as
###### In ###### In
````markdown ````markdown
```alpha ```markdown
bravo() # Mercury
``` ```
```` ````
@ -180,17 +180,17 @@ No messages.
````markdown ````markdown
``` ```
alpha() mercury()
``` ```
```` ````
###### Out ###### Out
```text ```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 }`. When configured with `{ allowEmpty: true }`.
@ -198,7 +198,7 @@ When configured with `{ allowEmpty: true }`.
````markdown ````markdown
``` ```
alpha() mercury()
``` ```
```` ````
@ -206,7 +206,7 @@ alpha()
No messages. No messages.
##### `not-ok.md` ##### `not-ok-allow-empty.md`
When configured with `{ allowEmpty: false }`. When configured with `{ allowEmpty: false }`.
@ -214,25 +214,25 @@ When configured with `{ allowEmpty: false }`.
````markdown ````markdown
``` ```
alpha() mercury()
``` ```
```` ````
###### Out ###### Out
```text ```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 ###### In
````markdown ````markdown
```alpha ```markdown
bravo() # Mercury
``` ```
```` ````
@ -240,15 +240,15 @@ bravo()
No messages. No messages.
##### `ok.md` ##### `ok-options.md`
When configured with `{ flags: [ 'alpha' ] }`. When configured with `{ flags: [ 'markdown' ] }`.
###### In ###### In
````markdown ````markdown
```alpha ```markdown
bravo() # Mercury
``` ```
```` ````
@ -256,22 +256,50 @@ bravo()
No messages. No messages.
##### `not-ok.md` ##### `not-ok-array.md`
When configured with `[ 'charlie' ]`. When configured with `[ 'markdown' ]`.
###### In ###### In
````markdown ````markdown
```alpha ```javascript
bravo() mercury()
``` ```
```` ````
###### Out ###### Out
```text ```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
<h1>Mercury</h1>
```
````
###### 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 ## Compatibility

View File

@ -68,71 +68,70 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok-indented.md"}
* *
* Indented code blocks are not affected by this rule: * Indented code blocks are not affected by this rule:
* *
* bravo() * mercury()
* *
* @example * @example
* {"name": "ok.md", "config": "`"} * {"config": "`", "name": "ok-tick.md"}
* *
* ```alpha * ```javascript
* bravo() * mercury()
* ``` * ```
* *
* ``` * ```
* charlie() * venus()
* ``` * ```
* *
* @example * @example
* {"name": "ok.md", "config": "~"} * {"config": "~", "name": "ok-tilde.md"}
* *
* ~~~alpha * ~~~javascript
* bravo() * mercury()
* ~~~ * ~~~
* *
* ~~~ * ~~~
* charlie() * venus()
* ~~~ * ~~~
* *
* @example * @example
* {"name": "not-ok-consistent-tick.md", "label": "input"} * {"label": "input", "name": "not-ok-consistent-tick.md"}
* *
* ```alpha * ```javascript
* bravo() * mercury()
* ``` * ```
* *
* ~~~ * ~~~
* charlie() * venus()
* ~~~ * ~~~
* @example
* {"label": "output", "name": "not-ok-consistent-tick.md"}
*
* 5:1-7:4: Unexpected fenced code marker `~`, expected `` ` ``
* *
* @example * @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 * ~~~javascript
* * mercury()
* @example
* {"name": "not-ok-consistent-tilde.md", "label": "input"}
*
* ~~~alpha
* bravo()
* ~~~ * ~~~
* *
* ``` * ```
* charlie() * venus()
* ``` * ```
* @example
* {"label": "output", "name": "not-ok-consistent-tilde.md"}
*
* 5:1-7:4: Unexpected fenced code marker `` ` ``, expected `~`
* *
* @example * @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 * 1:1: Unexpected value `🌍` for `options`, expected ``'`'``, `'~'`, or `'consistent'`
*
* @example
* {"name": "not-ok-incorrect.md", "config": "💩", "label": "output", "positionless": true}
*
* 1:1: Incorrect fenced code marker `💩`: use either `'consistent'`, `` '`' ``, or `'~'`
*/ */
/** /**
@ -149,7 +148,8 @@
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart} from 'unist-util-position' 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( const remarkLintFencedCodeMarker = lintRule(
{ {
@ -165,38 +165,59 @@ const remarkLintFencedCodeMarker = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file, options) { function (tree, file, options) {
let option = options || 'consistent' const value = String(file)
const contents = 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( file.fail(
'Incorrect fenced code marker `' + 'Unexpected value `' +
option + options +
"`: use either `'consistent'`, `` '`' ``, or `'~'`" "` for `options`, expected ``'`'``, `'~'`, or `'consistent'`"
) )
} }
visit(tree, 'code', function (node) { visitParents(tree, 'code', function (node, parents) {
const start = pointStart(node) const start = pointStart(node)
if (start && typeof start.offset === 'number') { if (start && typeof start.offset === 'number') {
const marker = contents const actual = value
.slice(start.offset, start.offset + 4) .slice(start.offset, start.offset + 4)
.replace(/^\s+/, '') .replace(/^\s+/, '')
.charAt(0) .charAt(0)
// Ignore unfenced code blocks. // Ignore unfenced code blocks.
if (marker === '`' || marker === '~') { if (actual !== '`' && actual !== '~') return
if (option === 'consistent') {
option = marker if (expected) {
} else if (marker !== option) { if (actual !== expected) {
file.message( file.message(
'Fenced code should use `' + 'Unexpected fenced code marker ' +
(option === '~' ? option : '` ` `') + (actual === '~' ? '`~`' : '`` ` ``') +
'` as a marker', ', expected ' +
node (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'
}
)
} }
} }
}) })

View File

@ -35,7 +35,8 @@
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -175,33 +175,33 @@ Pass `fence: '~'` to always use tildes.
## Examples ## Examples
##### `ok.md` ##### `ok-indented.md`
###### In ###### In
```markdown ```markdown
Indented code blocks are not affected by this rule: Indented code blocks are not affected by this rule:
bravo() mercury()
``` ```
###### Out ###### Out
No messages. No messages.
##### `ok.md` ##### `ok-tick.md`
When configured with ``'`'``. When configured with ``'`'``.
###### In ###### In
````markdown ````markdown
```alpha ```javascript
bravo() mercury()
``` ```
``` ```
charlie() venus()
``` ```
```` ````
@ -209,19 +209,19 @@ charlie()
No messages. No messages.
##### `ok.md` ##### `ok-tilde.md`
When configured with `'~'`. When configured with `'~'`.
###### In ###### In
```markdown ```markdown
~~~alpha ~~~javascript
bravo() mercury()
~~~ ~~~
~~~ ~~~
charlie() venus()
~~~ ~~~
``` ```
@ -234,19 +234,19 @@ No messages.
###### In ###### In
````markdown ````markdown
```alpha ```javascript
bravo() mercury()
``` ```
~~~ ~~~
charlie() venus()
~~~ ~~~
```` ````
###### Out ###### Out
```text ```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` ##### `not-ok-consistent-tilde.md`
@ -254,29 +254,29 @@ charlie()
###### In ###### In
````markdown ````markdown
~~~alpha ~~~javascript
bravo() mercury()
~~~ ~~~
``` ```
charlie() venus()
``` ```
```` ````
###### Out ###### Out
```text ```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` ##### `not-ok-incorrect.md`
When configured with `'💩'`. When configured with `'🌍'`.
###### Out ###### Out
```text ```text
1:1: Incorrect fenced code marker `💩`: use either `'consistent'`, `` '`' ``, or `'~'` 1:1: Unexpected value `🌍` for `options`, expected ``'`'``, `'~'`, or `'consistent'`
``` ```
## Compatibility ## Compatibility

View File

@ -62,6 +62,7 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "readme.md"} * {"name": "readme.md"}
* *
@ -74,18 +75,23 @@
* @example * @example
* {"config": {"allowExtensionless": false}, "label": "output", "name": "readme", "positionless": true} * {"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 * @example
* {"label": "output", "name": "readme.mkd", "positionless": true} * {"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 * @example
* {"config": "mkd", "name": "readme.mkd"} * {"config": "mkd", "name": "readme.mkd"}
* *
* @example * @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> | string} Extensions * @typedef {Array<string> | string} Extensions
* File extension(s). * File extension(s).
* *
* @typedef Options * @typedef Options
* Configuration. * Configuration.
* @property {boolean | null | undefined} [allowExtensionless=true] * @property {boolean | null | undefined} [allowExtensionless=true]
* Allow no file extension such as `AUTHORS` or `LICENSE` (default: `true`). * Allow no file extension such as `AUTHORS` or `LICENSE` (default: `true`).
* @property {Extensions | null | undefined} [extensions=['mdx', 'md']] * @property {Readonly<Extensions> | null | undefined} [extensions=['mdx', 'md']]
* Allowed file extension(s) (default: `['mdx', 'md']`). * Allowed file extension(s) (default: `['mdx', 'md']`).
*/ */
import {lintRule} from 'unified-lint-rule'
import {quotation} from 'quotation' import {quotation} from 'quotation'
import {lintRule} from 'unified-lint-rule'
/** @type {ReadonlyArray<string>} */ /** @type {ReadonlyArray<string>} */
const defaultExtensions = ['mdx', 'md'] const defaultExtensions = ['mdx', 'md']
const listFormat = new Intl.ListFormat('en', {type: 'disjunction'}) const listFormat = new Intl.ListFormat('en', {type: 'disjunction'})
const listFormatUnit = new Intl.ListFormat('en', {type: 'unit'})
const remarkLintFileExtension = lintRule( const remarkLintFileExtension = lintRule(
{ {
@ -126,7 +133,7 @@ const remarkLintFileExtension = lintRule(
* Nothing. * Nothing.
*/ */
function (_, file, options) { function (_, file, options) {
let extensions = defaultExtensions let expected = defaultExtensions
let allowExtensionless = true let allowExtensionless = true
/** @type {Readonly<Extensions> | null | undefined} */ /** @type {Readonly<Extensions> | null | undefined} */
let extensionsValue let extensionsValue
@ -147,18 +154,25 @@ const remarkLintFileExtension = lintRule(
} }
if (Array.isArray(extensionsValue)) { if (Array.isArray(extensionsValue)) {
extensions = /** @type {ReadonlyArray<string>} */ (extensionsValue) expected = /** @type {ReadonlyArray<string>} */ (extensionsValue)
} else if (typeof extensionsValue === 'string') { } else if (typeof extensionsValue === 'string') {
extensions = [extensionsValue] expected = [extensionsValue]
} }
const extname = file.extname 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( file.message(
'Incorrect extension: use ' + (actual
listFormat.format(quotation(extensions, '`')) ? 'Unexpected file extension `' + actual + '`'
: 'Unexpected missing file extension') +
', expected ' +
expectedDisplay
) )
} }
} }

View File

@ -193,7 +193,7 @@ When configured with `{ allowExtensionless: false }`.
###### Out ###### Out
```text ```text
1:1: Incorrect extension: use `mdx` or `md` 1:1: Unexpected missing file extension, expected `mdx` or `md`
``` ```
##### `readme.mkd` ##### `readme.mkd`
@ -201,7 +201,7 @@ When configured with `{ allowExtensionless: false }`.
###### Out ###### Out
```text ```text
1:1: Incorrect extension: use `mdx` or `md` 1:1: Unexpected file extension `mkd`, expected `mdx` or `md`
``` ```
##### `readme.mkd` ##### `readme.mkd`
@ -212,13 +212,21 @@ When configured with `'mkd'`.
No messages. No messages.
##### `readme.mkd` ##### `readme.css`
When configured with `[ 'mkd' ]`. When configured with `[
'markdown', 'md',
'mdown', 'mdwn',
'mdx', 'mkd',
'mkdn', 'mkdown',
'ron'
]`.
###### Out ###### Out
No messages. ```text
1:1: Unexpected file extension `css`, expected `markdown`, `md`, `mdown`, …
```
## Compatibility ## Compatibility

View File

@ -38,64 +38,83 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* Paragraph. * Mercury.
* *
* [example]: http://example.com "Example Domain" * [venus]: http://example.com
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"name": "ok.md"}
* *
* Paragraph. * [mercury]: http://example.com/mercury/
* * [venus]: http://example.com/venus/
* [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`)
* *
* @example * @example
* {"name": "ok-html-comments.md"} * {"name": "ok-html-comments.md"}
* *
* Paragraph. * Mercury.
* *
* [example-1]: http://example.com/one/ * [venus]: http://example.com/venus/
* *
* <!-- Comments are fine between and after definitions. --> * <!-- HTML comments in markdown are ignored. -->
* *
* [example-2]: http://example.com/two/ * [earth]: http://example.com/earth/
* *
* @example * @example
* {"name": "ok-mdx-comments.mdx", "mdx": true} * {"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').Nodes} Nodes
* @typedef {import('mdast').FootnoteDefinition} FootnoteDefinition
* @typedef {import('mdast').Root} Root * @typedef {import('mdast').Root} Root
*
* @typedef {import('unist').Point} Point
*/ */
/// <reference types="mdast-util-mdx" /> /// <reference types="mdast-util-mdx" />
import {ok as assert} from 'devlop'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position' import {pointEnd, pointStart} from 'unist-util-position'
import {stringifyPosition} from 'unist-util-stringify-position' import {visitParents} from 'unist-util-visit-parents'
import {visit} from 'unist-util-visit' import {VFileMessage} from 'vfile-message'
const remarkLintFinalDefinition = lintRule( const remarkLintFinalDefinition = lintRule(
{ {
@ -109,14 +128,14 @@ const remarkLintFinalDefinition = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
/** @type {Array<Definition | FootnoteDefinition>} */ /** @type {Array<Array<Nodes>>} */
const definitions = [] const definitionStacks = []
/** @type {Point | undefined} */ /** @type {Array<Nodes> | undefined} */
let last let contentAncestors
visit(tree, function (node) { visitParents(tree, function (node, parents) {
if (node.type === 'definition' || node.type === 'footnoteDefinition') { if (node.type === 'definition' || node.type === 'footnoteDefinition') {
definitions.push(node) definitionStacks.push([...parents, node])
} else if ( } else if (
node.type === 'root' || node.type === 'root' ||
// Ignore HTML comments. // Ignore HTML comments.
@ -128,24 +147,42 @@ const remarkLintFinalDefinition = lintRule(
) { ) {
// Empty. // Empty.
} else { } else {
const place = pointEnd(node) contentAncestors = [...parents, node]
if (place) {
last = place
}
} }
}) })
for (const node of definitions) { const content = contentAncestors ? contentAncestors.at(-1) : undefined
const point = pointStart(node) const contentEnd = pointEnd(content)
if (point && last && point.line < last.line) { if (contentEnd) {
file.message( assert(content) // Always defined.
'Move definitions to the end of the file (after `' + assert(contentAncestors) // Always defined.
stringifyPosition(last) +
'`)', for (const definitionAncestors of definitionStacks) {
node 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
}
)
}
} }
} }
} }

View File

@ -33,12 +33,12 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"@types/unist": "^3.0.0", "devlop": "^1.0.0",
"mdast-util-mdx": "^3.0.0", "mdast-util-mdx": "^3.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-position": "^5.0.0",
"unist-util-stringify-position": "^4.0.0", "unist-util-visit-parents": "^6.0.0",
"unist-util-visit": "^5.0.0" "vfile-message": "^4.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -147,45 +147,40 @@ If you prefer that, turn on this rule.
###### In ###### In
```markdown ```markdown
Paragraph. Mercury.
[example]: http://example.com "Example Domain" [venus]: http://example.com
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `ok.md`
###### In ###### In
```markdown ```markdown
Paragraph. [mercury]: http://example.com/mercury/
[venus]: http://example.com/venus/
[example]: http://example.com "Example Domain"
Another paragraph.
``` ```
###### Out ###### Out
```text No messages.
3:1-3:47: Move definitions to the end of the file (after `5:19`)
```
##### `ok-html-comments.md` ##### `ok-html-comments.md`
###### In ###### In
```markdown ```markdown
Paragraph. Mercury.
[example-1]: http://example.com/one/ [venus]: http://example.com/venus/
<!-- Comments are fine between and after definitions. --> <!-- HTML comments in markdown are ignored. -->
[example-2]: http://example.com/two/ [earth]: http://example.com/earth/
``` ```
###### Out ###### Out
@ -200,19 +195,60 @@ No messages.
> MDX ([`remark-mdx`][github-remark-mdx]). > MDX ([`remark-mdx`][github-remark-mdx]).
```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 ###### Out
No messages. 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 ## Compatibility
Projects maintained by the unified collective are compatible with maintained 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-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-lint]: https://github.com/remarkjs/remark-lint
[github-remark-mdx]: https://mdxjs.com/packages/remark-mdx/ [github-remark-mdx]: https://mdxjs.com/packages/remark-mdx/

View File

@ -45,7 +45,7 @@
* ###### In * ###### In
* *
* ```markdown * ```markdown
* Alpha * Mercury
* ``` * ```
* *
* ###### Out * ###### Out
@ -57,13 +57,13 @@
* ###### In * ###### In
* *
* ```markdown * ```markdown
* Bravo * Mercury
* ``` * ```
* *
* ###### Out * ###### Out
* *
* ```text * ```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 * @module final-newline
@ -76,6 +76,7 @@
* @typedef {import('mdast').Root} Root * @typedef {import('mdast').Root} Root
*/ */
import {ok as assert} from 'devlop'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {location} from 'vfile-location' import {location} from 'vfile-location'
@ -95,8 +96,17 @@ const remarkLintFinalNewline = lintRule(
const end = location(file).toPoint(value.length) const end = location(file).toPoint(value.length)
const last = value.length - 1 const last = value.length - 1
if (end && last > -1 && value.charAt(last) !== '\n') { assert(end) // Always defined.
file.message('Missing newline character at end of file', end)
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
)
} }
} }
) )

View File

@ -33,6 +33,7 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"vfile-location": "^5.0.0" "vfile-location": "^5.0.0"
}, },

View File

@ -150,7 +150,7 @@ always adds final line endings.
###### In ###### In
```markdown ```markdown
Alpha Mercury
``` ```
###### Out ###### Out
@ -162,13 +162,13 @@ No messages.
###### In ###### In
```markdown ```markdown
Bravo Mercury
``` ```
###### Out ###### Out
```text ```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 ## Compatibility

View File

@ -25,16 +25,6 @@
* *
* Transform ([`Transformer` from `unified`][github-unified-transformer]). * Transform ([`Transformer` from `unified`][github-unified-transformer]).
* *
* ### `Depth`
*
* Depth (TypeScript type).
*
* ###### Type
*
* ```ts
* type Depth = 1 | 2 | 3 | 4 | 5 | 6
* ```
*
* ### `Options` * ### `Options`
* *
* Configuration (TypeScript type). * Configuration (TypeScript type).
@ -42,7 +32,7 @@
* ###### Type * ###### Type
* *
* ```ts * ```ts
* type Options = Depth * type Options = 1 | 2 | 3 | 4 | 5 | 6
* ``` * ```
* *
* ## Recommendation * ## Recommendation
@ -55,7 +45,6 @@
* in which case a value of `2` can be defined here or the rule can be turned * in which case a value of `2` can be defined here or the rule can be turned
* off. * off.
* *
* [api-depth]: #depth
* [api-options]: #options * [api-options]: #options
* [api-remark-lint-first-heading-level]: #unifieduseremarklintfirstheadinglevel-options * [api-remark-lint-first-heading-level]: #unifieduseremarklintfirstheadinglevel-options
* [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer * [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer
@ -64,93 +53,55 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"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 * @example
* {"name": "ok-html.md"} * {"name": "ok-html.md"}
* *
* <h1>An HTML heading is also seen by this rule.</h1> * <div>Mercury.</div>
*
* <h1>Venus</h1>
* *
* @example * @example
* {"name": "ok-delayed.md"} * {"mdx": true, "name": "ok-mdx.mdx"}
* *
* You can use markdown content before the heading. * <div>Mercury.</div>
* *
* <div>Or non-heading HTML</div> * <h1>Venus</h1>
*
* <h1>So the first heading, be it HTML or markdown, is checked</h1>
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true}
* *
* ## Bravo * 1:1: Unexpected value `🌍` for `options`, expected `1`, `2`, `3`, `4`, `5`, or `6`
*
* 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"}
*
* <h2>Charlie</h2>
*
* 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}
*
* <h2>Echo</h2>
*
* 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"}
*
* <h1>Golf</h1>
*
* 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, <b>JSX</b> is supported.
*
* <h1>First heading</h1>
*/ */
/** /**
@ -159,18 +110,14 @@
*/ */
/** /**
* @typedef {Heading['depth']} Depth * @typedef {1 | 2 | 3 | 4 | 5 | 6} Options
* Styles.
*
* @typedef {Depth} Options
* Configuration. * Configuration.
*/ */
/// <reference types="mdast-util-mdx" /> /// <reference types="mdast-util-mdx" />
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {position} from 'unist-util-position' import {EXIT, visitParents} from 'unist-util-visit-parents'
import {EXIT, visit} from 'unist-util-visit'
const htmlRe = /<h([1-6])/ const htmlRe = /<h([1-6])/
const jsxNameRe = /^h([1-6])$/ const jsxNameRe = /^h([1-6])$/
@ -189,31 +136,60 @@ const remarkLintFirstHeadingLevel = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file, options) { function (tree, file, options) {
const option = options || 1 /** @type {Heading['depth']} */
let expected
visit(tree, function (node) { if (options === null || options === undefined) {
/** @type {Depth | undefined} */ expected = 1
let rank } else if (
options === 1 ||
options === 2 ||
options === 3 ||
options === 4 ||
options === 5 ||
options === 6
) {
expected = options
} else {
file.fail(
'Unexpected value `' +
options +
'` for `options`, expected `1`, `2`, `3`, `4`, `5`, or `6`'
)
}
visitParents(tree, function (node, parents) {
/** @type {Heading['depth'] | undefined} */
let actual
if (node.type === 'heading') { if (node.type === 'heading') {
rank = node.depth actual = node.depth
} else if (node.type === 'html') { } else if (node.type === 'html') {
const results = node.value.match(htmlRe) const results = node.value.match(htmlRe)
rank = results ? /** @type {Depth} */ (Number(results[1])) : undefined actual = results
? /** @type {Heading['depth']} */ (Number(results[1]))
: undefined
} else if ( } else if (
(node.type === 'mdxJsxFlowElement' || (node.type === 'mdxJsxFlowElement' ||
node.type === 'mdxJsxTextElement') && node.type === 'mdxJsxTextElement') &&
node.name node.name
) { ) {
const results = node.name.match(jsxNameRe) const results = node.name.match(jsxNameRe)
rank = results ? /** @type {Depth} */ (Number(results[1])) : undefined actual = results
? /** @type {Heading['depth']} */ (Number(results[1]))
: undefined
} }
if (rank) { if (actual && node.position) {
const place = position(node) if (node.position && actual !== expected) {
file.message(
if (place && rank !== option) { 'Unexpected first heading rank `' +
file.message('First heading level should be `' + option + '`', place) actual +
'`, expected rank `' +
expected +
'`',
{ancestors: [...parents, node], place: node.position}
)
} }
return EXIT return EXIT

View File

@ -36,8 +36,7 @@
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"mdast-util-mdx": "^3.0.0", "mdast-util-mdx": "^3.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -21,7 +21,6 @@
* [Use](#use) * [Use](#use)
* [API](#api) * [API](#api)
* [`unified().use(remarkLintFirstHeadingLevel[, options])`](#unifieduseremarklintfirstheadinglevel-options) * [`unified().use(remarkLintFirstHeadingLevel[, options])`](#unifieduseremarklintfirstheadinglevel-options)
* [`Depth`](#depth)
* [`Options`](#options) * [`Options`](#options)
* [Recommendation](#recommendation) * [Recommendation](#recommendation)
* [Examples](#examples) * [Examples](#examples)
@ -115,8 +114,7 @@ On the CLI in a config file (here a `package.json`):
## API ## API
This package exports no identifiers. This package exports no identifiers.
It exports the [TypeScript][typescript] types It exports the [TypeScript][typescript] type
[`Depth`][api-depth] and
[`Options`][api-options]. [`Options`][api-options].
The default export is The default export is
[`remarkLintFirstHeadingLevel`][api-remark-lint-first-heading-level]. [`remarkLintFirstHeadingLevel`][api-remark-lint-first-heading-level].
@ -134,16 +132,6 @@ Warn when the first heading has an unexpected rank.
Transform ([`Transformer` from `unified`][github-unified-transformer]). Transform ([`Transformer` from `unified`][github-unified-transformer]).
### `Depth`
Depth (TypeScript type).
###### Type
```ts
type Depth = 1 | 2 | 3 | 4 | 5 | 6
```
### `Options` ### `Options`
Configuration (TypeScript type). Configuration (TypeScript type).
@ -151,7 +139,7 @@ Configuration (TypeScript type).
###### Type ###### Type
```ts ```ts
type Options = Depth type Options = 1 | 2 | 3 | 4 | 5 | 6
``` ```
## Recommendation ## Recommendation
@ -171,35 +159,21 @@ off.
###### In ###### In
```markdown ```markdown
# The default is to expect a level one heading # Mercury
``` ```
###### Out ###### Out
No messages. No messages.
##### `ok-html.md` ##### `ok-delay.md`
###### In ###### In
```markdown ```markdown
<h1>An HTML heading is also seen by this rule.</h1> Mercury.
```
###### Out # Venus
No messages.
##### `ok-delayed.md`
###### In
```markdown
You can use markdown content before the heading.
<div>Or non-heading HTML</div>
<h1>So the first heading, be it HTML or markdown, is checked</h1>
``` ```
###### Out ###### Out
@ -211,31 +185,15 @@ No messages.
###### In ###### In
```markdown ```markdown
## Bravo ## Mercury
Paragraph. Venus.
``` ```
###### Out ###### Out
```text ```text
1:1-1:9: First heading level should be `1` 1:1-1:11: Unexpected first heading rank `2`, expected rank `1`
```
##### `not-ok-html.md`
###### In
```markdown
<h2>Charlie</h2>
Paragraph.
```
###### Out
```text
1:1-1:17: First heading level should be `1`
``` ```
##### `ok.md` ##### `ok.md`
@ -245,9 +203,9 @@ When configured with `2`.
###### In ###### In
```markdown ```markdown
## Delta ## Mercury
Paragraph. Venus.
``` ```
###### Out ###### Out
@ -256,57 +214,19 @@ No messages.
##### `ok-html.md` ##### `ok-html.md`
When configured with `2`.
###### In ###### In
```markdown ```markdown
<h2>Echo</h2> <div>Mercury.</div>
Paragraph. <h1>Venus</h1>
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `ok-mdx.mdx`
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
<h1>Golf</h1>
Paragraph.
```
###### Out
```text
1:1-1:14: First heading level should be `2`
```
##### `ok.mdx`
###### In ###### In
@ -314,15 +234,25 @@ Paragraph.
> MDX ([`remark-mdx`][github-remark-mdx]). > MDX ([`remark-mdx`][github-remark-mdx]).
```mdx ```mdx
In MDX, <b>JSX</b> is supported. <div>Mercury.</div>
<h1>First heading</h1> <h1>Venus</h1>
``` ```
###### Out ###### Out
No messages. 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 ## Compatibility
Projects maintained by the unified collective are compatible with maintained Projects maintained by the unified collective are compatible with maintained
@ -348,8 +278,6 @@ abide by its terms.
[MIT][file-license] © [Titus Wormer][author] [MIT][file-license] © [Titus Wormer][author]
[api-depth]: #depth
[api-options]: #options [api-options]: #options
[api-remark-lint-first-heading-level]: #unifieduseremarklintfirstheadinglevel-options [api-remark-lint-first-heading-level]: #unifieduseremarklintfirstheadinglevel-options

View File

@ -41,19 +41,29 @@
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* Lorem ipsum * **Mercury** is the first planet from the Sun
* dolor sit amet * and the smallest in the Solar System.
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "not-ok.md"}
* *
* Lorem ipsum * **Mercury** is the first planet from the Sun
* dolor sit amet. * 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 * @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) const value = String(file)
visit(tree, 'break', function (node) { visit(tree, 'break', function (node) {
const start = pointStart(node)
const end = pointEnd(node) const end = pointEnd(node)
const start = pointStart(node)
if ( if (
end && end &&
@ -88,13 +98,18 @@ const remarkLintHardBreakSpaces = lintRule(
typeof end.offset === 'number' && typeof end.offset === 'number' &&
typeof start.offset === 'number' typeof start.offset === 'number'
) { ) {
const slice = value const slice = value.slice(start.offset, end.offset)
.slice(start.offset, end.offset)
.split('\n', 1)[0]
.replace(/\r$/, '')
if (slice.length > 2) { let actual = 0
file.message('Use two spaces for hard line breaks', node) while (slice.charCodeAt(actual) === 32) actual++
if (actual > 2) {
file.message(
'Unexpected `' +
actual +
'` spaces for hard break, expected `2` spaces',
node
)
} }
} }
}) })

View File

@ -48,7 +48,8 @@
"xo": { "xo": {
"prettier": true, "prettier": true,
"rules": { "rules": {
"capitalized-comments": "off" "capitalized-comments": "off",
"unicorn/prefer-code-point": "off"
} }
} }
} }

View File

@ -148,8 +148,8 @@ Due to this, its recommended to turn this rule on.
###### In ###### In
```markdown ```markdown
Lorem ipsum␠␠ **Mercury** is the first planet from the Sun␠␠
dolor sit amet and the smallest in the Solar System.
``` ```
###### Out ###### Out
@ -161,14 +161,33 @@ No messages.
###### In ###### In
```markdown ```markdown
Lorem ipsum␠␠␠ **Mercury** is the first planet from the Sun␠␠␠
dolor sit amet. and the smallest in the Solar System.
``` ```
###### Out ###### Out
```text ```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 ## Compatibility
@ -240,6 +259,8 @@ abide by its terms.
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c [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-lint]: https://github.com/remarkjs/remark-lint
[github-unified-transformer]: https://github.com/unifiedjs/unified#transformer [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer

View File

@ -44,50 +44,88 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* # Alpha * # Mercury
* *
* ## Bravo * ## Nomenclature
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"name": "also-ok.md"}
* *
* # Charlie * #### Impact basins and craters
* *
* ### Delta * #### Plains
*
* #### Compressional features
* *
* @example * @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 * @example
* {"name": "html.md"} * {"label": "output", "name": "not-ok.md"}
* *
* In markdown, <b>HTML</b> is supported. * 3:1-3:23: Unexpected heading rank `3`, exected rank `2`
* * 5:1-5:20: Unexpected heading rank `3`, exected rank `2`
* <h1>First heading</h1> * 9:1-9:16: Unexpected heading rank `4`, exected rank `3`
* *
* @example * @example
* {"name": "ok.mdx", "mdx": true} * {"label": "input", "name": "html.md"}
* *
* In MDX, <b>JSX</b> is supported. * # Mercury
* *
* <h1>First heading</h1> * <b>Mercury</b> is the first planet from the Sun and the smallest
* in the Solar System.
*
* <h3>Internal structure</h3>
*
* <h2>Orbit, rotation, and longitude</h2>
* @example
* {"label": "output", "name": "html.md"}
*
* 6:1-6:28: Unexpected heading rank `3`, exected rank `2`
*
* @example
* {"mdx": true, "name": "mdx.mdx"}
*
* # Mercury
*
* <b>Mercury</b> is the first planet from the Sun and the smallest
* in the Solar System.
*
* <h3>Internal structure</h3>
*
* <h2>Orbit, rotation, and longitude</h2>
* @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').Heading} Heading
* @typedef {import('mdast').Nodes} Nodes
* @typedef {import('mdast').Root} Root * @typedef {import('mdast').Root} Root
*/ */
/// <reference types="mdast-util-mdx" /> /// <reference types="mdast-util-mdx" />
import {ok as assert} from 'devlop'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {position} from 'unist-util-position' import {visitParents} from 'unist-util-visit-parents'
import {visit} from 'unist-util-visit' import {VFileMessage} from 'vfile-message'
const htmlRe = /<h([1-6])/ const htmlRe = /<h([1-6])/
const jsxNameRe = /^h([1-6])$/ const jsxNameRe = /^h([1-6])$/
@ -104,47 +142,89 @@ const remarkLintHeadingIncrement = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
/** @type {Heading['depth'] | undefined} */ /** @type {Array<Array<Nodes> | undefined>} */
let previous const stack = []
visit(tree, function (node) { visitParents(tree, function (node, parents) {
const place = position(node) const rank = inferRank(node)
if (place) { if (rank) {
/** @type {Heading['depth'] | undefined} */ let index = rank
let rank /** @type {Array<Nodes> | undefined} */
let closestAncestors
if (node.type === 'heading') { while (index--) {
rank = node.depth if (stack[index]) {
} else if (node.type === 'html') { closestAncestors = stack[index]
const results = node.value.match(htmlRe) break
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
} }
if (rank) { if (closestAncestors) {
if (previous && rank > previous + 1) { 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( file.message(
'Heading levels should increment by one level at a time', 'Unexpected heading rank `' +
place 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 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
}

View File

@ -33,10 +33,11 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"mdast-util-mdx": "^3.0.0", "mdast-util-mdx": "^3.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0",
"unist-util-visit": "^5.0.0" "vfile-message": "^4.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -153,9 +153,25 @@ its recommended that this rule is turned on.
###### In ###### In
```markdown ```markdown
# Alpha # Mercury
## Bravo ## Nomenclature
```
###### Out
No messages.
##### `also-ok.md`
###### In
```markdown
#### Impact basins and craters
#### Plains
#### Compressional features
``` ```
###### Out ###### Out
@ -167,15 +183,23 @@ No messages.
###### In ###### In
```markdown ```markdown
# Charlie # Mercury
### Delta ### Internal structure
### Surface geology
## Observation history
#### Mariner 10
``` ```
###### Out ###### Out
```text ```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` ##### `html.md`
@ -183,16 +207,23 @@ No messages.
###### In ###### In
```markdown ```markdown
In markdown, <b>HTML</b> is supported. # Mercury
<h1>First heading</h1> <b>Mercury</b> is the first planet from the Sun and the smallest
in the Solar System.
<h3>Internal structure</h3>
<h2>Orbit, rotation, and longitude</h2>
``` ```
###### Out ###### Out
No messages. ```text
6:1-6:28: Unexpected heading rank `3`, exected rank `2`
```
##### `ok.mdx` ##### `mdx.mdx`
###### In ###### In
@ -200,14 +231,21 @@ No messages.
> MDX ([`remark-mdx`][github-remark-mdx]). > MDX ([`remark-mdx`][github-remark-mdx]).
```mdx ```mdx
In MDX, <b>JSX</b> is supported. # Mercury
<h1>First heading</h1> <b>Mercury</b> is the first planet from the Sun and the smallest
in the Solar System.
<h3>Internal structure</h3>
<h2>Orbit, rotation, and longitude</h2>
``` ```
###### Out ###### Out
No messages. ```text
6:1-6:28: Unexpected heading rank `3`, exected rank `2`
```
## Compatibility ## Compatibility

View File

@ -81,55 +81,55 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
* @example
* {"name": "ok.md", "config": "atx"}
*
* # Alpha
*
* ## Bravo
*
* ### Charlie
* *
* @example * @example
* {"name": "ok.md", "config": "atx-closed"} * {"config": "atx", "name": "ok.md"}
* *
* # Delta ## * # Mercury
* *
* ## Echo ## * ## Venus
* *
* ### Foxtrot ### * ### Earth
* *
* @example * @example
* {"name": "ok.md", "config": "setext"} * {"config": "atx-closed", "name": "ok.md"}
* *
* Golf * # Mercury ##
* ====
* *
* Hotel * ## Venus ##
* -----
* *
* ### India * ### Earth ###
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"config": "setext", "name": "ok.md"}
* *
* Juliett * Mercury
* ======= * =======
* *
* ## Kilo * Venus
* -----
* *
* ### Lima ### * ### Earth
* *
* @example * @example
* {"name": "not-ok.md", "label": "output"} * {"label": "input", "name": "not-ok.md"}
* *
* 4:1-4:8: Headings should use setext * Mercury
* 6:1-6:13: Headings should use setext * =======
*
* ## 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 * @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 {headingStyle} from 'mdast-util-heading-style'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {position} from 'unist-util-position' 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( const remarkLintHeadingStyle = lintRule(
{ {
@ -163,30 +164,55 @@ const remarkLintHeadingStyle = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file, options) { function (tree, file, options) {
let option = options || 'consistent' /** @type {VFileMessage | undefined} */
let cause
/** @type {Style | undefined} */
let expected
if ( if (options === null || options === undefined || options === 'consistent') {
option !== 'atx' && // Empty.
option !== 'atx-closed' && } else if (
option !== 'consistent' && options === 'atx' ||
option !== 'setext' options === 'atx-closed' ||
options === 'setext'
) { ) {
expected = options
} else {
file.fail( file.fail(
'Incorrect heading style type `' + 'Unexpected value `' +
option + options +
"`: use either `'consistent'`, `'atx'`, `'atx-closed'`, or `'setext'`" "` 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 place = position(node)
const actual = headingStyle(node, expected)
if (place) { if (actual) {
if (option === 'consistent') { if (expected) {
/* c8 ignore next -- funky nodes perhaps cannot be detected. */ if (place && actual !== expected) {
option = headingStyle(node) || 'consistent' file.message(
} else if (headingStyle(node, option) !== option) { 'Unexpected ' +
file.message('Headings should use ' + option, place) 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 export default remarkLintHeadingStyle
/**
* @param {Style} style
* Style.
* @returns {string}
* Display.
*/
function displayStyle(style) {
return style === 'atx'
? 'ATX'
: style === 'atx-closed'
? 'ATX (closed)'
: 'setext'
}

View File

@ -37,7 +37,8 @@
"mdast-util-heading-style": "^3.0.0", "mdast-util-heading-style": "^3.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -195,11 +195,11 @@ When configured with `'atx'`.
###### In ###### In
```markdown ```markdown
# Alpha # Mercury
## Bravo ## Venus
### Charlie ### Earth
``` ```
###### Out ###### Out
@ -213,11 +213,11 @@ When configured with `'atx-closed'`.
###### In ###### In
```markdown ```markdown
# Delta ## # Mercury ##
## Echo ## ## Venus ##
### Foxtrot ### ### Earth ###
``` ```
###### Out ###### Out
@ -231,13 +231,13 @@ When configured with `'setext'`.
###### In ###### In
```markdown ```markdown
Golf Mercury
==== =======
Hotel Venus
----- -----
### India ### Earth
``` ```
###### Out ###### Out
@ -249,29 +249,29 @@ No messages.
###### In ###### In
```markdown ```markdown
Juliett Mercury
======= =======
## Kilo ## Venus
### Lima ### ### Earth ###
``` ```
###### Out ###### Out
```text ```text
4:1-4:8: Headings should use setext 4:1-4:9: Unexpected ATX heading, expected setext
6:1-6:13: Headings should use setext 6:1-6:14: Unexpected ATX (closed) heading, expected setext
``` ```
##### `not-ok.md` ##### `not-ok.md`
When configured with `'💩'`. When configured with `'🌍'`.
###### Out ###### Out
```text ```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 ## Compatibility

View File

@ -55,7 +55,7 @@
* *
* ## Fix * ## Fix
* *
* [`remark-stringify`][github-remark-stringify] always uses Unix linebreaks. * [`remark-stringify`][github-remark-stringify] always uses Unix line endings.
* *
* [api-options]: #options * [api-options]: #options
* [api-remark-lint-linebreak-style]: #unifieduseremarklintlinebreakstyle-options * [api-remark-lint-linebreak-style]: #unifieduseremarklintlinebreakstyle-options
@ -67,35 +67,56 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2017 Titus Wormer * @copyright 2017 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok-consistent-as-windows.md"} * {"name": "ok-consistent-as-windows.md"}
* *
* AlphaBravo * MercuryandVenus.
* *
* @example * @example
* {"name": "ok-consistent-as-unix.md"} * {"name": "ok-consistent-as-unix.md"}
* *
* AlphaBravo * MercuryandVenus.
* *
* @example * @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 * @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 * @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 * @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. * Styles.
*/ */
import {ok as assert} from 'devlop'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {location} from 'vfile-location' import {location} from 'vfile-location'
import {VFileMessage} from 'vfile-message'
const escaped = {unix: '\\n', windows: '\\r\\n'} const max = 5
const remarkLintLinebreakStyle = lintRule( const remarkLintLinebreakStyle = lintRule(
{ {
@ -129,28 +152,60 @@ const remarkLintLinebreakStyle = lintRule(
* Nothing. * Nothing.
*/ */
function (_, file, options) { function (_, file, options) {
let option = options || 'consistent'
const value = String(file) const value = String(file)
const toPoint = location(value).toPoint const toPoint = location(value).toPoint
let index = value.indexOf('\n') 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) { 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') { if (expected) {
option = type if (expected !== actual) {
} else if (option !== type) { if (messages === max) {
file.message( file.info(
'Expected linebreaks to be ' + 'Unexpected large number of incorrect line endings, stopping',
option + {place}
' (`' + )
escaped[option] + return
'`), not ' + }
type +
' (`' + file.message(
escaped[type] + 'Unexpected ' +
'`)', displayStyle(actual) +
toPoint(index) ' 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 export default remarkLintLinebreakStyle
/**
* @param {Style} style
* Style.
* @returns {string}
* Display.
*/
function displayStyle(style) {
return style === 'unix' ? 'unix (`\\n`)' : 'windows (`\\r\\n`)'
}

View File

@ -38,8 +38,10 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"vfile-location": "^5.0.0" "vfile-location": "^5.0.0",
"vfile-message": "^4.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -165,7 +165,7 @@ used.
## Fix ## Fix
[`remark-stringify`][github-remark-stringify] always uses Unix linebreaks. [`remark-stringify`][github-remark-stringify] always uses Unix line endings.
## Examples ## Examples
@ -174,7 +174,7 @@ used.
###### In ###### In
```markdown ```markdown
Alpha␍␊Bravo␍␊ Mercury␍␊and␍␊Venus.
``` ```
###### Out ###### Out
@ -186,7 +186,7 @@ No messages.
###### In ###### In
```markdown ```markdown
Alpha␊Bravo␊ Mercury␊and␊Venus.
``` ```
###### Out ###### Out
@ -200,13 +200,13 @@ When configured with `'unix'`.
###### In ###### In
```markdown ```markdown
Alpha␍␊ Mercury.␍␊
``` ```
###### Out ###### Out
```text ```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` ##### `not-ok-windows.md`
@ -216,13 +216,44 @@ When configured with `'windows'`.
###### In ###### In
```markdown ```markdown
Alpha Mercury.
``` ```
###### Out ###### Out
```text ```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 ## Compatibility

View File

@ -3,7 +3,8 @@
* *
* ## What is this? * ## 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? * ## When should I use this?
* *
@ -59,7 +60,7 @@
* *
* ## Fix * ## Fix
* *
* [`remark-stringify`][github-remark-stringify] formats titles with double * [`remark-stringify`][github-remark-stringify] formats titles with double
* quotes by default. * quotes by default.
* Pass `quote: "'"` to use single quotes. * Pass `quote: "'"` to use single quotes.
* There is no option to use parens. * There is no option to use parens.
@ -74,82 +75,90 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @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 theyre not a title (see GH-166):
*
* [Example](#Heading-(optional))
* *
* @example * @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 * @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 * @example
* {"name": "ok.md", "config": "'"} * {"config": "\"", "name": "ok-double.md"}
* *
* [Example](http://example.com#without-title) * [Mercury](http://example.com/mercury/ "Go to Mercury").
* [Example](http://example.com 'Example Domain')
* ![Example](http://example.com 'Example Domain')
*
* [Example]: http://example.com 'Example Domain'
* *
* @example * @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 * @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 * @example
* {"name": "ok.md", "config": "()"} * {"config": "'", "label": "input", "name": "not-ok-single.md"}
* *
* [Example](http://example.com#without-title) * [Mercury](http://example.com/mercury/ "Go to Mercury").
* [Example](http://example.com (Example Domain)) * @example
* ![Example](http://example.com (Example Domain)) * {"config": "'", "label": "output", "name": "not-ok-single.md"}
* *
* [Example]: http://example.com (Example Domain) * 1:1-1:55: Unexpected title markers `"`, expected `'`
* *
* @example * @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 * @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 * @example
* {"name": "not-ok.md", "label": "input"} * {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true}
* *
* [Example](http://example.com "Example Domain") * 1:1: Unexpected value `🌍` for `options`, expected `'"'`, `"'"`, `'()'`, or `'consistent'`
* [Example](http://example.com 'Example Domain')
* *
* @example * @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 * @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 {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position' import {pointEnd} from 'unist-util-position'
import {visit} from 'unist-util-visit' import {visitParents} from 'unist-util-visit-parents'
import {location} from 'vfile-location' import {VFileMessage} from 'vfile-message'
const markers = {
'"': '"',
"'": "'",
')': '('
}
const remarkLintLinkTitleStyle = lintRule( const remarkLintLinkTitleStyle = lintRule(
{ {
@ -190,78 +193,88 @@ const remarkLintLinkTitleStyle = lintRule(
*/ */
function (tree, file, options) { function (tree, file, options) {
const value = String(file) const value = String(file)
const loc = location(file) /** @type {Style | undefined} */
const option = options || 'consistent' let expected
// @ts-expect-error: allow `(` too, even though untyped. /** @type {VFileMessage | undefined} */
let look = option === '()' || option === '(' ? ')' : option 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( file.fail(
'Incorrect link title style marker `' + 'Unexpected value `' +
look + options +
"`: use either `'consistent'`, `'\"'`, `'\\''`, or `'()'`" "` for `options`, expected `'\"'`, `\"'\"`, `'()'`, or `'consistent'`"
) )
} }
visit(tree, function (node) { visitParents(tree, function (node, parents) {
if ( if (
node.type === 'definition' || node.type === 'definition' ||
node.type === 'image' || node.type === 'image' ||
node.type === 'link' node.type === 'link'
) { ) {
const tail = // Exit w/o title.
'children' in node if (!node.title) return
? node.children[node.children.length - 1]
: undefined
const begin = tail ? pointEnd(tail) : pointStart(node)
const end = pointEnd(node) const end = pointEnd(node)
let endIndex = end ? end.offset : undefined
if ( // Exit w/o position.
!begin || if (!endIndex) return
!end ||
typeof begin.offset !== 'number' || // `)`
typeof end.offset !== 'number' if (node.type !== 'definition') endIndex--
) {
return // 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') { /* c8 ignore next -- we should find a correct marker. */
last-- if (!actual) return
}
const final = /** @type {keyof markers} */ (value.charAt(last)) if (expected) {
if (actual !== expected) {
// Exit if the final marker is not a known marker. file.message(
if (!(final in markers)) { 'Unexpected title markers ' +
return displayStyle(actual) +
} ', expected ' +
displayStyle(expected),
const initial = markers[final] {ancestors: [...parents, node], cause, place: node.position}
)
// Find the starting delimiter }
const first = value.lastIndexOf(initial, last - 1) } else {
expected = actual
// Exit if theres no starting delimiter, the starting delimiter is before cause = new VFileMessage(
// the start of the node, or if its not preceded by whitespace. 'Title marker style ' +
if (first <= begin.offset || !/\s/.test(value.charAt(first - 1))) { displayStyle(expected) +
return " first defined for `'consistent'` here",
} {
ancestors: [...parents, node],
if (look === 'consistent') { place: node.position,
look = final ruleId: 'link-title-style',
} else if (look !== final) { source: 'remark-lint'
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
) )
} }
} }
@ -270,3 +283,13 @@ const remarkLintLinkTitleStyle = lintRule(
) )
export default remarkLintLinkTitleStyle export default remarkLintLinkTitleStyle
/**
* @param {Style} style
* Style.
* @returns {string}
* Display.
*/
function displayStyle(style) {
return style === '"' ? '`"`' : style === "'" ? "`'`" : "`'('` and `')'`"
}

View File

@ -36,8 +36,8 @@
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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" "vfile-message": "^4.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {
@ -50,7 +50,9 @@
"prettier": true, "prettier": true,
"rules": { "rules": {
"capitalized-comments": "off", "capitalized-comments": "off",
"unicorn/prefer-at": "off" "unicorn/prefer-at": "off",
"unicorn/prefer-code-point": "off",
"unicorn/prefer-switch": "off"
} }
} }
} }

View File

@ -32,7 +32,8 @@
## What is this? ## 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? ## When should I use this?
@ -174,143 +175,179 @@ markdown, so its recommended to configure this rule with `'"'`.
## Fix ## Fix
[`remark-stringify`][github-remark-stringify] formats titles with double [`remark-stringify`][github-remark-stringify] formats titles with double
quotes by default. quotes by default.
Pass `quote: "'"` to use single quotes. Pass `quote: "'"` to use single quotes.
There is no option to use parens. There is no option to use parens.
## Examples ## 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 `'"'`. When configured with `'"'`.
###### In ###### In
```markdown ```markdown
[Example](http://example.com#without-title) [Mercury](http://example.com/mercury/ "Go to Mercury").
[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 theyre not a title (see GH-166):
[Example](#Heading-(optional))
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `not-ok-double.md`
When configured with `'"'`. When configured with `'"'`.
###### In ###### In
```markdown ```markdown
[Example]: http://example.com 'Example Domain' [Mercury](http://example.com/mercury/ 'Go to Mercury').
``` ```
###### Out ###### Out
```text ```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 `"'"`. When configured with `"'"`.
###### In ###### In
```markdown ```markdown
[Example](http://example.com#without-title) [Mercury](http://example.com/mercury/ 'Go to Mercury').
[Example](http://example.com 'Example Domain')
![Example](http://example.com 'Example Domain')
[Example]: http://example.com 'Example Domain'
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `not-ok-single.md`
When configured with `"'"`. When configured with `"'"`.
###### In ###### In
```markdown ```markdown
[Example]: http://example.com "Example Domain" [Mercury](http://example.com/mercury/ "Go to Mercury").
``` ```
###### Out ###### Out
```text ```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 `'()'`. When configured with `'()'`.
###### In ###### In
```markdown ```markdown
[Example](http://example.com#without-title) [Mercury](http://example.com/mercury/ (Go to Mercury)).
[Example](http://example.com (Example Domain))
![Example](http://example.com (Example Domain))
[Example]: http://example.com (Example Domain)
``` ```
###### Out ###### Out
No messages. No messages.
##### `not-ok.md` ##### `not-ok-paren.md`
When configured with `'()'`. When configured with `'()'`.
###### In ###### In
```markdown ```markdown
[Example](http://example.com 'Example Domain') [Mercury](http://example.com/mercury/ "Go to Mercury").
``` ```
###### Out ###### Out
```text ```text
1:30-1:46: Titles should use `()` as a quote 1:1-1:55: Unexpected title markers `"`, expected `'('` and `')'`
``` ```
##### `not-ok.md` ##### `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 ###### In
```markdown ```markdown
[Example](http://example.com "Example Domain") Parens in URLs work correctly:
[Example](http://example.com 'Example Domain')
[Mercury](http://example.com/(mercury) "Go to Mercury") and
[Venus](http://example.com/(venus)).
``` ```
###### Out ###### Out
```text No messages.
2:30-2:46: Titles should use `"` as a quote
##### `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 ###### Out
```text No messages.
1:1: Incorrect link title style marker `💩`: use either `'consistent'`, `'"'`, `'\''`, or `'()'`
```
## Compatibility ## Compatibility

View File

@ -30,9 +30,9 @@
* While it is possible to use an indent to align ordered lists on their marker: * While it is possible to use an indent to align ordered lists on their marker:
* *
* ```markdown * ```markdown
* 1. One * 1. Mercury
* 10. Ten * 10. Venus
* 100. Hundred * 100. Earth
* ``` * ```
* *
* such a style is uncommon and hard to maintain as adding a 10th item * such a style is uncommon and hard to maintain as adding a 10th item
@ -56,34 +56,33 @@
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* Paragraph. * Mercury.
* *
* * List item * * Venus.
* * List item * * Earth.
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "not-ok.md"}
* *
* Paragraph. * Mercury.
* *
* * List item * * Venus.
* * List item * * Earth.
* *
* @example * @example
* {"name": "not-ok.md", "label": "output"} * {"label": "output", "name": "not-ok.md"}
* *
* 3:2: Incorrect indentation before bullet: remove 1 space * 3:2: Unexpected `1` space before list item, expected `0` spaces, remove them
* 4:2: Incorrect indentation before bullet: remove 1 space * 4:2: Unexpected `1` space before list item, expected `0` spaces, remove them
*/ */
/** /**
* @typedef {import('mdast').Root} Root * @typedef {import('mdast').Root} Root
*/ */
import plural from 'pluralize' import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart} from 'unist-util-position' import {pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit'
const remarkLintListItemBulletIndent = lintRule( const remarkLintListItemBulletIndent = lintRule(
{ {
@ -97,34 +96,37 @@ const remarkLintListItemBulletIndent = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
visit(tree, 'list', function (list, _, grandparent) { const treeStart = pointStart(tree)
let index = -1
const pointStartGrandparent = pointStart(grandparent)
while (++index < list.children.length) { // Unknown containers are not supported.
const item = list.children[index] if (!tree || tree.type !== 'root' || !treeStart) return
const itemStart = pointStart(item)
if ( for (const child of tree.children) {
grandparent && if (child.type !== 'list') continue
pointStartGrandparent &&
itemStart &&
grandparent.type === 'root'
) {
const indent = itemStart.column - pointStartGrandparent.column
if (indent) { const list = child
file.message(
'Incorrect indentation before bullet: remove ' + for (const item of list.children) {
indent + const place = pointStart(item)
' ' +
plural('space', indent), /* c8 ignore next 2 -- doesnt happen in tests as the whole tree is
itemStart * 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}
)
} }
} }
}) }
} }
) )

View File

@ -35,8 +35,7 @@
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-position": "^5.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -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: While it is possible to use an indent to align ordered lists on their marker:
```markdown ```markdown
1. One 1. Mercury
10. Ten 10. Venus
100. Hundred 100. Earth
``` ```
…such a style is uncommon and hard to maintain as adding a 10th item …such a style is uncommon and hard to maintain as adding a 10th item
@ -162,10 +162,10 @@ indent.
###### In ###### In
```markdown ```markdown
Paragraph. Mercury.
* List item * Venus.
* List item * Earth.
``` ```
###### Out ###### Out
@ -177,17 +177,17 @@ No messages.
###### In ###### In
```markdown ```markdown
Paragraph. Mercury.
␠* List item ␠* Venus.
␠* List item ␠* Earth.
``` ```
###### Out ###### Out
```text ```text
3:2: Incorrect indentation before bullet: remove 1 space 3:2: Unexpected `1` space before list item, expected `0` spaces, remove them
4:2: Incorrect indentation before bullet: remove 1 space 4:2: Unexpected `1` space before list item, expected `0` spaces, remove them
``` ```
## Compatibility ## Compatibility

View File

@ -5,6 +5,8 @@
* ## What is this? * ## What is this?
* *
* This package checks the indent of list item content. * 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? * ## When should I use this?
* *
@ -42,32 +44,76 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
* @example
* {"name": "ok.md", "gfm": true}
*
* 1.[x] Alpha
* 1. Bravo
* *
* @example * @example
* {"name": "not-ok.md", "label": "input", "gfm": true} * {"name": "ok.md"}
* *
* 1.[x] Charlie * 1.Mercury.
* 1. Delta * ***
* * Venus.
* *
* @example * @example
* {"name": "not-ok.md", "label": "output", "gfm": true} * {"label": "input", "name": "not-ok.md"}
* *
* 2:5: Dont 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 * @typedef {import('mdast').Root} Root
*/ */
import plural from 'pluralize' import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart} from 'unist-util-position' 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( const remarkLintListItemContentIndent = lintRule(
{ {
@ -82,57 +128,67 @@ const remarkLintListItemContentIndent = lintRule(
*/ */
function (tree, file) { function (tree, file) {
const value = String(file) const value = String(file)
/** @type {VFileMessage | undefined} */
let cause
visit(tree, 'listItem', function (node) { visitParents(tree, 'listItem', function (node, parents) {
let index = -1 let index = -1
/** @type {number | undefined} */ /** @type {number | undefined} */
let style let expected
while (++index < node.children.length) { while (++index < node.children.length) {
const item = node.children[index] const child = node.children[index]
const begin = pointStart(item) const childStart = pointStart(child)
if (!begin || typeof begin.offset !== 'number') { if (!childStart || typeof childStart.offset !== 'number') {
continue continue
} }
let column = begin.column let actual = childStart.column
// Get indentation for the first child. // Get indentation for the first child.
// Only the first item can have a checkbox, so here we remove that from // Only the first item can have a checkbox,
// the column. // when its a paragraph,
if (index === 0) { // so here we remove that from the column.
// If theres a checkbox before the content, look backwards to find if (index === 0 && typeof node.checked === 'boolean') {
// the start of that checkbox. let beforeIndex = childStart.offset - 1
if (typeof node.checked === 'boolean') {
let char = begin.offset - 1
while (char > 0 && value.charAt(char) !== '[') { while (
char-- beforeIndex > 0 &&
} value.charCodeAt(beforeIndex) !== 91 /* `[` */
) {
column -= begin.offset - char beforeIndex--
} }
style = column actual -= childStart.offset - beforeIndex
continue
} }
// Warn for violating children. if (expected) {
if (style && column !== style) { // Warn for violating children.
const diff = style - column if (actual !== expected) {
const abs = Math.abs(diff) const difference = expected - actual
const differenceAbsolute = Math.abs(difference)
file.message( file.message(
'Dont use mixed indentation for children, ' + 'Unexpected unaligned list item child, expected to align with first child, ' +
/* c8 ignore next -- hard to test, I couldnt find it at least. */ (difference > 0 ? 'add' : 'remove') +
(diff > 0 ? 'add' : 'remove') + ' `' +
' ' + differenceAbsolute +
abs + '` ' +
' ' + pluralize('space', differenceAbsolute),
plural('space', abs), {ancestors: [...parents, node, child], cause, place: childStart}
{line: begin.line, column} )
}
} 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'
}
) )
} }
} }

View File

@ -37,7 +37,8 @@
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {
@ -49,7 +50,8 @@
"xo": { "xo": {
"prettier": true, "prettier": true,
"rules": { "rules": {
"capitalized-comments": "off" "capitalized-comments": "off",
"unicorn/prefer-code-point": "off"
} }
} }
} }

View File

@ -32,6 +32,8 @@ consistent.
## What is this? ## What is this?
This package checks the indent of list item content. 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? ## When should I use this?
@ -151,12 +153,10 @@ Further children should align with it.
###### In ###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
1.␠[x] Alpha 1.␠Mercury.
␠␠␠1. Bravo ␠␠␠***
␠␠␠* Venus.
``` ```
###### Out ###### Out
@ -167,18 +167,82 @@ No messages.
###### In ###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
1.␠[x] Charlie 1.␠Mercury.
␠␠␠␠1. Delta ␠␠␠␠␠***
␠␠␠␠* Venus.
``` ```
###### Out ###### Out
```text ```text
2:5: Dont 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 ## Compatibility

View File

@ -92,114 +92,204 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* *List * *Mercury.
* item. * *Venus.
* *
* Paragraph. * 111.Earth
* and Mars.
* *
* 11.List * ***Jupiter**.
* item.
* *
* Paragraph. * Jupiter is the fifth planet from the Sun and the largest in the Solar
* System.
* *
* *List * *Saturn.
* item.
* *
* *List * Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
* item.
* *
* @example * @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 * *Saturn.
* item.
* *
* *List * Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
* item.
* *
* @example * @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 * *Saturn.
* item.
* *
* *List * Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
* item. * @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 * @example
* {"config": "tab", "name": "ok.md"} * {"config": "tab", "name": "ok.md"}
* *
* *List * *Mercury.
* item. * *Venus.
* *
* Paragraph. * 111.Earth
* and Mars.
* *
* 11.List * ***Jupiter**.
* item.
* *
* Paragraph. * Jupiter is the fifth planet from the Sun and the largest in the Solar
* System.
* *
* *List * *Saturn.
* item.
* *
* *List * Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
* item.
* *
* @example * @example
* {"name": "not-ok.md", "config": "one", "label": "input"} * {"config": "tab", "label": "input", "name": "not-ok.md"}
* *
* *List * *Mercury.
* item. * *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 * @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 * @example
* {"name": "not-ok.md", "config": "tab", "label": "input"} * {"config": "mixed", "gfm": true, "label": "input", "name": "gfm.md"}
* *
* *List * *[x] Mercury.
* item. *
* 1.[ ] Venus.
*
* 2.[ ] Earth.
* *
* @example * @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 * @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 * @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 * *Mercury.
* {"name": "not-ok.md", "config": "💩", "label": "output", "positionless": true}
* *
* 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. * Configuration.
*/ */
import plural from 'pluralize' import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart} from 'unist-util-position' import {pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit' import {visitParents} from 'unist-util-visit-parents'
const remarkLintListItemIndent = lintRule( const remarkLintListItemIndent = lintRule(
{ {
@ -231,70 +321,106 @@ const remarkLintListItemIndent = lintRule(
*/ */
function (tree, file, options) { function (tree, file, options) {
const value = String(file) const value = String(file)
const option = options || 'one' /** @type {Options} */
let expected
/* c8 ignore next 13 -- previous names. */ if (options === null || options === undefined) {
// @ts-expect-error: old name. expected = 'one'
if (option === 'space') { /* c8 ignore next 10 -- previous names. */
// @ts-expect-error: old name.
} else if (options === 'space') {
file.fail( 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. visitParents(tree, 'list', function (list, parents) {
if (option === 'tab-size') { let loose = list.spread
file.fail(
'Incorrect list-item indent style `' + option + "`: use `'tab'` instead"
)
}
if (option !== 'mixed' && option !== 'one' && option !== 'tab') { if (!loose) {
file.fail( for (const item of list.children) {
'Incorrect list-item indent style `' + if (item.spread) {
option + loose = true
"`: use either `'mixed'`, `'one'`, or `'tab'`" break
) }
} }
}
visit(tree, 'list', function (node) { for (const item of list.children) {
const spread = node.spread
let index = -1
while (++index < node.children.length) {
const item = node.children[index]
const head = item.children[0] const head = item.children[0]
const start = pointStart(item) const itemStart = pointStart(item)
const final = pointStart(head) const headStart = pointStart(head)
if ( if (
start && itemStart &&
final && headStart &&
typeof start.offset === 'number' && typeof itemStart.offset === 'number' &&
typeof final.offset === 'number' typeof headStart.offset === 'number'
) { ) {
const marker = value let slice = value.slice(itemStart.offset, headStart.offset)
.slice(start.offset, final.offset)
.replace(/\[[x ]?]\s*$/i, '')
const bulletSize = marker.replace(/\s+$/, '').length // GFM tasklist.
const checkboxIndex = slice.indexOf('[')
if (checkboxIndex !== -1) slice = slice.slice(0, checkboxIndex)
const style = const actualIndent = slice.length
option === 'tab' || (option === 'mixed' && spread)
? Math.ceil(bulletSize / 4) * 4
: bulletSize + 1
if (marker.length !== style) { // To do: actual hard tabs?
const diff = style - marker.length // Remove whitespace.
const abs = Math.abs(diff) 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( file.message(
'Incorrect list-item indent: ' + 'Unexpected `' +
(diff > 0 ? 'add' : 'remove') + actualSpaces +
' ' + '` ' +
abs + pluralize('space', actualSpaces) +
' ' + ' between list item marker and content' +
plural('space', abs), (expected === 'mixed'
final ? ' in ' + (loose ? 'loose' : 'tight') + ' list'
: '') +
', expected `' +
expectedSpaces +
'` ' +
pluralize('space', expectedSpaces) +
', ' +
(difference > 0 ? 'add' : 'remove') +
' `' +
differenceAbsolute +
'` ' +
pluralize('space', differenceAbsolute),
{ancestors: [...parents, list, item], place: headStart}
) )
} }
} }

View File

@ -36,7 +36,7 @@
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0" "unist-util-visit-parents": "^6.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {
@ -48,7 +48,9 @@
"xo": { "xo": {
"prettier": true, "prettier": true,
"rules": { "rules": {
"capitalized-comments": "off" "capitalized-comments": "off",
"unicorn/prefer-code-point": "off",
"unicorn/prefer-switch": "off"
} }
} }
} }

View File

@ -203,21 +203,20 @@ by default.
###### In ###### In
```markdown ```markdown
*␠List *␠Mercury.
␠␠item. *␠Venus.
Paragraph. 111.␠Earth
␠␠␠␠␠and Mars.
11.␠List *␠**Jupiter**.
␠␠␠␠item.
Paragraph. ␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar
␠␠System.
*␠List *␠Saturn.
␠␠item.
*␠List ␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
␠␠item.
``` ```
###### Out ###### Out
@ -231,113 +230,26 @@ When configured with `'mixed'`.
###### In ###### In
```markdown ```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 *␠␠␠Saturn.
␠␠␠␠item.
*␠␠␠List ␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
␠␠␠␠item.
``` ```
###### Out ###### Out
No messages. 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` ##### `not-ok.md`
When configured with `'mixed'`. When configured with `'mixed'`.
@ -345,25 +257,250 @@ When configured with `'mixed'`.
###### In ###### In
```markdown ```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 ###### Out
```text ```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` ##### `not-ok.md`
When configured with `'💩'`. When configured with `'🌍'`.
###### Out ###### Out
```text ```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 ## Compatibility
Projects maintained by the unified collective are compatible with maintained 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-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-lint]: https://github.com/remarkjs/remark-lint
[github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify [github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify

View File

@ -65,97 +65,77 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* A tight list: * * Mercury.
* * Venus.
* *
* - item 1 * + Mercury and
* - item 2 * Venus.
* - item 3
* *
* A loose list: * + Earth.
*
* - Wrapped
* item
*
* - item 2
*
* - item 3
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"config": {"checkBlanks": true}, "name": "ok-check-blanks.md"}
* *
* A tight list: * * Mercury.
* * Venus.
* *
* - Wrapped * + Mercury
* item
* - item 2
* - item 3
* *
* A loose list: * Mercury is the first planet from the Sun and the smallest in the Solar
* System.
* *
* - item 1 * + Earth.
*
* - item 2
*
* - item 3
* *
* @example * @example
* {"name": "not-ok.md", "label": "output"} * {"label": "input", "name": "not-ok.md"}
* *
* 4:9-5:1: Missing new line after list item * * Mercury.
* 5:11-6:1: Missing new line after list item *
* 10:11-12:1: Extraneous new line after list item * * Venus.
* 12:11-14:1: Extraneous new line after list item *
* + 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 * @example
* {"name": "ok.md", "config": {"checkBlanks": true}} * {"config": {"checkBlanks": true}, "label": "input", "name": "not-ok-blank.md"}
* *
* A tight list: * * Mercury.
* *
* - item 1 * * Venus.
* - item 1.A
* - item 2
* > Block quote
* *
* A loose list: * + Mercury and
* Venus.
* *
* - item 1 * + Earth.
* *
* - item 1.A * * Mercury.
*
* - item 2
*
* > Block quote
* *
* Mercury is the first planet from the Sun and the smallest in the Solar
* System.
* * Earth.
* @example * @example
* {"name": "not-ok.md", "config": {"checkBlanks": true}, "label": "input"} * {"config": {"checkBlanks": true}, "label": "output", "name": "not-ok-blank.md"}
* *
* A tight list: * 1:11-3:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line
* * 6:11-8:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line
* - item 1 * 13:12-14:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line
*
* - 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
*/ */
/** /**
@ -171,9 +151,11 @@
* preference (default: `false`). * preference (default: `false`).
*/ */
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position' 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<Options>} */ /** @type {Readonly<Options>} */
const emptyOptions = {} const emptyOptions = {}
@ -193,82 +175,83 @@ const remarkLintListItemSpacing = lintRule(
*/ */
function (tree, file, options) { function (tree, file, options) {
const settings = options || emptyOptions const settings = options || emptyOptions
// To do: change options. Maybe to `Style = 'markdown' | 'markdown-style-guide'`?
const checkBlanks = settings.checkBlanks || false const checkBlanks = settings.checkBlanks || false
const infer = checkBlanks ? blanksBetween : multiline
visit(tree, 'list', function (node) { visitParents(tree, 'list', function (list, parents) {
let index = -1 /** @type {VFileMessage | undefined} */
let anySpaced = false let spacedCause
while (++index < node.children.length) { for (const item of list.children) {
const spaced = infer(node.children[index]) /** @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) { 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 break
} }
} }
index = 0 // Skip first. const expected = spacedCause ? 1 : 0
/** @type {ListItem | undefined} */
let previous
while (++index < node.children.length) { for (const item of list.children) {
const previous = node.children[index - 1]
const current = node.children[index]
const previousEnd = pointEnd(previous) const previousEnd = pointEnd(previous)
const start = pointStart(current) const itemStart = pointStart(item)
if (previousEnd && start) { if (previousEnd && itemStart) {
const spaced = start.line - previousEnd.line > 1 const actual = itemStart.line - previousEnd.line - 1
if (actual !== expected) {
const difference = expected - actual
const differenceAbsolute = Math.abs(difference)
if (spaced !== anySpaced) {
file.message( file.message(
anySpaced 'Unexpected `' +
? 'Missing new line after list item' actual +
: 'Extraneous new line after list item', '` blank ' +
{start: previousEnd, end: start} 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 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)
}

View File

@ -34,9 +34,11 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -174,20 +174,35 @@ all items must be loose.
###### In ###### In
```markdown ```markdown
A tight list: * Mercury.
* Venus.
- item 1 + Mercury and
- item 2 Venus.
- item 3
A loose list: + Earth.
```
- Wrapped ###### Out
item
- 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 ###### Out
@ -199,92 +214,58 @@ No messages.
###### In ###### In
```markdown ```markdown
A tight list: * Mercury.
- Wrapped * Venus.
item
- item 2
- item 3
A loose list: + Mercury and
Venus.
+ Earth.
- item 1 * Mercury.
- item 2 Mercury is the first planet from the Sun and the smallest in the Solar
System.
- item 3 * Earth.
``` ```
###### Out ###### Out
```text ```text
4:9-5:1: Missing new line after list item 1:11-3:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line
5:11-6:1: Missing new line after list item 6:11-7:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line
10:11-12:1: Extraneous new line after list item 12:12-13:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line
12:11-14:1: Extraneous new line after list item
``` ```
##### `ok.md` ##### `not-ok-blank.md`
When configured with `{ checkBlanks: true }`. When configured with `{ checkBlanks: true }`.
###### In ###### In
```markdown ```markdown
A tight list: * Mercury.
- item 1 * Venus.
- item 1.A
- item 2
> Block quote
A loose list: + Mercury and
Venus.
- item 1 + Earth.
- item 1.A * Mercury.
- item 2 Mercury is the first planet from the Sun and the smallest in the Solar
System.
> Block quote * Earth.
```
###### 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
``` ```
###### Out ###### Out
```text ```text
5:15-6:1: Missing new line after list item 1:11-3:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line
8:18-9:1: Missing new line after list item 6:11-8:1: Unexpected `1` blank line between list items, expected `0` blank lines, remove `1` blank line
14:15-16:1: Extraneous new line after list item 13:12-14:1: Unexpected `0` blank lines between list items, expected `1` blank line, add `1` blank line
``` ```
## Compatibility ## Compatibility

View File

@ -43,28 +43,31 @@
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* # Alpha bravo charlie delta echo foxtrot golf hotel * # Mercury is the first planet from the Sun
*
* # ![Alpha bravo charlie delta echo foxtrot golf hotel](http://example.com/nato.png)
* *
* @example * @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 * @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 * @example
* {"config": 30, "label": "input", "mdx": true, "name": "ok.mdx"} * {"config": 30, "label": "input", "mdx": true, "name": "mdx.mdx"}
* *
* <h1>In MDX, headings are checked too</h1> * <h1>Mercury is the first planet from the Sun</h1>
* @example * @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 {toString} from 'mdast-util-to-string'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {position} from 'unist-util-position' 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 jsxNameRe = /^h([1-6])$/
@ -94,12 +97,22 @@ const remarkLintMaximumHeadingLength = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file, options) { 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, // Note: HTML headings cannot properly be checked,
// because for markdown, blocks are one single raw string. // because for markdown, blocks are one single raw string.
visit(tree, function (node) { visitParents(tree, function (node, parents) {
if ( if (
node.type === 'heading' || node.type === 'heading' ||
((node.type === 'mdxJsxFlowElement' || ((node.type === 'mdxJsxFlowElement' ||
@ -108,10 +121,17 @@ const remarkLintMaximumHeadingLength = lintRule(
jsxNameRe.test(node.name)) jsxNameRe.test(node.name))
) { ) {
const place = position(node) 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) { if (place && actual > expected) {
file.message('Use headings shorter than `' + option + '`', place) file.message(
'Unexpected `' +
actual +
'` characters in heading, expected at most `' +
expected +
'` characters',
{ancestors: [...parents, node], place}
)
} }
} }
}) })

View File

@ -36,7 +36,7 @@
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0" "unist-util-visit-parents": "^6.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -149,9 +149,7 @@ every heading out loud to navigate within a page).
###### In ###### In
```markdown ```markdown
# Alpha bravo charlie delta echo foxtrot golf hotel # Mercury is the first planet from the Sun
# ![Alpha bravo charlie delta echo foxtrot golf hotel](http://example.com/nato.png)
``` ```
###### Out ###### Out
@ -160,21 +158,21 @@ No messages.
##### `not-ok.md` ##### `not-ok.md`
When configured with `40`. When configured with `30`.
###### In ###### In
```markdown ```markdown
# Alpha bravo charlie delta echo foxtrot golf hotel # Mercury is the first planet from the Sun
``` ```
###### Out ###### Out
```text ```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`. When configured with `30`.
@ -184,13 +182,23 @@ When configured with `30`.
> MDX ([`remark-mdx`][github-remark-mdx]). > MDX ([`remark-mdx`][github-remark-mdx]).
```mdx ```mdx
<h1>In MDX, headings are checked too</h1> <h1>Mercury is the first planet from the Sun</h1>
``` ```
###### Out ###### Out
```text ```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 ## Compatibility

View File

@ -43,80 +43,112 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
* @example
* {"name": "ok.md", "positionless": true, "gfm": true}
*
* This line is simply not toooooooooooooooooooooooooooooooooooooooooooo
* long.
*
* This is also fine: <http://this-long-url-with-a-long-domain.co.uk/a-long-path?query=variables>
*
* <http://this-link-is-fine.com>
*
* `alphaBravoCharlieDeltaEchoFoxtrotGolfHotelIndiaJuliettKiloLimaMikeNovemberOscarPapaQuebec.romeo()`
*
* [foo](http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables)
*
* <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 | cant | just |
* | -- | --------- | -- | ---- | ------ | -- | ---- | ------ | ------- | ----- | ----- | ---- |
* | be | helped | | | | | | | | | | . |
*
* <a><b><i><p><q><s><u>alpha bravo charlie delta echo foxtrot golf</u></s></q></p></i></b></a>
*
* The following is also fine (note the `.`), because there is no whitespace.
*
* <http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables>.
*
* In addition, definitions are also fine:
*
* [foo]: <http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables>
* *
* @example * @example
* {"name": "not-ok.md", "config": 80, "label": "input", "positionless": true} * {"name": "ok.md", "positionless": true}
* *
* This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo * Mercury mercury mercury mercury mercury mercury mercury mercury mercury mercury
* long. * 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: <http://line.com> * Mercury mercury mercury mercury mercury mercury mercury mercury mercury <http://localhost>.
* *
* <http://this-long-url-with-a-long-domain-is-not-ok.co.uk/a-long-path?query=variables> 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. * <div>Mercury mercury mercury mercury mercury mercury mercury mercury mercury</div>
*
* [foo]: http://localhost/mercury/mercury/mercury/mercury/mercury/mercury/mercury/mercury
* *
* @example * @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 * Mercury mercury mercury
* 6:99: Line must be at most 80 characters * mercury.
* 8:96: Line must be at most 80 characters *
* 10:97: Line must be at most 80 characters * Mercury mercury mercury `mercury()`.
* 12:99: Line must be at most 80 characters *
* Mercury mercury mercury <http://localhost>.
*
* Mercury mercury mercury [m](example.com).
*
* Mercury mercury mercury ![m](example.com).
*
* `mercury()` mercury mercury mercury.
*
* <http://localhost> mercury.
*
* [m](example.com) mercury.
*
* ![m](example.com) mercury.
*
* Mercury mercury ![m](example.com) mercury.
* *
* @example * @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}
* *
* 012345678901234567890123401234 * 012345678901234567890123401234
* *
* @example * @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}
* *
* 0123456789010123456789010123456789001234567890 * 0123456789010123456789010123456789001234567890
* *
* @example * @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 * 1:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
* 2:13: Line must be at most 10 characters * 2:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
* 3:12: Line must be at most 10 characters * 3:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character
* 4:12: Line must be at most 10 characters * 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 @@
/// <reference types="mdast-util-mdx" /> /// <reference types="mdast-util-mdx" />
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position' import {pointEnd, pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit' import {SKIP, visit} from 'unist-util-visit'
const remarkLintMaximumLineLength = lintRule( const remarkLintMaximumLineLength = lintRule(
{ {
@ -145,16 +178,29 @@ const remarkLintMaximumLineLength = lintRule(
function (tree, file, options) { function (tree, file, options) {
const value = String(file) const value = String(file)
const lines = value.split(/\r?\n/) const lines = value.split(/\r?\n/)
const option = options || 80 let expected = 80
// Allow nodes that cannot be wrapped. if (options === null || options === undefined) {
visit(tree, function (node) { // 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 ( if (
node.type === 'code' || node.type === 'code' ||
node.type === 'definition' || node.type === 'definition' ||
node.type === 'heading' || node.type === 'heading' ||
node.type === 'html' || node.type === 'html' ||
node.type === 'mdxJsxTextElement' || node.type === 'math' ||
node.type === 'mdxjsEsm' ||
node.type === 'mdxFlowExpression' ||
node.type === 'mdxTextExpression' || node.type === 'mdxTextExpression' ||
node.type === 'table' || node.type === 'table' ||
// @ts-expect-error: TOML from frontmatter. // @ts-expect-error: TOML from frontmatter.
@ -165,44 +211,52 @@ const remarkLintMaximumLineLength = lintRule(
const start = pointStart(node) const start = pointStart(node)
if (end && start) { 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 ( if (
(node.type === 'image' || node.type === 'image' ||
node.type === 'inlineCode' || node.type === 'inlineCode' ||
node.type === 'link') && node.type === 'link'
initial &&
final &&
parent &&
typeof index === 'number'
) { ) {
// Not allowing when starting after the border, or ending before it. const end = pointEnd(node)
if (initial.column > option || final.column < option) { const start = pointStart(node)
return
if (end && start && parent && typeof index === 'number') {
// Not allowing when starting after the border.
if (start.column > expected) return
// Not allowing when ending before it.
if (end.column < expected) return
const next = parent.children[index + 1]
const nextStart = pointStart(next)
// Not allowing when theres 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 theres 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 let index = -1
while (++index < lines.length) { 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) { if (difference > 0) {
file.message('Line must be at most ' + option + ' characters', { file.message(
line: index + 1, 'Unexpected `' +
column: lineLength + 1 actualCharacters +
}) '` character line, expected at most `' +
} expected +
} '` characters, remove `' +
difference +
/** '` ' +
* Allowlist from `initial` to `final`, zero-based. pluralize('character', difference),
* {
* @param {number} initial line: index + 1,
* Initial line. column: actualBytes + 1
* @param {number} final }
* Final line. )
* @returns {undefined}
* Nothing.
*/
function allowList(initial, final) {
while (initial < final) {
lines[initial++] = ''
} }
} }
} }

View File

@ -32,6 +32,7 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"pluralize": "^8.0.0",
"mdast-util-mdx": "^3.0.0", "mdast-util-mdx": "^3.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-position": "^5.0.0",

View File

@ -151,38 +151,21 @@ Whether to wrap prose or not is a stylistic choice.
###### In ###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
This line is simply not toooooooooooooooooooooooooooooooooooooooooooo Mercury mercury mercury mercury mercury mercury mercury mercury mercury mercury
long. mercury.
This is also fine: <http://this-long-url-with-a-long-domain.co.uk/a-long-path?query=variables> Mercury mercury mercury mercury mercury mercury mercury mercury mercury `mercury()`.
<http://this-link-is-fine.com> Mercury mercury mercury mercury mercury mercury mercury mercury mercury <http://localhost>.
`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).
<http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables> <div>Mercury mercury mercury mercury mercury mercury mercury mercury mercury</div>
![foo](http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables) [foo]: http://localhost/mercury/mercury/mercury/mercury/mercury/mercury/mercury/mercury
| An | exception | is | line | length | in | long | tables | because | those | cant | just |
| -- | --------- | -- | ---- | ------ | -- | ---- | ------ | ------- | ----- | ----- | ---- |
| be | helped | | | | | | | | | | . |
<a><b><i><p><q><s><u>alpha bravo charlie delta echo foxtrot golf</u></s></q></p></i></b></a>
The following is also fine (note the `.`), because there is no whitespace.
<http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables>.
In addition, definitions are also fine:
[foo]: <http://this-long-url-with-a-long-domain-is-ok.co.uk/a-long-path?query=variables>
``` ```
###### Out ###### Out
@ -191,35 +174,123 @@ No messages.
##### `not-ok.md` ##### `not-ok.md`
When configured with `80`. When configured with `20`.
###### In ###### In
```markdown ```markdown
This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo Mercury mercury mercury
long. mercury.
Just like thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis one. Mercury mercury mercury `mercury()`.
And this one is also very wrong: because the link starts aaaaaaafter the column: <http://line.com> Mercury mercury mercury <http://localhost>.
<http://this-long-url-with-a-long-domain-is-not-ok.co.uk/a-long-path?query=variables> 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.
<http://localhost> mercury.
[m](example.com) mercury.
![m](example.com) mercury.
Mercury mercury ![m](example.com) mercury.
``` ```
###### Out ###### Out
```text ```text
4:86: Line must be at most 80 characters 1:24: Unexpected `23` character line, expected at most `20` characters, remove `3` characters
6:99: Line must be at most 80 characters 4:37: Unexpected `36` character line, expected at most `20` characters, remove `16` characters
8:96: Line must be at most 80 characters 6:44: Unexpected `43` character line, expected at most `20` characters, remove `23` characters
10:97: Line must be at most 80 characters 8:42: Unexpected `41` character line, expected at most `20` characters, remove `21` characters
12:99: Line must be at most 80 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` ##### `ok-mixed-line-endings.md`
When configured with `10`. When configured with `10`.
@ -247,10 +318,20 @@ When configured with `10`.
###### Out ###### Out
```text ```text
1:13: Line must be at most 10 characters 1:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
2:13: Line must be at most 10 characters 2:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
3:12: Line must be at most 10 characters 3:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character
4:12: Line must be at most 10 characters 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 ## Compatibility
@ -322,10 +403,16 @@ abide by its terms.
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c [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-gfm]: https://github.com/remarkjs/remark-gfm
[github-remark-lint]: https://github.com/remarkjs/remark-lint [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 [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer
[npm-install]: https://docs.npmjs.com/cli/install [npm-install]: https://docs.npmjs.com/cli/install

View File

@ -45,50 +45,82 @@
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* > Foo * > Mercury,
* > bar * > Venus,
* > baz. * > and Earth.
*
* Mars.
* *
* @example * @example
* {"name": "ok-tabs.md"} * {"name": "ok-tabs.md"}
* *
* >Foo * >Mercury,
* >bar * >Venus,
* >baz. * >and Earth.
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "not-ok.md"}
* *
* > Foo * > Mercury,
* bar * Venus,
* > baz. * > 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 * @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 * @example
* {"name": "not-ok-tabs.md", "label": "input"} * {"label": "input", "name": "containers.md"}
* *
* >Foo * * > Mercury and
* bar * Venus.
* baz.
* *
* > * Mercury and
* Venus.
*
* * > * Mercury and
* Venus.
*
* > * > Mercury and
* Venus.
*
* ***
*
* > * > Mercury and
* > Venus.
* @example * @example
* {"name": "not-ok-tabs.md", "label": "output"} * {"label": "output", "name": "containers.md"}
* *
* 2:1: Missing marker in block quote * 2:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker
* 3:1: Missing marker in block quote * 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 * @typedef {import('mdast').Root} Root
*/ */
/// <reference types="mdast-util-directive" />
import {ok as assert} from 'devlop'
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position' 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' import {location} from 'vfile-location'
const remarkLintNoBlockquoteWithoutMarker = lintRule( const remarkLintNoBlockquoteWithoutMarker = lintRule(
@ -106,35 +138,81 @@ const remarkLintNoBlockquoteWithoutMarker = lintRule(
const value = String(file) const value = String(file)
const loc = location(file) const loc = location(file)
visit(tree, 'blockquote', function (node) { // Only paragraphs can be lazy.
let index = -1 visitParents(tree, 'paragraph', function (node, parents) {
let expected = 0
while (++index < node.children.length) { for (const parent of parents) {
const child = node.children[index] if (parent.type === 'blockquote') {
const start = pointStart(child) expected++
const end = pointEnd(child) }
// 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) { if (!expected) return SKIP
const column = start.column
let line = start.line
// Skip past the first line. const end = pointEnd(node)
while (++line <= end.line) { const start = pointStart(node)
const offset = loc.toOffset({line, column})
if ( if (!end || !start) return SKIP
typeof offset !== 'number' ||
/>[\t ]+$/.test(value.slice(offset - 5, offset))
) {
continue
}
// Roughly here. let line = start.line
file.message('Missing marker in block quote', {
line, while (++line <= end.line) {
column: column - 2 // 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}
)
} }
} }
}) })

View File

@ -33,9 +33,12 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@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", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.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" "vfile-location": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
@ -48,7 +51,8 @@
"xo": { "xo": {
"prettier": true, "prettier": true,
"rules": { "rules": {
"capitalized-comments": "off" "capitalized-comments": "off",
"unicorn/prefer-code-point": "off"
} }
} }
} }

View File

@ -152,9 +152,11 @@ in a block quote.
###### In ###### In
```markdown ```markdown
> Foo… > Mercury,
> …bar… > Venus,
> …baz. > and Earth.
Mars.
``` ```
###### Out ###### Out
@ -166,9 +168,9 @@ No messages.
###### In ###### In
```markdown ```markdown
>␉Foo… >␉Mercury,
>␉…bar… >␉Venus,
>␉…baz. >␉and Earth.
``` ```
###### Out ###### Out
@ -180,15 +182,15 @@ No messages.
###### In ###### In
```markdown ```markdown
> Foo… > Mercury,
…bar… Venus,
> …baz. > and Earth.
``` ```
###### Out ###### Out
```text ```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` ##### `not-ok-tabs.md`
@ -196,16 +198,49 @@ No messages.
###### In ###### In
```markdown ```markdown
>␉Foo… >␉Mercury,
…bar… Venus,
…baz. and Earth.
``` ```
###### Out ###### Out
```text ```text
2:1: Missing marker in block quote 2:2: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker
3:1: Missing marker in block quote 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 ## Compatibility

View File

@ -41,37 +41,207 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* FooBar. * # 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 * @example
* {"name": "empty-document.md"} * {"name": "empty-document.md"}
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "block-quote.md"}
* *
* FooBar * > 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 * @example
* {"name": "not-ok.md", "label": "output"} * {"directive": true, "label": "input", "name": "directive.md"}
* *
* 4:1: Remove 1 line before node * :::mercury
* 4:5: Remove 2 lines after node * 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"}
*
* <Mercury>
* Venus.
*
*
* Earth.
* </Mercury>
* @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('mdast').Root} Root
* @typedef {import('unist').Point} Point
*/ */
import plural from 'pluralize' /// <reference types="mdast-util-directive" />
/// <reference types="mdast-util-mdx" />
import {phrasing} from 'mdast-util-phrasing'
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position' import {pointEnd, pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit' import {SKIP, visitParents} from 'unist-util-visit-parents'
const unknownContainerSize = new Set(['mdxJsxFlowElement', 'mdxJsxTextElement'])
const remarkLintNoConsecutiveBlankLines = lintRule( const remarkLintNoConsecutiveBlankLines = lintRule(
{ {
@ -85,79 +255,108 @@ const remarkLintNoConsecutiveBlankLines = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
visit(tree, function (node) { visitParents(tree, function (node, parents) {
if ('children' in node) { const parent = parents.at(-1)
// Ignore phrasing nodes and non-parents.
if (!parent) return
if (phrasing(node)) return SKIP
const siblings = /** @type {Array<Nodes>} */ (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 start = pointStart(node)
const head = node.children[0]
const headStart = pointStart(head)
if (head && headStart && start) { if (parentStart && start) {
if (!unknownContainerSize.has(node.type)) { // For footnote definitions, the first line with the label can
// Compare parent and first child. // otherwise be empty.
compare(start, headStart, 0) const difference =
} start.line -
parentStart.line -
(parent.type === 'footnoteDefinition' ? 1 : 0)
// Compare between each child. if (difference > 0) {
let index = -1 file.message(
'Unexpected `' +
while (++index < node.children.length) { difference +
const previous = node.children[index - 1] '` blank ' +
const child = node.children[index] pluralize('line', difference) +
const previousEnd = pointEnd(previous) ' before node, expected `0` blank lines, remove `' +
const childStart = pointStart(child) difference +
'` blank ' +
if (previous && previousEnd && childStart) { pluralize('line', difference),
compare(previousEnd, childStart, 2) {ancestors: [...parents, node], place: start}
} )
}
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)
} }
} }
} }
})
/** const next = siblings[index + 1]
* Compare the difference between `start` and `end`, and warn when that const end = pointEnd(node)
* difference exceeds `max`. const nextStart = pointStart(next)
*
* @param {Point} start
* Start.
* @param {Point} end
* End.
* @param {0 | 1 | 2} max
* Max.
* @returns {undefined}
* Nothing.
*/
function compare(start, end, max) {
const diff = end.line - start.line
const lines = Math.abs(diff) - max
if (lines > 0) { // Compare child and next sibling.
file.message( if (end && nextStart) {
'Remove ' + // `2` for line ending after node and optional line ending of blank
lines + // line.
' ' + const difference = nextStart.line - end.line - 2
plural('line', Math.abs(lines)) +
' ' + if (difference > 0) {
(diff > 0 ? 'before' : 'after') + const actual = difference + 1
' node',
end 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}
)
}
}
})
} }
) )

View File

@ -32,11 +32,13 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@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", "pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0" "unist-util-visit-parents": "^6.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {
@ -49,7 +51,8 @@
"prettier": true, "prettier": true,
"rules": { "rules": {
"capitalized-comments": "off", "capitalized-comments": "off",
"unicorn/prefer-at": "off" "unicorn/prefer-at": "off",
"unicorn/prefer-set-has": "off"
} }
} }
} }

View File

@ -150,32 +150,266 @@ It has a `join` option to configure more complex cases.
###### In ###### In
```markdown ```markdown
Foo…␊␊…Bar. # Planets
Mercury.
Venus.
``` ```
###### Out ###### Out
No messages. No messages.
##### `empty-document.md`
###### Out
No messages.
##### `not-ok.md` ##### `not-ok.md`
###### In ###### In
```markdown ```markdown
Foo…␊␊␊…Bar␊␊␊ # Planets
Mercury.
Venus.
``` ```
###### Out ###### Out
```text ```text
4:1: Remove 1 line before node 4:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line
4:5: Remove 2 lines after node 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
<Mercury>
Venus.
Earth.
</Mercury>
```
###### 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 ## Compatibility
@ -247,8 +481,14 @@ abide by its terms.
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c [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-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-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify
[github-unified-transformer]: https://github.com/unifiedjs/unified#transformer [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer

View File

@ -35,32 +35,33 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2020 Titus Wormer * @copyright 2020 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* [alpha]: alpha.com * [mercury]: https://example.com/mercury/
* [bravo]: bravo.com * [venus]: https://example.com/venus/
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "not-ok.md"}
*
* [alpha]: alpha.com
* [bravo]: alpha.com
* *
* [mercury]: https://example.com/mercury/
* [venus]: https://example.com/mercury/
* @example * @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 * @typedef {import('mdast').Root} Root
*/ */
import {ok as assert} from 'devlop'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart, position} from 'unist-util-position' import {visitParents} from 'unist-util-visit-parents'
import {stringifyPosition} from 'unist-util-stringify-position' import {VFileMessage} from 'vfile-message'
import {visit} from 'unist-util-visit'
const remarkLintNoDuplicateDefinedUrls = lintRule( const remarkLintNoDuplicateDefinedUrls = lintRule(
{ {
@ -74,27 +75,39 @@ const remarkLintNoDuplicateDefinedUrls = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
/** @type {Map<string, string>} */ /** @type {Map<string, Array<Nodes>>} */
const map = new Map() const map = new Map()
visit(tree, 'definition', function (node) { visitParents(tree, 'definition', function (node, parents) {
const place = position(node) const ancestors = [...parents, node]
const start = pointStart(node)
if (place && start && node.url) { if (node.position && node.url) {
const url = String(node.url).toUpperCase() const urlNormal = String(node.url).toUpperCase()
const duplicate = map.get(url) 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( file.message(
'Do not use different definitions with the same URL (' + 'Unexpected definition with an already defined URL (as `' +
duplicate + duplicate.identifier +
')', '`), expected unique URLs',
place {
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)
} }
}) })
} }

View File

@ -33,10 +33,10 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0",
"unist-util-stringify-position": "^4.0.0", "vfile-message": "4.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -140,8 +140,8 @@ identifiers.
###### In ###### In
```markdown ```markdown
[alpha]: alpha.com [mercury]: https://example.com/mercury/
[bravo]: bravo.com [venus]: https://example.com/venus/
``` ```
###### Out ###### Out
@ -153,14 +153,14 @@ No messages.
###### In ###### In
```markdown ```markdown
[alpha]: alpha.com [mercury]: https://example.com/mercury/
[bravo]: alpha.com [venus]: https://example.com/mercury/
``` ```
###### Out ###### Out
```text ```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 ## Compatibility

View File

@ -34,45 +34,50 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* [foo]: bar * [mercury]: https://example.com/mercury/
* [baz]: qux * [venus]: https://example.com/venus/
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "not-ok.md"}
*
* [foo]: bar
* [foo]: qux
* *
* [mercury]: https://example.com/mercury/
* [mercury]: https://example.com/venus/
* @example * @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 * @example
* {"gfm": true, "label": "input", "name": "gfm.md"} * {"gfm": true, "label": "input", "name": "gfm.md"}
* *
* GFM footnote definitions are checked too[^a]. * Mercury[^mercury].
* *
* [^a]: alpha * [^mercury]:
* [^a]: bravo * 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 * @example
* {"gfm": true, "label": "output", "name": "gfm.md"} * {"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 * @typedef {import('mdast').Root} Root
*/ */
import {ok as assert} from 'devlop'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart, position} from 'unist-util-position' import {visitParents} from 'unist-util-visit-parents'
import {stringifyPosition} from 'unist-util-stringify-position' import {VFileMessage} from 'vfile-message'
import {visit} from 'unist-util-visit'
/** @type {ReadonlyArray<never>} */ /** @type {ReadonlyArray<never>} */
const empty = [] const empty = []
@ -89,14 +94,12 @@ const remarkLintNoDuplicateDefinitions = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
/** @type {Map<string, string>} */ /** @type {Map<string, Array<Nodes>>} */
const definitions = new Map() const definitions = new Map()
/** @type {Map<string, string>} */ /** @type {Map<string, Array<Nodes>>} */
const footnoteDefinitions = new Map() const footnoteDefinitions = new Map()
visit(tree, function (node) { visitParents(tree, function (node, parents) {
const place = position(node)
const start = pointStart(node)
const [map, identifier] = const [map, identifier] =
node.type === 'definition' node.type === 'definition'
? [definitions, node.identifier] ? [definitions, node.identifier]
@ -104,21 +107,34 @@ const remarkLintNoDuplicateDefinitions = lintRule(
? [footnoteDefinitions, node.identifier] ? [footnoteDefinitions, node.identifier]
: empty : empty
if (map && identifier && place && start) { if (map && identifier && node.position) {
const duplicate = map.get(identifier) const ancestors = [...parents, node]
const duplicateAncestors = map.get(identifier)
if (duplicateAncestors) {
const duplicate = duplicateAncestors.at(-1)
assert(duplicate) // Always defined.
if (duplicate) {
file.message( file.message(
'Do not use' + 'Unexpected ' +
(node.type === 'footnoteDefinition' ? ' footnote' : '') + (node.type === 'footnoteDefinition' ? 'footnote ' : '') +
' definitions with the same identifier (' + 'definition with an already defined identifier (`' +
duplicate + identifier +
')', '`), expected unique identifiers',
place {
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)
} }
}) })
} }

View File

@ -32,10 +32,10 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0",
"unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -143,8 +143,8 @@ Its a mistake when the same identifier is defined multiple times.
###### In ###### In
```markdown ```markdown
[foo]: bar [mercury]: https://example.com/mercury/
[baz]: qux [venus]: https://example.com/venus/
``` ```
###### Out ###### Out
@ -156,14 +156,14 @@ No messages.
###### In ###### In
```markdown ```markdown
[foo]: bar [mercury]: https://example.com/mercury/
[foo]: qux [mercury]: https://example.com/venus/
``` ```
###### Out ###### Out
```text ```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` ##### `gfm.md`
@ -174,16 +174,20 @@ No messages.
> GFM ([`remark-gfm`][github-remark-gfm]). > GFM ([`remark-gfm`][github-remark-gfm]).
```markdown ```markdown
GFM footnote definitions are checked too[^a]. Mercury[^mercury].
[^a]: alpha [^mercury]:
[^a]: bravo 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 ###### Out
```text ```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 ## Compatibility

View File

@ -39,80 +39,83 @@
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* ## Alpha * # Planets
* *
* ### Bravo * ## Venus
* *
* ## Charlie * ### Discovery
* *
* ### Bravo * ## Mars
* *
* ### Delta * ### Discovery
* *
* #### Bravo * ### Phobos
* *
* #### Echo * #### Discovery
*
* ##### Bravo
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "not-ok.md"}
* *
* ## Foxtrot * # Planets
* *
* ### Golf * ## Mars
* *
* ### Golf * ### Discovery
*
* ### Discovery
* *
* @example * @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 * @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 * @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 * @example
* {"label": "input", "mdx": true, "name": "mdx.mdx"} * {"label": "input", "mdx": true, "name": "mdx.mdx"}
* *
* MDX is supported <em>too</em>. * MDX is supported <em>too</em>.
* *
* <h2>Alpha</h2> * <h1>Planets</h1>
* <h2>Alpha</h2> * <h2>Mars</h2>
* <h3>Discovery</h3>
* <h3>Discovery</h3>
* *
* @example * @example
* {"label": "output", "mdx": true, "name": "mdx.mdx"} * {"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').Heading} Heading
* @typedef {import('mdast').Nodes} Nodes
* @typedef {import('mdast').Root} Root * @typedef {import('mdast').Root} Root
*/ */
/// <reference types="mdast-util-mdx" /> /// <reference types="mdast-util-mdx" />
import {ok as assert} from 'devlop'
import {toString} from 'mdast-util-to-string' import {toString} from 'mdast-util-to-string'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart, position} from 'unist-util-position' import {visitParents} from 'unist-util-visit-parents'
import {stringifyPosition} from 'unist-util-stringify-position' import {VFileMessage} from 'vfile-message'
import {visit} from 'unist-util-visit'
const jsxNameRe = /^h([1-6])$/ const jsxNameRe = /^h([1-6])$/
@ -128,10 +131,10 @@ const remarkLintNoDuplicateHeadingsInSection = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
/** @type {Array<Map<string, string>>} */ /** @type {Array<Map<string, Array<Nodes>>>} */
const stack = [] const stack = []
visit(tree, function (node) { visitParents(tree, function (node, parents) {
/** @type {Heading['depth'] | undefined} */ /** @type {Heading['depth'] | undefined} */
let rank let rank
@ -149,23 +152,32 @@ const remarkLintNoDuplicateHeadingsInSection = lintRule(
} }
if (rank) { if (rank) {
const ancestors = [...parents, node]
const value = toString(node).toLowerCase() const value = toString(node).toLowerCase()
const index = rank - 1 const index = rank - 1
const scope = stack[index] || (stack[index] = new Map()) const map = stack[index] || (stack[index] = new Map())
const duplicate = scope.get(value) const duplicateAncestors = map.get(value)
const place = position(node)
const start = pointStart(node) if (node.position && duplicateAncestors) {
const duplicate = duplicateAncestors.at(-1)
assert(duplicate) // Always defined.
if (place && duplicate) {
file.message( file.message(
'Do not use headings with similar content per section (' + 'Unexpected heading with equivalent text in section, expected unique headings',
duplicate + {
')', ancestors,
place 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. // Drop things after it.
stack.length = rank stack.length = rank
} }

View File

@ -33,12 +33,12 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"mdast-util-mdx": "^3.0.0", "mdast-util-mdx": "^3.0.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0",
"unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -141,21 +141,19 @@ section.
###### In ###### In
```markdown ```markdown
## Alpha # Planets
### Bravo ## Venus
## Charlie ### Discovery
### Bravo ## Mars
### Delta ### Discovery
#### Bravo ### Phobos
#### Echo #### Discovery
##### Bravo
``` ```
###### Out ###### Out
@ -167,39 +165,41 @@ No messages.
###### In ###### In
```markdown ```markdown
## Foxtrot # Planets
### Golf ## Mars
### Golf ### Discovery
### Discovery
``` ```
###### Out ###### Out
```text ```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 ###### In
```markdown ```markdown
# Alpha # Planets
#### Bravo #### Discovery
###### Charlie ###### Phobos
#### Bravo #### Discovery
###### Delta ###### Deimos
``` ```
###### Out ###### Out
```text ```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` ##### `mdx.mdx`
@ -212,14 +212,16 @@ No messages.
```mdx ```mdx
MDX is supported <em>too</em>. MDX is supported <em>too</em>.
<h2>Alpha</h2> <h1>Planets</h1>
<h2>Alpha</h2> <h2>Mars</h2>
<h3>Discovery</h3>
<h3>Discovery</h3>
``` ```
###### Out ###### Out
```text ```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 ## Compatibility

View File

@ -42,53 +42,51 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* # Foo * # Mercury
* *
* ## Bar * ## Venus
* *
* @example * @example
* {"label": "input", "name": "not-ok.md"} * {"label": "input", "name": "not-ok.md"}
* *
* # Foo * # Mercury
* *
* ## Foo * ## Mercury
*
* ## [Foo](http://foo.com/bar)
* *
* ## [Mercury](http://example.com/mercury/)
* @example * @example
* {"label": "output", "name": "not-ok.md"} * {"label": "output", "name": "not-ok.md"}
* *
* 3:1-3:7: Do not use headings with similar content (1:1) * 3:1-3:11: Unexpected heading with equivalent text, expected unique headings
* 5:1-5:29: Do not use headings with similar content (3:1) * 5:1-5:42: Unexpected heading with equivalent text, expected unique headings
* *
* @example * @example
* {"label": "input", "mdx": true, "name": "mdx.mdx"} * {"label": "input", "mdx": true, "name": "mdx.mdx"}
* *
* MDX is supported too. * <h1>Mercury</h1>
* * <h2>Mercury</h2>
* <h1>Alpha</h1>
* <h2>Alpha</h2>
*
* @example * @example
* {"label": "output", "mdx": true, "name": "mdx.mdx"} * {"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 * @typedef {import('mdast').Root} Root
*/ */
/// <reference types="mdast-util-mdx" /> /// <reference types="mdast-util-mdx" />
import {ok as assert} from 'devlop'
import {toString} from 'mdast-util-to-string' import {toString} from 'mdast-util-to-string'
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {pointStart, position} from 'unist-util-position' import {visitParents} from 'unist-util-visit-parents'
import {stringifyPosition} from 'unist-util-stringify-position' import {VFileMessage} from 'vfile-message'
import {visit} from 'unist-util-visit'
const jsxNameRe = /^h([1-6])$/ const jsxNameRe = /^h([1-6])$/
@ -104,10 +102,10 @@ const remarkLintNoDuplicateHeadings = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
/** @type {Map<string, string>} */ /** @type {Map<string, Array<Nodes>>} */
const map = new Map() const map = new Map()
visit(tree, function (node) { visitParents(tree, function (node, parents) {
if ( if (
node.type === 'heading' || node.type === 'heading' ||
((node.type === 'mdxJsxFlowElement' || ((node.type === 'mdxJsxFlowElement' ||
@ -115,22 +113,30 @@ const remarkLintNoDuplicateHeadings = lintRule(
node.name && node.name &&
jsxNameRe.test(node.name)) jsxNameRe.test(node.name))
) { ) {
const place = position(node) const ancestors = [...parents, node]
const start = pointStart(node) const value = toString(node).toLowerCase()
const duplicateAncestors = map.get(value)
if (place && start) { if (node.position && duplicateAncestors) {
const value = toString(node).toLowerCase() const duplicate = duplicateAncestors.at(-1)
const duplicate = map.get(value) assert(duplicate) // Always defined.
if (duplicate) { file.message(
file.message( 'Unexpected heading with equivalent text, expected unique headings',
'Do not use headings with similar content (' + duplicate + ')', {
node ancestors,
) cause: new VFileMessage('Equivalent heading text defined here', {
} ancestors: duplicateAncestors,
place: duplicate.position,
map.set(value, stringifyPosition(start)) source: 'remark-lint',
ruleId: 'no-duplicate-headings'
}),
place: node.position
}
)
} }
map.set(value, ancestors)
} }
}) })
} }

View File

@ -32,12 +32,12 @@
], ],
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"mdast-util-mdx": "^3.0.0", "mdast-util-mdx": "^3.0.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0",
"unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -151,9 +151,9 @@ which makes linking to them prone to changes.
###### In ###### In
```markdown ```markdown
# Foo # Mercury
## Bar ## Venus
``` ```
###### Out ###### Out
@ -165,18 +165,18 @@ No messages.
###### In ###### In
```markdown ```markdown
# Foo # Mercury
## Foo ## Mercury
## [Foo](http://foo.com/bar) ## [Mercury](http://example.com/mercury/)
``` ```
###### Out ###### Out
```text ```text
3:1-3:7: Do not use headings with similar content (1:1) 3:1-3:11: Unexpected heading with equivalent text, expected unique headings
5:1-5:29: Do not use headings with similar content (3:1) 5:1-5:42: Unexpected heading with equivalent text, expected unique headings
``` ```
##### `mdx.mdx` ##### `mdx.mdx`
@ -187,16 +187,14 @@ No messages.
> MDX ([`remark-mdx`][github-remark-mdx]). > MDX ([`remark-mdx`][github-remark-mdx]).
```mdx ```mdx
MDX is supported too. <h1>Mercury</h1>
<h2>Mercury</h2>
<h1>Alpha</h1>
<h2>Alpha</h2>
``` ```
###### Out ###### Out
```text ```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 ## Compatibility

View File

@ -38,38 +38,40 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"name": "ok.md"}
* *
* # Foo * # Mercury
* *
* Bar. * **Mercury** is the first planet from the Sun and the smallest in the Solar
* System.
* *
* @example * @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__ * *Venus*
*
* Quux.
* *
* **Venus** is the second planet from the Sun.
* @example * @example
* {"name": "not-ok.md", "label": "output"} * {"label": "output", "name": "not-ok.md"}
* *
* 1:1-1:6: Dont use emphasis to introduce a section, use a heading * 1:1-1:12: Unexpected strong introducing a section, expected a heading instead
* 5:1-5:8: Dont use emphasis to introduce a section, use a heading * 6:1-6:8: Unexpected emphasis introducing a section, expected a heading instead
*/ */
/** /**
* @typedef {import('mdast').Root} Root * @typedef {import('mdast').Root} Root
* @typedef {import('mdast').RootContent} RootContent
*/ */
import {lintRule} from 'unified-lint-rule' import {lintRule} from 'unified-lint-rule'
import {visit} from 'unist-util-visit' import {visitParents} from 'unist-util-visit-parents'
import {position} from 'unist-util-position'
const remarkLintNoEmphasisAsHeading = lintRule( const remarkLintNoEmphasisAsHeading = lintRule(
{ {
@ -83,31 +85,37 @@ const remarkLintNoEmphasisAsHeading = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { 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<RootContent>} */ (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 head = node.children[0]
const place = position(node)
if ( if (
place && node.children.length !== 1 ||
parent && (head.type !== 'emphasis' && head.type !== 'strong')
typeof index === 'number' &&
node.children.length === 1 &&
(head.type === 'emphasis' || head.type === 'strong')
) { ) {
const previous = parent.children[index - 1] return
const next = parent.children[index + 1]
if (
(!previous || previous.type !== 'heading') &&
next &&
next.type === 'paragraph'
) {
file.message(
'Dont use emphasis to introduce a section, use a heading',
place
)
}
} }
file.message(
'Unexpected ' +
head.type +
' introducing a section, expected a heading instead',
{ancestors: [...parents, node, head], place: node.position}
)
}) })
} }
) )

View File

@ -33,8 +33,7 @@
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -147,9 +147,10 @@ Its recommended to use actual headings instead.
###### In ###### In
```markdown ```markdown
# Foo # Mercury
Bar. **Mercury** is the first planet from the Sun and the smallest in the Solar
System.
``` ```
###### Out ###### Out
@ -161,20 +162,21 @@ No messages.
###### In ###### In
```markdown ```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 ###### Out
```text ```text
1:1-1:6: Dont use emphasis to introduce a section, use a heading 1:1-1:12: Unexpected strong introducing a section, expected a heading instead
5:1-5:8: Dont use emphasis to introduce a section, use a heading 6:1-6:8: Unexpected emphasis introducing a section, expected a heading instead
``` ```
## Compatibility ## Compatibility

View File

@ -38,32 +38,31 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "ok.md"} * {"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]. * [earth]: http://example.com/earth/
*
* [india]: http://juliett.com
* *
* @example * @example
* {"name": "not-ok.md", "label": "input"} * {"label": "input", "name": "not-ok.md"}
* *
* [alpha](). * [Mercury]().
* *
* ![bravo](#). * ![Venus](#).
* *
* [charlie]: <> * [earth]: <>
* *
* @example * @example
* {"name": "not-ok.md", "label": "output"} * {"label": "output", "name": "not-ok.md"}
* *
* 1:1-1:10: Dont use links without URL * 1:1-1:12: Unexpected empty link URL referencing the current document, expected URL
* 3:1-3:12: Dont use images without URL * 3:1-3:12: Unexpected empty image URL referencing the current document, expected URL
* 5:1-5:14: Dont use definitions without 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 {lintRule} from 'unified-lint-rule'
import {position} from 'unist-util-position' import {visitParents} from 'unist-util-visit-parents'
import {visit} from 'unist-util-visit'
const remarkLintNoEmptyUrl = lintRule( const remarkLintNoEmptyUrl = lintRule(
{ {
@ -86,17 +84,20 @@ const remarkLintNoEmptyUrl = lintRule(
* Nothing. * Nothing.
*/ */
function (tree, file) { function (tree, file) {
visit(tree, function (node) { visitParents(tree, function (node, parents) {
const place = position(node)
if ( if (
(node.type === 'definition' || (node.type === 'definition' ||
node.type === 'image' || node.type === 'image' ||
node.type === 'link') && node.type === 'link') &&
place && node.position &&
(!node.url || node.url === '#' || node.url === '?') (!node.url || node.url === '#' || node.url === '?')
) { ) {
file.message('Dont 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}
)
} }
}) })
} }

View File

@ -35,8 +35,7 @@
"dependencies": { "dependencies": {
"@types/mdast": "^4.0.0", "@types/mdast": "^4.0.0",
"unified-lint-rule": "^2.0.0", "unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0", "unist-util-visit-parents": "^6.0.0"
"unist-util-visit": "^5.0.0"
}, },
"scripts": {}, "scripts": {},
"typeCoverage": { "typeCoverage": {

View File

@ -143,13 +143,11 @@ Its recommended to fill them out.
###### In ###### In
```markdown ```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]. [earth]: http://example.com/earth/
[india]: http://juliett.com
``` ```
###### Out ###### Out
@ -161,19 +159,19 @@ No messages.
###### In ###### In
```markdown ```markdown
[alpha](). [Mercury]().
![bravo](#). ![Venus](#).
[charlie]: <> [earth]: <>
``` ```
###### Out ###### Out
```text ```text
1:1-1:10: Dont use links without URL 1:1-1:12: Unexpected empty link URL referencing the current document, expected URL
3:1-3:12: Dont use images without URL 3:1-3:12: Unexpected empty image URL referencing the current document, expected URL
5:1-5:14: Dont use definitions without URL 5:1-5:12: Unexpected empty definition URL referencing the current document, expected URL
``` ```
## Compatibility ## Compatibility

View File

@ -30,28 +30,24 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "title.md"} * {"name": "title.md"}
* *
* @example * @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 * @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 * @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` * 1:1: Unexpected file name starting with `an`, remove it
*
* @example
* {"name": "an-article.md", "label": "output", "positionless": true}
*
* 1:1: Do not start file names with `an`
*/ */
/** /**
@ -72,10 +68,12 @@ const remarkLintNoFileNameArticles = lintRule(
* Nothing. * Nothing.
*/ */
function (_, file) { 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) { if (match) {
file.message('Do not start file names with `' + match[0] + '`') file.message(
'Unexpected file name starting with `' + match[0] + '`, remove it'
)
} }
} }
) )

View File

@ -144,7 +144,7 @@ No messages.
###### Out ###### Out
```text ```text
1:1: Do not start file names with `a` 1:1: Unexpected file name starting with `a`, remove it
``` ```
##### `the-title.md` ##### `the-title.md`
@ -152,15 +152,7 @@ No messages.
###### Out ###### Out
```text ```text
1:1: Do not start file names with `the` 1:1: Unexpected file name starting with `the`, remove it
```
##### `teh-title.md`
###### Out
```text
1:1: Do not start file names with `teh`
``` ```
##### `an-article.md` ##### `an-article.md`
@ -168,7 +160,7 @@ No messages.
###### Out ###### Out
```text ```text
1:1: Do not start file names with `an` 1:1: Unexpected file name starting with `an`, remove it
``` ```
## Compatibility ## Compatibility

View File

@ -30,13 +30,14 @@
* @author Titus Wormer * @author Titus Wormer
* @copyright 2015 Titus Wormer * @copyright 2015 Titus Wormer
* @license MIT * @license MIT
*
* @example * @example
* {"name": "plug-ins.md"} * {"name": "plug-ins.md"}
* *
* @example * @example
* {"name": "plug--ins.md", "label": "output", "positionless": true} * {"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) { function (_, file) {
if (file.stem && /-{2,}/.test(file.stem)) { 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 `-`')
} }
} }
) )

View File

@ -144,7 +144,7 @@ No messages.
###### Out ###### Out
```text ```text
1:1: Do not use consecutive dashes in a file name 1:1: Unexpected consecutive dashes in a file name, expected `-`
``` ```
## Compatibility ## Compatibility

Some files were not shown because too many files have changed in this diff Show More