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-comment-config": "^8.0.0",
"remark-directive": "^3.0.0",
"remark-frontmatter": "^5.0.0",
"remark-gfm": "^4.0.0",
"remark-github": "^12.0.0",
"remark-math": "^6.0.0",

View File

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

View File

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

View File

@ -171,36 +171,48 @@ Due to this, its recommended to configure this rule with `2`.
## Examples
##### `ok.md`
When configured with `4`.
###### In
```markdown
> Hello
Paragraph.
> World
```
###### Out
No messages.
##### `ok.md`
##### `ok-2.md`
When configured with `2`.
###### In
```markdown
> Hello
> Mercury.
Paragraph.
Venus.
> World
> Earth.
```
###### Out
No messages.
##### `ok-4.md`
When configured with `4`.
###### In
```markdown
> Mercury.
Venus.
> Earth.
```
###### Out
No messages.
##### `ok-tab.md`
###### In
```markdown
>␉Mercury.
```
###### Out
@ -212,22 +224,32 @@ No messages.
###### In
```markdown
> Hello
> Mercury.
Paragraph.
Venus.
> World
> Earth.
Paragraph.
Mars.
> World
> Jupiter
```
###### Out
```text
5:5: Remove 1 space between block quote and content
9:3: Add 1 space between block quote and content
5:5: Unexpected `4` spaces between block quote marker and content, expected `3` spaces, remove `1` space
9:3: Unexpected `2` spaces between block quote marker and content, expected `3` spaces, add `1` space
```
##### `not-ok-options.md`
When configured with `'🌍'`.
###### Out
```text
1:1: Unexpected value `🌍` for `options`, expected `number` or `'consistent'`
```
## Compatibility

View File

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

View File

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

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

View File

@ -55,38 +55,48 @@
* @author Titus Wormer
* @copyright 2015 Titus Wormer
* @license MIT
* @example
* {"name": "ok.md", "gfm": true}
*
* - [ ] List item
* + [x] List Item
* * [X] List item
* - [ ] List item
*
* @example
* {"name": "not-ok.md", "label": "input", "gfm": true}
* {"gfm": true, "name": "ok.md"}
*
* - [ ] List item
* + [x] List item
* * [X] List item
* - [ ] List item
* - [ ] Mercury.
* + [x] Venus.
* * [X] Earth.
* - [ ] Mars.
*
* @example
* {"name": "not-ok.md", "label": "output", "gfm": true}
* {"gfm": true, "label": "input", "name": "not-ok.md"}
*
* 2:7-2:8: Checkboxes should be followed by a single character
* 3:7-3:9: Checkboxes should be followed by a single character
* 4:7-4:10: Checkboxes should be followed by a single character
* - [ ] Mercury.
* + [x] Venus.
* * [X] Earth.
* - [ ] Mars.
* @example
* {"gfm": true, "label": "output", "name": "not-ok.md"}
*
* 2:8: Unexpected `2` spaces between checkbox and content, expected `1` space, remove `1` space
* 3:9: Unexpected `3` spaces between checkbox and content, expected `1` space, remove `2` spaces
* 4:10: Unexpected `4` spaces between checkbox and content, expected `1` space, remove `3` spaces
*
* @example
* {"gfm": true, "label": "input", "name": "tab.md"}
*
* - [ ]Mercury.
* + [x]Venus.
* @example
* {"gfm": true, "label": "output", "name": "tab.md"}
*
* 2:8: Unexpected `2` spaces between checkbox and content, expected `1` space, remove `1` space
*/
/**
* @typedef {import('mdast').Root} Root
*/
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule'
import {pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit'
import {location} from 'vfile-location'
import {visitParents} from 'unist-util-visit-parents'
const remarkLintCheckboxContentIndent = lintRule(
{
@ -101,45 +111,59 @@ const remarkLintCheckboxContentIndent = lintRule(
*/
function (tree, file) {
const value = String(file)
const loc = location(file)
visit(tree, 'listItem', function (node) {
visitParents(tree, 'listItem', function (node, parents) {
const head = node.children[0]
const point = pointStart(head)
const headStart = pointStart(head)
// Exit early for items without checkbox.
// A list item cannot be checked and empty, according to GFM.
// A list item cannot be checked and empty according to GFM.
if (
!point ||
!head ||
!headStart ||
typeof node.checked !== 'boolean' ||
typeof point.offset !== 'number'
typeof headStart.offset !== 'number'
) {
return
}
// Assume we start with a checkbox, because well, `checked` is set.
// Assume we start with a checkbox as `checked` is set.
const match = /\[([\t xX])]/.exec(
value.slice(point.offset - 4, point.offset + 1)
value.slice(headStart.offset - 4, headStart.offset + 1)
)
/* c8 ignore next -- make sure we dont crash if there actually isnt a checkbox. */
if (!match) return
// Move past checkbox.
const initial = point.offset
let final = initial
let final = headStart.offset
let code = value.charCodeAt(final)
while (/[\t ]/.test(value.charAt(final))) final++
while (code === 9 || code === 32) {
final++
code = value.charCodeAt(final)
}
if (final - initial > 0) {
const start = loc.toPoint(initial)
const end = loc.toPoint(final)
const size = final - headStart.offset
if (size) {
file.message(
'Checkboxes should be followed by a single character',
/* c8 ignore next -- we get here if we have offsets. */
start && end ? {start, end} : undefined
'Unexpected `' +
(size + 1) +
'` ' +
pluralize('space', size + 1) +
' between checkbox and content, expected `1` space, remove `' +
size +
'` ' +
pluralize('space', size),
{
ancestors: [...parents, node],
place: {
line: headStart.line,
column: headStart.column + size,
offset: headStart.offset + size
}
}
)
}
})

View File

@ -34,10 +34,10 @@
],
"dependencies": {
"@types/mdast": "^4.0.0",
"pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile-location": "^5.0.0"
"unist-util-visit-parents": "^6.0.0"
},
"scripts": {},
"typeCoverage": {
@ -49,7 +49,8 @@
"xo": {
"prettier": true,
"rules": {
"capitalized-comments": "off"
"capitalized-comments": "off",
"unicorn/prefer-code-point": "off"
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -37,30 +37,31 @@
* @author Titus Wormer
* @copyright 2015 Titus Wormer
* @license MIT
*
* @example
* {"name": "ok.md"}
*
* [example]: http://example.com "Example Domain"
* [mercury]: http://example.com "Mercury"
*
* @example
* {"name": "not-ok.md", "label": "input"}
*
* [Example]: http://example.com "Example Domain"
* {"label": "input", "name": "not-ok.md"}
*
* [Mercury]: http://example.com "Mercury"
* @example
* {"name": "not-ok.md", "label": "output"}
* {"label": "output", "name": "not-ok.md"}
*
* 1:1-1:47: Do not use uppercase characters in definition labels
* 1:1-1:40: Unexpected uppercase characters in definition label, expected lowercase
*
* @example
* {"gfm": true, "label": "input", "name": "gfm.md"}
*
* [^X]: Footnote definitions (from GFM) are checked too.
*
* [^Mercury]:
* **Mercury** is the first planet from the Sun and the smallest
* in the Solar System.
* @example
* {"gfm": true, "label": "output", "name": "gfm.md"}
*
* 1:1-1:55: Do not use uppercase characters in definition labels
* 1:1-3:25: Unexpected uppercase characters in footnote definition label, expected lowercase
*/
/**
@ -68,10 +69,7 @@
*/
import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit'
const label = /^\s*\[((?:\\[\s\S]|[^[\]])+)]/
import {visitParents} from 'unist-util-visit-parents'
const remarkLintDefinitionCase = lintRule(
{
@ -85,28 +83,19 @@ const remarkLintDefinitionCase = lintRule(
* Nothing.
*/
function (tree, file) {
const value = String(file)
visit(tree, function (node) {
if (node.type === 'definition' || node.type === 'footnoteDefinition') {
const end = pointEnd(node)
const start = pointStart(node)
if (
end &&
start &&
typeof end.offset === 'number' &&
typeof start.offset === 'number'
) {
const match = value.slice(start.offset, end.offset).match(label)
if (match && match[1] !== match[1].toLowerCase()) {
file.message(
'Do not use uppercase characters in definition labels',
node
)
}
}
visitParents(tree, function (node, parents) {
if (
(node.type === 'definition' || node.type === 'footnoteDefinition') &&
node.position &&
node.label &&
node.label !== node.label.toLowerCase()
) {
file.message(
'Unexpected uppercase characters in ' +
(node.type === 'definition' ? '' : 'footnote ') +
'definition label, expected lowercase',
{ancestors: [...parents, node], place: node.position}
)
}
})
}

View File

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

View File

@ -146,7 +146,7 @@ Due to this, its recommended to use lowercase and turn this rule on.
###### In
```markdown
[example]: http://example.com "Example Domain"
[mercury]: http://example.com "Mercury"
```
###### Out
@ -158,13 +158,13 @@ No messages.
###### In
```markdown
[Example]: http://example.com "Example Domain"
[Mercury]: http://example.com "Mercury"
```
###### Out
```text
1:1-1:47: Do not use uppercase characters in definition labels
1:1-1:40: Unexpected uppercase characters in definition label, expected lowercase
```
##### `gfm.md`
@ -175,13 +175,15 @@ No messages.
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
[^X]: Footnote definitions (from GFM) are checked too.
[^Mercury]:
**Mercury** is the first planet from the Sun and the smallest
in the Solar System.
```
###### Out
```text
1:1-1:55: Do not use uppercase characters in definition labels
1:1-3:25: Unexpected uppercase characters in footnote definition label, expected lowercase
```
## Compatibility

View File

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

View File

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

View File

@ -150,25 +150,41 @@ Due to this, its recommended to use one space and turn this rule on.
###### In
```markdown
[example domain]: http://example.com "Example Domain"
[planet mercury]: http://example.com
```
###### Out
No messages.
##### `not-ok.md`
##### `not-ok-consecutive.md`
###### In
```markdown
[example␠␠␠␠domain]: http://example.com "Example Domain"
[planet␠␠␠␠mercury]: http://example.com
```
###### Out
```text
1:1-1:57: Do not use consecutive whitespace in definition labels
1:1-1:40: Unexpected `4` consecutive spaces in definition label, expected `1` space, remove `3` spaces
```
##### `not-ok-non-space.md`
###### In
```markdown
[pla␉net␊mer␍cury]: http://e.com
```
###### Out
```text
1:1-3:20: Unexpected non-space whitespace character `\t` in definition label, expected `1` space, replace it
1:1-3:20: Unexpected non-space whitespace character `\n` in definition label, expected `1` space, replace it
1:1-3:20: Unexpected non-space whitespace character `\r` in definition label, expected `1` space, replace it
```
## Compatibility

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -165,8 +165,8 @@ Its recommended to instead use a certain flag for plain text (such as
###### In
````markdown
```alpha
bravo()
```markdown
# Mercury
```
````
@ -180,17 +180,17 @@ No messages.
````markdown
```
alpha()
mercury()
```
````
###### Out
```text
1:1-3:4: Missing code language flag
1:1-3:4: Unexpected missing fenced code language flag in info string, expected keyword
```
##### `ok.md`
##### `ok-allow-empty.md`
When configured with `{ allowEmpty: true }`.
@ -198,7 +198,7 @@ When configured with `{ allowEmpty: true }`.
````markdown
```
alpha()
mercury()
```
````
@ -206,7 +206,7 @@ alpha()
No messages.
##### `not-ok.md`
##### `not-ok-allow-empty.md`
When configured with `{ allowEmpty: false }`.
@ -214,25 +214,25 @@ When configured with `{ allowEmpty: false }`.
````markdown
```
alpha()
mercury()
```
````
###### Out
```text
1:1-3:4: Missing code language flag
1:1-3:4: Unexpected missing fenced code language flag in info string, expected keyword
```
##### `ok.md`
##### `ok-array.md`
When configured with `[ 'alpha' ]`.
When configured with `[ 'markdown' ]`.
###### In
````markdown
```alpha
bravo()
```markdown
# Mercury
```
````
@ -240,15 +240,15 @@ bravo()
No messages.
##### `ok.md`
##### `ok-options.md`
When configured with `{ flags: [ 'alpha' ] }`.
When configured with `{ flags: [ 'markdown' ] }`.
###### In
````markdown
```alpha
bravo()
```markdown
# Mercury
```
````
@ -256,22 +256,50 @@ bravo()
No messages.
##### `not-ok.md`
##### `not-ok-array.md`
When configured with `[ 'charlie' ]`.
When configured with `[ 'markdown' ]`.
###### In
````markdown
```alpha
bravo()
```javascript
mercury()
```
````
###### Out
```text
1:1-3:4: Incorrect code language flag
1:1-3:4: Unexpected fenced code language flag `javascript` in info string, expected `markdown`
```
##### `not-ok-long-array.md`
When configured with `[ 'javascript', 'markdown', 'mdx', 'typescript' ]`.
###### In
````markdown
```html
<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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,64 +38,83 @@
* @author Titus Wormer
* @copyright 2015 Titus Wormer
* @license MIT
*
* @example
* {"name": "ok.md"}
*
* Paragraph.
* Mercury.
*
* [example]: http://example.com "Example Domain"
* [venus]: http://example.com
*
* @example
* {"name": "not-ok.md", "label": "input"}
* {"name": "ok.md"}
*
* Paragraph.
*
* [example]: http://example.com "Example Domain"
*
* Another paragraph.
*
* @example
* {"name": "not-ok.md", "label": "output"}
*
* 3:1-3:47: Move definitions to the end of the file (after `5:19`)
* [mercury]: http://example.com/mercury/
* [venus]: http://example.com/venus/
*
* @example
* {"name": "ok-html-comments.md"}
*
* Paragraph.
* Mercury.
*
* [example-1]: http://example.com/one/
* [venus]: http://example.com/venus/
*
* <!-- 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
* {"name": "ok-mdx-comments.mdx", "mdx": true}
*
* Paragraph.
* Mercury.
*
* [example-1]: http://example.com/one/
* [venus]: http://example.com/venus/
*
* {/* Comments are fine in MDX. *␀/}
* {/* Comments in expressions in MDX are ignored. *␀/}
*
* [example-2]: http://example.com/two/
* [earth]: http://example.com/earth/
*
* @example
* {"label": "input", "name": "not-ok.md"}
*
* Mercury.
*
* [venus]: https://example.com/venus/
*
* Earth.
* @example
* {"label": "output", "name": "not-ok.md"}
*
* 3:1-3:36: Unexpected definition before last content, expected definitions after line `5`
*
* @example
* {"gfm": true, "label": "input", "name": "gfm.md"}
*
* Mercury.
*
* [^venus]:
* **Venus** is the second planet from
* the Sun.
*
* Earth.
* @example
* {"gfm": true, "label": "output", "name": "gfm.md"}
*
* 3:1-5:13: Unexpected footnote definition before last content, expected definitions after line `7`
*/
/**
* @typedef {import('mdast').Definition} Definition
* @typedef {import('mdast').FootnoteDefinition} FootnoteDefinition
* @typedef {import('mdast').Nodes} Nodes
* @typedef {import('mdast').Root} Root
*
* @typedef {import('unist').Point} Point
*/
/// <reference types="mdast-util-mdx" />
import {ok as assert} from 'devlop'
import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position'
import {stringifyPosition} from 'unist-util-stringify-position'
import {visit} from 'unist-util-visit'
import {visitParents} from 'unist-util-visit-parents'
import {VFileMessage} from 'vfile-message'
const remarkLintFinalDefinition = lintRule(
{
@ -109,14 +128,14 @@ const remarkLintFinalDefinition = lintRule(
* Nothing.
*/
function (tree, file) {
/** @type {Array<Definition | FootnoteDefinition>} */
const definitions = []
/** @type {Point | undefined} */
let last
/** @type {Array<Array<Nodes>>} */
const definitionStacks = []
/** @type {Array<Nodes> | undefined} */
let contentAncestors
visit(tree, function (node) {
visitParents(tree, function (node, parents) {
if (node.type === 'definition' || node.type === 'footnoteDefinition') {
definitions.push(node)
definitionStacks.push([...parents, node])
} else if (
node.type === 'root' ||
// Ignore HTML comments.
@ -128,24 +147,42 @@ const remarkLintFinalDefinition = lintRule(
) {
// Empty.
} else {
const place = pointEnd(node)
if (place) {
last = place
}
contentAncestors = [...parents, node]
}
})
for (const node of definitions) {
const point = pointStart(node)
const content = contentAncestors ? contentAncestors.at(-1) : undefined
const contentEnd = pointEnd(content)
if (point && last && point.line < last.line) {
file.message(
'Move definitions to the end of the file (after `' +
stringifyPosition(last) +
'`)',
node
)
if (contentEnd) {
assert(content) // Always defined.
assert(contentAncestors) // Always defined.
for (const definitionAncestors of definitionStacks) {
const definition = definitionAncestors.at(-1)
assert(definition) // Always defined.
const definitionStart = pointStart(definition)
if (definitionStart && definitionStart.line < contentEnd.line) {
file.message(
'Unexpected ' +
(definition.type === 'footnoteDefinition' ? 'footnote ' : '') +
'definition before last content, expected definitions after line `' +
contentEnd.line +
'`',
{
ancestors: definitionAncestors,
cause: new VFileMessage('Last content defined here', {
ancestors: contentAncestors,
place: content.position,
ruleId: 'final-definition',
source: 'remark-lint'
}),
place: definition.position
}
)
}
}
}
}

View File

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

View File

@ -147,45 +147,40 @@ If you prefer that, turn on this rule.
###### In
```markdown
Paragraph.
Mercury.
[example]: http://example.com "Example Domain"
[venus]: http://example.com
```
###### Out
No messages.
##### `not-ok.md`
##### `ok.md`
###### In
```markdown
Paragraph.
[example]: http://example.com "Example Domain"
Another paragraph.
[mercury]: http://example.com/mercury/
[venus]: http://example.com/venus/
```
###### Out
```text
3:1-3:47: Move definitions to the end of the file (after `5:19`)
```
No messages.
##### `ok-html-comments.md`
###### In
```markdown
Paragraph.
Mercury.
[example-1]: http://example.com/one/
[venus]: http://example.com/venus/
<!-- 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
@ -200,19 +195,60 @@ No messages.
> MDX ([`remark-mdx`][github-remark-mdx]).
```mdx
Paragraph.
Mercury.
[example-1]: http://example.com/one/
[venus]: http://example.com/venus/
{/* Comments are fine in MDX. */}
{/* Comments in expressions in MDX are ignored. */}
[example-2]: http://example.com/two/
[earth]: http://example.com/earth/
```
###### Out
No messages.
##### `not-ok.md`
###### In
```markdown
Mercury.
[venus]: https://example.com/venus/
Earth.
```
###### Out
```text
3:1-3:36: Unexpected definition before last content, expected definitions after line `5`
```
##### `gfm.md`
###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
Mercury.
[^venus]:
**Venus** is the second planet from
the Sun.
Earth.
```
###### Out
```text
3:1-5:13: Unexpected footnote definition before last content, expected definitions after line `7`
```
## Compatibility
Projects maintained by the unified collective are compatible with maintained
@ -282,6 +318,8 @@ abide by its terms.
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[github-remark-gfm]: https://github.com/remarkjs/remark-gfm
[github-remark-lint]: https://github.com/remarkjs/remark-lint
[github-remark-mdx]: https://mdxjs.com/packages/remark-mdx/

View File

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

View File

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

View File

@ -150,7 +150,7 @@ always adds final line endings.
###### In
```markdown
Alpha
Mercury
```
###### Out
@ -162,13 +162,13 @@ No messages.
###### In
```markdown
Bravo
Mercury
```
###### Out
```text
1:6: Missing newline character at end of file
1:8: Unexpected missing final newline character, expected line feed (`\n`) at end of file
```
## Compatibility

View File

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

View File

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

View File

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

View File

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

View File

@ -48,7 +48,8 @@
"xo": {
"prettier": true,
"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
```markdown
Lorem ipsum␠␠
dolor sit amet
**Mercury** is the first planet from the Sun␠␠
and the smallest in the Solar System.
```
###### Out
@ -161,14 +161,33 @@ No messages.
###### In
```markdown
Lorem ipsum␠␠␠
dolor sit amet.
**Mercury** is the first planet from the Sun␠␠␠
and the smallest in the Solar System.
```
###### Out
```text
1:12-2:1: Use two spaces for hard line breaks
1:45-2:1: Unexpected `3` spaces for hard break, expected `2` spaces
```
##### `containers.md`
###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
[^mercury]:
> * > * **Mercury** is the first planet from the Sun␠␠␠
> > and the smallest in the Solar System.
```
###### Out
```text
2:57-3:1: Unexpected `3` spaces for hard break, expected `2` spaces
```
## Compatibility
@ -240,6 +259,8 @@ abide by its terms.
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[github-remark-gfm]: https://github.com/remarkjs/remark-gfm
[github-remark-lint]: https://github.com/remarkjs/remark-lint
[github-unified-transformer]: https://github.com/unifiedjs/unified#transformer

View File

@ -44,50 +44,88 @@
* @author Titus Wormer
* @copyright 2015 Titus Wormer
* @license MIT
*
* @example
* {"name": "ok.md"}
*
* # Alpha
* # Mercury
*
* ## Bravo
* ## Nomenclature
*
* @example
* {"name": "not-ok.md", "label": "input"}
* {"name": "also-ok.md"}
*
* # Charlie
* #### Impact basins and craters
*
* ### Delta
* #### Plains
*
* #### Compressional features
*
* @example
* {"name": "not-ok.md", "label": "output"}
* {"label": "input", "name": "not-ok.md"}
*
* 3:1-3:10: Heading levels should increment by one level at a time
* # Mercury
*
* ### Internal structure
*
* ### Surface geology
*
* ## Observation history
*
* #### Mariner 10
*
* @example
* {"name": "html.md"}
* {"label": "output", "name": "not-ok.md"}
*
* In markdown, <b>HTML</b> is supported.
*
* <h1>First heading</h1>
* 3:1-3:23: Unexpected heading rank `3`, exected rank `2`
* 5:1-5:20: Unexpected heading rank `3`, exected rank `2`
* 9:1-9:16: Unexpected heading rank `4`, exected rank `3`
*
* @example
* {"name": "ok.mdx", "mdx": true}
* {"label": "input", "name": "html.md"}
*
* In MDX, <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').Nodes} Nodes
* @typedef {import('mdast').Root} Root
*/
/// <reference types="mdast-util-mdx" />
import {ok as assert} from 'devlop'
import {lintRule} from 'unified-lint-rule'
import {position} from 'unist-util-position'
import {visit} from 'unist-util-visit'
import {visitParents} from 'unist-util-visit-parents'
import {VFileMessage} from 'vfile-message'
const htmlRe = /<h([1-6])/
const jsxNameRe = /^h([1-6])$/
@ -104,47 +142,89 @@ const remarkLintHeadingIncrement = lintRule(
* Nothing.
*/
function (tree, file) {
/** @type {Heading['depth'] | undefined} */
let previous
/** @type {Array<Array<Nodes> | undefined>} */
const stack = []
visit(tree, function (node) {
const place = position(node)
visitParents(tree, function (node, parents) {
const rank = inferRank(node)
if (place) {
/** @type {Heading['depth'] | undefined} */
let rank
if (rank) {
let index = rank
/** @type {Array<Nodes> | undefined} */
let closestAncestors
if (node.type === 'heading') {
rank = node.depth
} else if (node.type === 'html') {
const results = node.value.match(htmlRe)
rank = results
? /** @type {Heading['depth']} */ (Number(results[1]))
: undefined
} else if (
(node.type === 'mdxJsxFlowElement' ||
node.type === 'mdxJsxTextElement') &&
node.name
) {
const results = node.name.match(jsxNameRe)
rank = results
? /** @type {Heading['depth']} */ (Number(results[1]))
: undefined
while (index--) {
if (stack[index]) {
closestAncestors = stack[index]
break
}
}
if (rank) {
if (previous && rank > previous + 1) {
if (closestAncestors) {
const parent = closestAncestors.at(-1)
assert(parent) // Always defined.
const parentRank = inferRank(parent)
assert(parentRank) // Always defined.
if (node.position && rank > parentRank + 1) {
file.message(
'Heading levels should increment by one level at a time',
place
'Unexpected heading rank `' +
rank +
'`, exected rank `' +
(parentRank + 1) +
'`',
{
ancestors: [...parents, node],
cause: new VFileMessage('Parent heading defined here', {
ancestors: closestAncestors,
place: parent.position,
source: 'remark-lint',
ruleId: 'heading-increment'
}),
place: node.position
}
)
}
previous = rank
}
stack[rank] = [...parents, node]
// Drop things after it.
stack.length = rank + 1
}
})
}
)
export default remarkLintHeadingIncrement
/**
* Get rank of a node.
*
* @param {Nodes} node
* Node.
* @returns {Heading['depth'] | undefined}
* Rank, if heading.
*/
function inferRank(node) {
/** @type {Heading['depth'] | undefined} */
let rank
if (node.type === 'heading') {
rank = node.depth
} else if (node.type === 'html') {
const results = node.value.match(htmlRe)
rank = results
? /** @type {Heading['depth']} */ (Number(results[1]))
: undefined
} else if (
(node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
node.name
) {
const results = node.name.match(jsxNameRe)
rank = results
? /** @type {Heading['depth']} */ (Number(results[1]))
: undefined
}
return rank
}

View File

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

View File

@ -153,9 +153,25 @@ its recommended that this rule is turned on.
###### In
```markdown
# Alpha
# Mercury
## Bravo
## Nomenclature
```
###### Out
No messages.
##### `also-ok.md`
###### In
```markdown
#### Impact basins and craters
#### Plains
#### Compressional features
```
###### Out
@ -167,15 +183,23 @@ No messages.
###### In
```markdown
# Charlie
# Mercury
### Delta
### Internal structure
### Surface geology
## Observation history
#### Mariner 10
```
###### Out
```text
3:1-3:10: Heading levels should increment by one level at a time
3:1-3:23: Unexpected heading rank `3`, exected rank `2`
5:1-5:20: Unexpected heading rank `3`, exected rank `2`
9:1-9:16: Unexpected heading rank `4`, exected rank `3`
```
##### `html.md`
@ -183,16 +207,23 @@ No messages.
###### In
```markdown
In markdown, <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
No messages.
```text
6:1-6:28: Unexpected heading rank `3`, exected rank `2`
```
##### `ok.mdx`
##### `mdx.mdx`
###### In
@ -200,14 +231,21 @@ No messages.
> MDX ([`remark-mdx`][github-remark-mdx]).
```mdx
In MDX, <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
No messages.
```text
6:1-6:28: Unexpected heading rank `3`, exected rank `2`
```
## Compatibility

View File

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

View File

@ -37,7 +37,8 @@
"mdast-util-heading-style": "^3.0.0",
"unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0"
"unist-util-visit-parents": "^6.0.0",
"vfile-message": "^4.0.0"
},
"scripts": {},
"typeCoverage": {

View File

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

View File

@ -55,7 +55,7 @@
*
* ## Fix
*
* [`remark-stringify`][github-remark-stringify] always uses Unix linebreaks.
* [`remark-stringify`][github-remark-stringify] always uses Unix line endings.
*
* [api-options]: #options
* [api-remark-lint-linebreak-style]: #unifieduseremarklintlinebreakstyle-options
@ -67,35 +67,56 @@
* @author Titus Wormer
* @copyright 2017 Titus Wormer
* @license MIT
*
* @example
* {"name": "ok-consistent-as-windows.md"}
*
* AlphaBravo
* MercuryandVenus.
*
* @example
* {"name": "ok-consistent-as-unix.md"}
*
* AlphaBravo
* MercuryandVenus.
*
* @example
* {"name": "not-ok-unix.md", "label": "input", "config": "unix", "positionless": true}
* {"config": "unix", "label": "input", "name": "not-ok-unix.md", "positionless": true}
*
* Alpha
* Mercury.
*
* @example
* {"name": "not-ok-unix.md", "label": "output", "config": "unix"}
* {"config": "unix", "label": "output", "name": "not-ok-unix.md", "positionless": true}
*
* 1:7: Expected linebreaks to be unix (`\n`), not windows (`\r\n`)
* 1:10: Unexpected windows (`\r\n`) line ending, expected unix (`\n`) line endings
*
* @example
* {"name": "not-ok-windows.md", "label": "input", "config": "windows", "positionless": true}
* {"config": "windows", "label": "input", "name": "not-ok-windows.md", "positionless": true}
*
* Alpha
* Mercury.
*
* @example
* {"name": "not-ok-windows.md", "label": "output", "config": "windows"}
* {"config": "windows", "label": "output", "name": "not-ok-windows.md", "positionless": true}
*
* 1:6: Expected linebreaks to be windows (`\r\n`), not unix (`\n`)
* 1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
*
* @example
* {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true}
*
* 1:1: Unexpected value `🌍` for `options`, expected `'unix'`, `'windows'`, or `'consistent'`
*
* @example
* {"config": "windows", "label": "input", "name": "many.md", "positionless": true}
*
* Mercury.Venus.Earth.Mars.Jupiter.Saturn.Uranus.Neptune.
*
* @example
* {"config": "windows", "label": "output", "name": "many.md", "positionless": true}
*
* 1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
* 2:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
* 3:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
* 4:6: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
* 5:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
* 6:8: Unexpected large number of incorrect line endings, stopping
*/
/**
@ -110,10 +131,12 @@
* Styles.
*/
import {ok as assert} from 'devlop'
import {lintRule} from 'unified-lint-rule'
import {location} from 'vfile-location'
import {VFileMessage} from 'vfile-message'
const escaped = {unix: '\\n', windows: '\\r\\n'}
const max = 5
const remarkLintLinebreakStyle = lintRule(
{
@ -129,28 +152,60 @@ const remarkLintLinebreakStyle = lintRule(
* Nothing.
*/
function (_, file, options) {
let option = options || 'consistent'
const value = String(file)
const toPoint = location(value).toPoint
let index = value.indexOf('\n')
/** @type {VFileMessage | undefined} */
let cause
/** @type {Style | undefined} */
let expected
if (options === null || options === undefined || options === 'consistent') {
// Empty.
} else if (options === 'unix' || options === 'windows') {
expected = options
} else {
file.fail(
'Unexpected value `' +
options +
"` for `options`, expected `'unix'`, `'windows'`, or `'consistent'`"
)
}
let messages = 0
while (index !== -1) {
const type = value.charAt(index - 1) === '\r' ? 'windows' : 'unix'
const actual = value.charAt(index - 1) === '\r' ? 'windows' : 'unix'
const place = toPoint(index)
assert(place) // Always defined.
if (option === 'consistent') {
option = type
} else if (option !== type) {
file.message(
'Expected linebreaks to be ' +
option +
' (`' +
escaped[option] +
'`), not ' +
type +
' (`' +
escaped[type] +
'`)',
toPoint(index)
if (expected) {
if (expected !== actual) {
if (messages === max) {
file.info(
'Unexpected large number of incorrect line endings, stopping',
{place}
)
return
}
file.message(
'Unexpected ' +
displayStyle(actual) +
' line ending, expected ' +
displayStyle(expected) +
' line endings',
{cause, place}
)
messages++
}
} else {
expected = actual
cause = new VFileMessage(
'Line ending style ' +
displayStyle(expected) +
" first defined for `'consistent'` here",
{place, ruleId: 'linebreak-style', source: 'remark-lint'}
)
}
@ -160,3 +215,13 @@ const remarkLintLinebreakStyle = lintRule(
)
export default remarkLintLinebreakStyle
/**
* @param {Style} style
* Style.
* @returns {string}
* Display.
*/
function displayStyle(style) {
return style === 'unix' ? 'unix (`\\n`)' : 'windows (`\\r\\n`)'
}

View File

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

View File

@ -165,7 +165,7 @@ used.
## Fix
[`remark-stringify`][github-remark-stringify] always uses Unix linebreaks.
[`remark-stringify`][github-remark-stringify] always uses Unix line endings.
## Examples
@ -174,7 +174,7 @@ used.
###### In
```markdown
Alpha␍␊Bravo␍␊
Mercury␍␊and␍␊Venus.
```
###### Out
@ -186,7 +186,7 @@ No messages.
###### In
```markdown
Alpha␊Bravo␊
Mercury␊and␊Venus.
```
###### Out
@ -200,13 +200,13 @@ When configured with `'unix'`.
###### In
```markdown
Alpha␍␊
Mercury.␍␊
```
###### Out
```text
1:7: Expected linebreaks to be unix (`\n`), not windows (`\r\n`)
1:10: Unexpected windows (`\r\n`) line ending, expected unix (`\n`) line endings
```
##### `not-ok-windows.md`
@ -216,13 +216,44 @@ When configured with `'windows'`.
###### In
```markdown
Alpha
Mercury.
```
###### Out
```text
1:6: Expected linebreaks to be windows (`\r\n`), not unix (`\n`)
1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
```
##### `not-ok-options.md`
When configured with `'🌍'`.
###### Out
```text
1:1: Unexpected value `🌍` for `options`, expected `'unix'`, `'windows'`, or `'consistent'`
```
##### `many.md`
When configured with `'windows'`.
###### In
```markdown
Mercury.␊Venus.␊Earth.␊Mars.␊Jupiter.␊Saturn.␊Uranus.␊Neptune.␊
```
###### Out
```text
1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
2:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
3:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
4:6: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
5:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
6:8: Unexpected large number of incorrect line endings, stopping
```
## Compatibility

View File

@ -3,7 +3,8 @@
*
* ## What is this?
*
* This package checks the style of link title markers.
* This package checks the style of link (*and* image and definition) title
* markers.
*
* ## When should I use this?
*
@ -59,7 +60,7 @@
*
* ## Fix
*
* [`remark-stringify`][github-remark-stringify] formats titles with double
* [`remark-stringify`][github-remark-stringify] formats titles with double
* quotes by default.
* Pass `quote: "'"` to use single quotes.
* There is no option to use parens.
@ -74,82 +75,90 @@
* @author Titus Wormer
* @copyright 2015 Titus Wormer
* @license MIT
* @example
* {"name": "ok.md", "config": "\""}
*
* [Example](http://example.com#without-title)
* [Example](http://example.com "Example Domain")
* ![Example](http://example.com "Example Domain")
*
* [Example]: http://example.com "Example Domain"
*
* You can use parens in URLs if theyre not a title (see GH-166):
*
* [Example](#Heading-(optional))
*
* @example
* {"name": "not-ok.md", "label": "input", "config": "\""}
* {"name": "ok-consistent.md"}
*
* [Example]: http://example.com 'Example Domain'
* [Mercury](http://example.com/mercury/),
* [Venus](http://example.com/venus/ "Go to Venus"), and
* ![Earth](http://example.com/earth/ "Go to Earth").
*
* [Mars]: http://example.com/mars/ "Go to Mars"
*
* @example
* {"name": "not-ok.md", "label": "output", "config": "\""}
* {"label": "input", "name": "not-ok-consistent.md"}
*
* 1:31-1:47: Titles should use `"` as a quote
* [Mercury](http://example.com/mercury/ "Go to Mercury") and
* ![Venus](http://example.com/venus/ 'Go to Venus').
*
* [Earth]: http://example.com/earth/ (Go to Earth)
* @example
* {"label": "output", "name": "not-ok-consistent.md"}
*
* 2:1-2:50: Unexpected title markers `'`, expected `"`
* 4:1-4:49: Unexpected title markers `'('` and `')'`, expected `"`
*
* @example
* {"name": "ok.md", "config": "'"}
* {"config": "\"", "name": "ok-double.md"}
*
* [Example](http://example.com#without-title)
* [Example](http://example.com 'Example Domain')
* ![Example](http://example.com 'Example Domain')
*
* [Example]: http://example.com 'Example Domain'
* [Mercury](http://example.com/mercury/ "Go to Mercury").
*
* @example
* {"name": "not-ok.md", "label": "input", "config": "'"}
* {"config": "\"", "label": "input", "name": "not-ok-double.md"}
*
* [Example]: http://example.com "Example Domain"
* [Mercury](http://example.com/mercury/ 'Go to Mercury').
* @example
* {"config": "\"", "label": "output", "name": "not-ok-double.md"}
*
* 1:1-1:55: Unexpected title markers `'`, expected `"`
*
* @example
* {"name": "not-ok.md", "label": "output", "config": "'"}
* {"config": "'", "name": "ok-single.md"}
*
* 1:31-1:47: Titles should use `'` as a quote
* [Mercury](http://example.com/mercury/ 'Go to Mercury').
*
* @example
* {"name": "ok.md", "config": "()"}
* {"config": "'", "label": "input", "name": "not-ok-single.md"}
*
* [Example](http://example.com#without-title)
* [Example](http://example.com (Example Domain))
* ![Example](http://example.com (Example Domain))
* [Mercury](http://example.com/mercury/ "Go to Mercury").
* @example
* {"config": "'", "label": "output", "name": "not-ok-single.md"}
*
* [Example]: http://example.com (Example Domain)
* 1:1-1:55: Unexpected title markers `"`, expected `'`
*
* @example
* {"name": "not-ok.md", "label": "input", "config": "()"}
* {"config": "()", "name": "ok-paren.md"}
*
* [Example](http://example.com 'Example Domain')
* [Mercury](http://example.com/mercury/ (Go to Mercury)).
*
* @example
* {"name": "not-ok.md", "label": "output", "config": "()"}
* {"config": "()", "label": "input", "name": "not-ok-paren.md"}
*
* 1:30-1:46: Titles should use `()` as a quote
* [Mercury](http://example.com/mercury/ "Go to Mercury").
* @example
* {"config": "()", "label": "output", "name": "not-ok-paren.md"}
*
* 1:1-1:55: Unexpected title markers `"`, expected `'('` and `')'`
*
* @example
* {"name": "not-ok.md", "label": "input"}
* {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true}
*
* [Example](http://example.com "Example Domain")
* [Example](http://example.com 'Example Domain')
* 1:1: Unexpected value `🌍` for `options`, expected `'"'`, `"'"`, `'()'`, or `'consistent'`
*
* @example
* {"name": "not-ok.md", "label": "output"}
* {"config": "\"", "name": "ok-parens-in-url.md"}
*
* 2:30-2:46: Titles should use `"` as a quote
* Parens in URLs work correctly:
*
* [Mercury](http://example.com/(mercury) "Go to Mercury") and
* [Venus](http://example.com/(venus)).
*
* @example
* {"name": "not-ok.md", "config": "💩", "label": "output", "positionless": true}
* {"config": "\"", "name": "ok-whitespace.md"}
*
* 1:1: Incorrect link title style marker `💩`: use either `'consistent'`, `'"'`, `'\''`, or `'()'`
* Trailing whitespace works correctly:
*
* [Mercury](http://example.com/mercury/␠"Go to Mercury"␠).
*/
/**
@ -165,15 +174,9 @@
*/
import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit'
import {location} from 'vfile-location'
const markers = {
'"': '"',
"'": "'",
')': '('
}
import {pointEnd} from 'unist-util-position'
import {visitParents} from 'unist-util-visit-parents'
import {VFileMessage} from 'vfile-message'
const remarkLintLinkTitleStyle = lintRule(
{
@ -190,78 +193,88 @@ const remarkLintLinkTitleStyle = lintRule(
*/
function (tree, file, options) {
const value = String(file)
const loc = location(file)
const option = options || 'consistent'
// @ts-expect-error: allow `(` too, even though untyped.
let look = option === '()' || option === '(' ? ')' : option
/** @type {Style | undefined} */
let expected
/** @type {VFileMessage | undefined} */
let cause
if (look !== 'consistent' && !Object.hasOwn(markers, look)) {
if (options === null || options === undefined || options === 'consistent') {
// Empty.
/* c8 ignore next 3 */
// @ts-expect-error: to do: remove.
} else if (options === '(') {
expected = '()'
} else if (options === '"' || options === "'" || options === '()') {
expected = options
} else {
file.fail(
'Incorrect link title style marker `' +
look +
"`: use either `'consistent'`, `'\"'`, `'\\''`, or `'()'`"
'Unexpected value `' +
options +
"` for `options`, expected `'\"'`, `\"'\"`, `'()'`, or `'consistent'`"
)
}
visit(tree, function (node) {
visitParents(tree, function (node, parents) {
if (
node.type === 'definition' ||
node.type === 'image' ||
node.type === 'link'
) {
const tail =
'children' in node
? node.children[node.children.length - 1]
: undefined
const begin = tail ? pointEnd(tail) : pointStart(node)
// Exit w/o title.
if (!node.title) return
const end = pointEnd(node)
let endIndex = end ? end.offset : undefined
if (
!begin ||
!end ||
typeof begin.offset !== 'number' ||
typeof end.offset !== 'number'
) {
return
// Exit w/o position.
if (!endIndex) return
// `)`
if (node.type !== 'definition') endIndex--
// Whitespace.
let before = value.charCodeAt(endIndex - 1)
while (before === 9 || before === 32) {
endIndex--
before = value.charCodeAt(endIndex - 1)
}
let last = end.offset - 1
/** @type {Style | undefined} */
const actual =
before === 34 /* `"` */
? '"'
: before === 39 /* `'` */
? "'"
: before === 41 /* `)` */
? '()'
: /* c8 ignore next -- we should find a correct marker. */
undefined
if (node.type !== 'definition') {
last--
}
/* c8 ignore next -- we should find a correct marker. */
if (!actual) return
const final = /** @type {keyof markers} */ (value.charAt(last))
// Exit if the final marker is not a known marker.
if (!(final in markers)) {
return
}
const initial = markers[final]
// Find the starting delimiter
const first = value.lastIndexOf(initial, last - 1)
// Exit if theres no starting delimiter, the starting delimiter is before
// the start of the node, or if its not preceded by whitespace.
if (first <= begin.offset || !/\s/.test(value.charAt(first - 1))) {
return
}
if (look === 'consistent') {
look = final
} else if (look !== final) {
const start = loc.toPoint(first)
const end = loc.toPoint(last + 1)
/* c8 ignore next -- we get here if we have offsets. */
const place = start && end ? {start, end} : undefined
file.message(
'Titles should use `' +
(look === ')' ? '()' : look) +
'` as a quote',
place
if (expected) {
if (actual !== expected) {
file.message(
'Unexpected title markers ' +
displayStyle(actual) +
', expected ' +
displayStyle(expected),
{ancestors: [...parents, node], cause, place: node.position}
)
}
} else {
expected = actual
cause = new VFileMessage(
'Title marker style ' +
displayStyle(expected) +
" first defined for `'consistent'` here",
{
ancestors: [...parents, node],
place: node.position,
ruleId: 'link-title-style',
source: 'remark-lint'
}
)
}
}
@ -270,3 +283,13 @@ const remarkLintLinkTitleStyle = lintRule(
)
export default remarkLintLinkTitleStyle
/**
* @param {Style} style
* Style.
* @returns {string}
* Display.
*/
function displayStyle(style) {
return style === '"' ? '`"`' : style === "'" ? "`'`" : "`'('` and `')'`"
}

View File

@ -36,8 +36,8 @@
"@types/mdast": "^4.0.0",
"unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"vfile-location": "^5.0.0"
"unist-util-visit-parents": "^6.0.0",
"vfile-message": "^4.0.0"
},
"scripts": {},
"typeCoverage": {
@ -50,7 +50,9 @@
"prettier": true,
"rules": {
"capitalized-comments": "off",
"unicorn/prefer-at": "off"
"unicorn/prefer-at": "off",
"unicorn/prefer-code-point": "off",
"unicorn/prefer-switch": "off"
}
}
}

View File

@ -32,7 +32,8 @@
## What is this?
This package checks the style of link title markers.
This package checks the style of link (*and* image and definition) title
markers.
## When should I use this?
@ -174,143 +175,179 @@ markdown, so its recommended to configure this rule with `'"'`.
## Fix
[`remark-stringify`][github-remark-stringify] formats titles with double
[`remark-stringify`][github-remark-stringify] formats titles with double
quotes by default.
Pass `quote: "'"` to use single quotes.
There is no option to use parens.
## Examples
##### `ok.md`
##### `ok-consistent.md`
###### In
```markdown
[Mercury](http://example.com/mercury/),
[Venus](http://example.com/venus/ "Go to Venus"), and
![Earth](http://example.com/earth/ "Go to Earth").
[Mars]: http://example.com/mars/ "Go to Mars"
```
###### Out
No messages.
##### `not-ok-consistent.md`
###### In
```markdown
[Mercury](http://example.com/mercury/ "Go to Mercury") and
![Venus](http://example.com/venus/ 'Go to Venus').
[Earth]: http://example.com/earth/ (Go to Earth)
```
###### Out
```text
2:1-2:50: Unexpected title markers `'`, expected `"`
4:1-4:49: Unexpected title markers `'('` and `')'`, expected `"`
```
##### `ok-double.md`
When configured with `'"'`.
###### In
```markdown
[Example](http://example.com#without-title)
[Example](http://example.com "Example Domain")
![Example](http://example.com "Example Domain")
[Example]: http://example.com "Example Domain"
You can use parens in URLs if theyre not a title (see GH-166):
[Example](#Heading-(optional))
[Mercury](http://example.com/mercury/ "Go to Mercury").
```
###### Out
No messages.
##### `not-ok.md`
##### `not-ok-double.md`
When configured with `'"'`.
###### In
```markdown
[Example]: http://example.com 'Example Domain'
[Mercury](http://example.com/mercury/ 'Go to Mercury').
```
###### Out
```text
1:31-1:47: Titles should use `"` as a quote
1:1-1:55: Unexpected title markers `'`, expected `"`
```
##### `ok.md`
##### `ok-single.md`
When configured with `"'"`.
###### In
```markdown
[Example](http://example.com#without-title)
[Example](http://example.com 'Example Domain')
![Example](http://example.com 'Example Domain')
[Example]: http://example.com 'Example Domain'
[Mercury](http://example.com/mercury/ 'Go to Mercury').
```
###### Out
No messages.
##### `not-ok.md`
##### `not-ok-single.md`
When configured with `"'"`.
###### In
```markdown
[Example]: http://example.com "Example Domain"
[Mercury](http://example.com/mercury/ "Go to Mercury").
```
###### Out
```text
1:31-1:47: Titles should use `'` as a quote
1:1-1:55: Unexpected title markers `"`, expected `'`
```
##### `ok.md`
##### `ok-paren.md`
When configured with `'()'`.
###### In
```markdown
[Example](http://example.com#without-title)
[Example](http://example.com (Example Domain))
![Example](http://example.com (Example Domain))
[Example]: http://example.com (Example Domain)
[Mercury](http://example.com/mercury/ (Go to Mercury)).
```
###### Out
No messages.
##### `not-ok.md`
##### `not-ok-paren.md`
When configured with `'()'`.
###### In
```markdown
[Example](http://example.com 'Example Domain')
[Mercury](http://example.com/mercury/ "Go to Mercury").
```
###### Out
```text
1:30-1:46: Titles should use `()` as a quote
1:1-1:55: Unexpected title markers `"`, expected `'('` and `')'`
```
##### `not-ok.md`
When configured with `'🌍'`.
###### Out
```text
1:1: Unexpected value `🌍` for `options`, expected `'"'`, `"'"`, `'()'`, or `'consistent'`
```
##### `ok-parens-in-url.md`
When configured with `'"'`.
###### In
```markdown
[Example](http://example.com "Example Domain")
[Example](http://example.com 'Example Domain')
Parens in URLs work correctly:
[Mercury](http://example.com/(mercury) "Go to Mercury") and
[Venus](http://example.com/(venus)).
```
###### Out
```text
2:30-2:46: Titles should use `"` as a quote
No messages.
##### `ok-whitespace.md`
When configured with `'"'`.
###### In
```markdown
Trailing whitespace works correctly:
[Mercury](http://example.com/mercury/␠"Go to Mercury"␠).
```
##### `not-ok.md`
When configured with `'💩'`.
###### Out
```text
1:1: Incorrect link title style marker `💩`: use either `'consistent'`, `'"'`, `'\''`, or `'()'`
```
No messages.
## Compatibility

View File

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

View File

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

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

View File

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

View File

@ -37,7 +37,8 @@
"pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0"
"unist-util-visit-parents": "^6.0.0",
"vfile-message": "^4.0.0"
},
"scripts": {},
"typeCoverage": {
@ -49,7 +50,8 @@
"xo": {
"prettier": true,
"rules": {
"capitalized-comments": "off"
"capitalized-comments": "off",
"unicorn/prefer-code-point": "off"
}
}
}

View File

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

View File

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

View File

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

View File

@ -203,21 +203,20 @@ by default.
###### In
```markdown
*␠List
␠␠item.
*␠Mercury.
*␠Venus.
Paragraph.
111.␠Earth
␠␠␠␠␠and Mars.
11.␠List
␠␠␠␠item.
*␠**Jupiter**.
Paragraph.
␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar
␠␠System.
*␠List
␠␠item.
*␠Saturn.
*␠List
␠␠item.
␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
```
###### Out
@ -231,113 +230,26 @@ When configured with `'mixed'`.
###### In
```markdown
*␠List item.
*␠Mercury.
*␠Venus.
Paragraph.
111.␠Earth
␠␠␠␠␠and Mars.
11.␠List item
*␠␠␠**Jupiter**.
Paragraph.
␠␠␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar
␠␠␠␠System.
*␠␠␠List
␠␠␠␠item.
*␠␠␠Saturn.
*␠␠␠List
␠␠␠␠item.
␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
```
###### Out
No messages.
##### `ok.md`
When configured with `'one'`.
###### In
```markdown
*␠List item.
Paragraph.
11.␠List item
Paragraph.
*␠List
␠␠item.
*␠List
␠␠item.
```
###### Out
No messages.
##### `ok.md`
When configured with `'tab'`.
###### In
```markdown
*␠␠␠List
␠␠␠␠item.
Paragraph.
11.␠List
␠␠␠␠item.
Paragraph.
*␠␠␠List
␠␠␠␠item.
*␠␠␠List
␠␠␠␠item.
```
###### Out
No messages.
##### `not-ok.md`
When configured with `'one'`.
###### In
```markdown
*␠␠␠List
␠␠␠␠item.
```
###### Out
```text
1:5: Incorrect list-item indent: remove 2 spaces
```
##### `not-ok.md`
When configured with `'tab'`.
###### In
```markdown
*␠List
␠␠item.
```
###### Out
```text
1:3: Incorrect list-item indent: add 2 spaces
```
##### `not-ok.md`
When configured with `'mixed'`.
@ -345,25 +257,250 @@ When configured with `'mixed'`.
###### In
```markdown
*␠␠␠List item.
*␠␠␠Mercury.
*␠␠␠Venus.
111.␠␠␠␠Earth
␠␠␠␠␠␠␠␠and Mars.
*␠**Jupiter**.
␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar
␠␠System.
*␠Saturn.
␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
```
###### Out
```text
1:5: Incorrect list-item indent: remove 2 spaces
1:5: Unexpected `3` spaces between list item marker and content in tight list, expected `1` space, remove `2` spaces
2:5: Unexpected `3` spaces between list item marker and content in tight list, expected `1` space, remove `2` spaces
4:9: Unexpected `4` spaces between list item marker and content in tight list, expected `1` space, remove `3` spaces
7:3: Unexpected `1` space between list item marker and content in loose list, expected `3` spaces, add `2` spaces
12:3: Unexpected `1` space between list item marker and content in loose list, expected `3` spaces, add `2` spaces
```
##### `ok.md`
When configured with `'one'`.
###### In
```markdown
*␠Mercury.
*␠Venus.
111.␠Earth
␠␠␠␠␠and Mars.
*␠**Jupiter**.
␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar
␠␠System.
*␠Saturn.
␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
```
###### Out
No messages.
##### `not-ok.md`
When configured with `'one'`.
###### In
```markdown
*␠␠␠Mercury.
*␠␠␠Venus.
111.␠␠␠␠Earth
␠␠␠␠␠␠␠␠and Mars.
*␠␠␠**Jupiter**.
␠␠␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar
␠␠␠␠System.
*␠␠␠Saturn.
␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
```
###### Out
```text
1:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces
2:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces
4:9: Unexpected `4` spaces between list item marker and content, expected `1` space, remove `3` spaces
7:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces
12:5: Unexpected `3` spaces between list item marker and content, expected `1` space, remove `2` spaces
```
##### `ok.md`
When configured with `'tab'`.
###### In
```markdown
*␠␠␠Mercury.
*␠␠␠Venus.
111.␠␠␠␠Earth
␠␠␠␠␠␠␠␠and Mars.
*␠␠␠**Jupiter**.
␠␠␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar
␠␠␠␠System.
*␠␠␠Saturn.
␠␠␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
```
###### Out
No messages.
##### `not-ok.md`
When configured with `'tab'`.
###### In
```markdown
*␠Mercury.
*␠Venus.
111.␠Earth
␠␠␠␠␠and Mars.
*␠**Jupiter**.
␠␠Jupiter is the fifth planet from the Sun and the largest in the Solar
␠␠System.
*␠Saturn.
␠␠Saturn is the sixth planet from the Sun and the second-largest in the Solar System, after Jupiter.
```
###### Out
```text
1:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces
2:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces
4:6: Unexpected `1` space between list item marker and content, expected `4` spaces, add `3` spaces
7:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces
12:3: Unexpected `1` space between list item marker and content, expected `3` spaces, add `2` spaces
```
##### `not-ok.md`
When configured with `'💩'`.
When configured with `'🌍'`.
###### Out
```text
1:1: Incorrect list-item indent style `💩`: use either `'mixed'`, `'one'`, or `'tab'`
1:1: Unexpected value `🌍` for `options`, expected `'mixed'`, `'one'`, or `'tab'`
```
##### `gfm.md`
When configured with `'mixed'`.
###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
*␠[x] Mercury.
1.␠␠[ ] Venus.
2.␠␠[ ] Earth.
```
###### Out
No messages.
##### `gfm.md`
When configured with `'one'`.
###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
*␠[x] Mercury.
1.␠[ ] Venus.
2.␠[ ] Earth.
```
###### Out
No messages.
##### `gfm.md`
When configured with `'tab'`.
###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
*␠␠␠[x] Mercury.
1.␠␠[ ] Venus.
2.␠␠[ ] Earth.
```
###### Out
No messages.
##### `loose-tight.md`
When configured with `'mixed'`.
###### In
```markdown
Loose lists have blank lines between items:
*␠␠␠Mercury.
*␠␠␠Venus.
…or between children of items:
1.␠␠Earth.
␠␠␠␠Earth is the third planet from the Sun and the only astronomical
␠␠␠␠object known to harbor life.
```
###### Out
No messages.
## Compatibility
Projects maintained by the unified collective are compatible with maintained
@ -435,6 +572,8 @@ abide by its terms.
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[github-remark-gfm]: https://github.com/remarkjs/remark-gfm
[github-remark-lint]: https://github.com/remarkjs/remark-lint
[github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -149,9 +149,7 @@ every heading out loud to navigate within a page).
###### In
```markdown
# Alpha bravo charlie delta echo foxtrot golf hotel
# ![Alpha bravo charlie delta echo foxtrot golf hotel](http://example.com/nato.png)
# Mercury is the first planet from the Sun
```
###### Out
@ -160,21 +158,21 @@ No messages.
##### `not-ok.md`
When configured with `40`.
When configured with `30`.
###### In
```markdown
# Alpha bravo charlie delta echo foxtrot golf hotel
# Mercury is the first planet from the Sun
```
###### Out
```text
1:1-1:52: Use headings shorter than `40`
1:1-1:43: Unexpected `40` characters in heading, expected at most `30` characters
```
##### `ok.mdx`
##### `mdx.mdx`
When configured with `30`.
@ -184,13 +182,23 @@ When configured with `30`.
> MDX ([`remark-mdx`][github-remark-mdx]).
```mdx
<h1>In MDX, headings are checked too</h1>
<h1>Mercury is the first planet from the Sun</h1>
```
###### Out
```text
1:1-1:42: Use headings shorter than `30`
1:1-1:50: Unexpected `40` characters in heading, expected at most `30` characters
```
##### `not-ok.md`
When configured with `'🌍'`.
###### Out
```text
1:1: Unexpected value `🌍` for `options`, expected `number`
```
## Compatibility

View File

@ -43,80 +43,112 @@
* @author Titus Wormer
* @copyright 2015 Titus Wormer
* @license MIT
* @example
* {"name": "ok.md", "positionless": true, "gfm": true}
*
* This line is simply not toooooooooooooooooooooooooooooooooooooooooooo
* long.
*
* This is also fine: <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
* {"name": "not-ok.md", "config": 80, "label": "input", "positionless": true}
* {"name": "ok.md", "positionless": true}
*
* This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo
* long.
* Mercury mercury mercury mercury mercury mercury mercury mercury mercury mercury
* mercury.
*
* Just like thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis one.
* Mercury mercury mercury mercury mercury mercury mercury mercury mercury `mercury()`.
*
* And this one is also very wrong: because the link starts aaaaaaafter the column: <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
* {"name": "not-ok.md", "config": 80, "label": "output", "positionless": true}
* {"config": 20, "label": "input", "name": "not-ok.md", "positionless": true}
*
* 4:86: Line must be at most 80 characters
* 6:99: Line must be at most 80 characters
* 8:96: Line must be at most 80 characters
* 10:97: Line must be at most 80 characters
* 12:99: Line must be at most 80 characters
* Mercury mercury mercury
* mercury.
*
* Mercury mercury mercury `mercury()`.
*
* Mercury mercury mercury <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
* {"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
*
* @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
*
* @example
* {"name": "not-ok-mixed-line-endings.md", "config": 10, "label": "output", "positionless": true}
* {"config": 10, "label": "output", "name": "not-ok-mixed-line-endings.md", "positionless": true}
*
* 1:13: Line must be at most 10 characters
* 2:13: Line must be at most 10 characters
* 3:12: Line must be at most 10 characters
* 4:12: Line must be at most 10 characters
* 1:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
* 2:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
* 3:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character
* 4:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character
*
* @example
* {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true}
*
* 1:1: Unexpected value `🌍` for `options`, expected `number`
*/
/**
@ -125,9 +157,10 @@
/// <reference types="mdast-util-mdx" />
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit'
import {SKIP, visit} from 'unist-util-visit'
const remarkLintMaximumLineLength = lintRule(
{
@ -145,16 +178,29 @@ const remarkLintMaximumLineLength = lintRule(
function (tree, file, options) {
const value = String(file)
const lines = value.split(/\r?\n/)
const option = options || 80
let expected = 80
// Allow nodes that cannot be wrapped.
visit(tree, function (node) {
if (options === null || options === undefined) {
// Empty.
} else if (typeof options === 'number') {
expected = options
} else {
file.fail(
'Unexpected value `' + options + '` for `options`, expected `number`'
)
}
// eslint-disable-next-line complexity
visit(tree, function (node, index, parent) {
// Allow nodes that cannot be wrapped.
if (
node.type === 'code' ||
node.type === 'definition' ||
node.type === 'heading' ||
node.type === 'html' ||
node.type === 'mdxJsxTextElement' ||
node.type === 'math' ||
node.type === 'mdxjsEsm' ||
node.type === 'mdxFlowExpression' ||
node.type === 'mdxTextExpression' ||
node.type === 'table' ||
// @ts-expect-error: TOML from frontmatter.
@ -165,44 +211,52 @@ const remarkLintMaximumLineLength = lintRule(
const start = pointStart(node)
if (end && start) {
allowList(start.line - 1, end.line)
let line = start.line - 1
while (line < end.line) {
lines[line++] = ''
}
}
return SKIP
}
})
// Allow text spans to cross the border.
visit(tree, function (node, index, parent) {
const final = pointEnd(node)
const initial = pointStart(node)
// Allow text spans to cross the border.
if (
(node.type === 'image' ||
node.type === 'inlineCode' ||
node.type === 'link') &&
initial &&
final &&
parent &&
typeof index === 'number'
node.type === 'image' ||
node.type === 'inlineCode' ||
node.type === 'link'
) {
// Not allowing when starting after the border, or ending before it.
if (initial.column > option || final.column < option) {
return
const end = pointEnd(node)
const start = pointStart(node)
if (end && start && parent && typeof index === 'number') {
// Not allowing when starting after the border.
if (start.column > expected) return
// Not allowing when ending before it.
if (end.column < expected) return
const next = parent.children[index + 1]
const nextStart = pointStart(next)
// Not allowing when 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
while (++index < lines.length) {
const lineLength = lines[index].length
const actualBytes = lines[index].length
const actualCharacters = Array.from(lines[index]).length
const difference = actualCharacters - expected
if (lineLength > option) {
file.message('Line must be at most ' + option + ' characters', {
line: index + 1,
column: lineLength + 1
})
}
}
/**
* Allowlist from `initial` to `final`, zero-based.
*
* @param {number} initial
* Initial line.
* @param {number} final
* Final line.
* @returns {undefined}
* Nothing.
*/
function allowList(initial, final) {
while (initial < final) {
lines[initial++] = ''
if (difference > 0) {
file.message(
'Unexpected `' +
actualCharacters +
'` character line, expected at most `' +
expected +
'` characters, remove `' +
difference +
'` ' +
pluralize('character', difference),
{
line: index + 1,
column: actualBytes + 1
}
)
}
}
}

View File

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

View File

@ -151,38 +151,21 @@ Whether to wrap prose or not is a stylistic choice.
###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
This line is simply not toooooooooooooooooooooooooooooooooooooooooooo
long.
Mercury mercury mercury mercury mercury mercury mercury mercury mercury mercury
mercury.
This is also fine: <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)
| 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>
[foo]: http://localhost/mercury/mercury/mercury/mercury/mercury/mercury/mercury/mercury
```
###### Out
@ -191,35 +174,123 @@ No messages.
##### `not-ok.md`
When configured with `80`.
When configured with `20`.
###### In
```markdown
This line is simply not tooooooooooooooooooooooooooooooooooooooooooooooooooooooo
long.
Mercury mercury mercury
mercury.
Just like thiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiis one.
Mercury mercury mercury `mercury()`.
And this one is also very wrong: because the link starts aaaaaaafter the column: <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
```text
4:86: Line must be at most 80 characters
6:99: Line must be at most 80 characters
8:96: Line must be at most 80 characters
10:97: Line must be at most 80 characters
12:99: Line must be at most 80 characters
1:24: Unexpected `23` character line, expected at most `20` characters, remove `3` characters
4:37: Unexpected `36` character line, expected at most `20` characters, remove `16` characters
6:44: Unexpected `43` character line, expected at most `20` characters, remove `23` characters
8:42: Unexpected `41` character line, expected at most `20` characters, remove `21` characters
10:43: Unexpected `42` character line, expected at most `20` characters, remove `22` characters
12:37: Unexpected `36` character line, expected at most `20` characters, remove `16` characters
14:28: Unexpected `27` character line, expected at most `20` characters, remove `7` characters
16:26: Unexpected `25` character line, expected at most `20` characters, remove `5` characters
18:27: Unexpected `26` character line, expected at most `20` characters, remove `6` characters
20:43: Unexpected `42` character line, expected at most `20` characters, remove `22` characters
```
##### `ok.md`
When configured with `20`.
###### In
> 👉 **Note**: this example uses
> frontmatter ([`remark-frontmatter`][github-remark-frontmatter]).
```markdown
---
description: Mercury mercury mercury mercury.
---
```
###### Out
No messages.
##### `ok.md`
When configured with `20`.
###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
| Mercury | Mercury | Mercury |
| ------- | ------- | ------- |
```
###### Out
No messages.
##### `ok.md`
When configured with `20`.
###### In
> 👉 **Note**: this example uses
> math ([`remark-math`][github-remark-math]).
```markdown
$$
L = \frac{1}{2} \rho v^2 S C_L
$$
```
###### Out
No messages.
##### `ok.md`
When configured with `20`.
###### In
> 👉 **Note**: this example uses
> MDX ([`remark-mdx`][github-remark-mdx]).
```mdx
export const description = 'Mercury mercury mercury mercury.'
{description}
```
###### Out
No messages.
##### `ok-mixed-line-endings.md`
When configured with `10`.
@ -247,10 +318,20 @@ When configured with `10`.
###### Out
```text
1:13: Line must be at most 10 characters
2:13: Line must be at most 10 characters
3:12: Line must be at most 10 characters
4:12: Line must be at most 10 characters
1:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
2:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
3:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character
4:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character
```
##### `not-ok.md`
When configured with `'🌍'`.
###### Out
```text
1:1: Unexpected value `🌍` for `options`, expected `number`
```
## Compatibility
@ -322,10 +403,16 @@ abide by its terms.
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[github-remark-frontmatter]: https://github.com/remarkjs/remark-frontmatter
[github-remark-gfm]: https://github.com/remarkjs/remark-gfm
[github-remark-lint]: https://github.com/remarkjs/remark-lint
[github-remark-math]: https://github.com/remarkjs/remark-math
[github-remark-mdx]: https://mdxjs.com/packages/remark-mdx/
[github-unified-transformer]: https://github.com/unifiedjs/unified#transformer
[npm-install]: https://docs.npmjs.com/cli/install

View File

@ -45,50 +45,82 @@
* @example
* {"name": "ok.md"}
*
* > Foo
* > bar
* > baz.
* > Mercury,
* > Venus,
* > and Earth.
*
* Mars.
*
* @example
* {"name": "ok-tabs.md"}
*
* >Foo
* >bar
* >baz.
* >Mercury,
* >Venus,
* >and Earth.
*
* @example
* {"name": "not-ok.md", "label": "input"}
* {"label": "input", "name": "not-ok.md"}
*
* > Foo
* bar
* > baz.
* > Mercury,
* Venus,
* > and Earth.
* @example
* {"label": "output", "name": "not-ok.md"}
*
* 2:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker
*
* @example
* {"name": "not-ok.md", "label": "output"}
* {"label": "input", "name": "not-ok-tabs.md"}
*
* 2:1: Missing marker in block quote
* >Mercury,
* Venus,
* and Earth.
* @example
* {"label": "output", "name": "not-ok-tabs.md"}
*
* 2:2: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker
* 3:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker
*
* @example
* {"name": "not-ok-tabs.md", "label": "input"}
* {"label": "input", "name": "containers.md"}
*
* >Foo
* bar
* baz.
* * > Mercury and
* Venus.
*
* > * Mercury and
* Venus.
*
* * > * Mercury and
* Venus.
*
* > * > Mercury and
* Venus.
*
* ***
*
* > * > Mercury and
* > Venus.
* @example
* {"name": "not-ok-tabs.md", "label": "output"}
* {"label": "output", "name": "containers.md"}
*
* 2:1: Missing marker in block quote
* 3:1: Missing marker in block quote
* 2:1: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker
* 5:3: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker
* 8:5: Unexpected `0` block quote markers before paragraph line, expected `1` marker, add `1` marker
* 11:7: Unexpected `0` block quote markers before paragraph line, expected `2` markers, add `2` markers
* 16:7: Unexpected `1` block quote marker before paragraph line, expected `2` markers, add `1` marker
*/
/**
* @typedef {import('mdast').Root} Root
*/
/// <reference types="mdast-util-directive" />
import {ok as assert} from 'devlop'
import pluralize from 'pluralize'
import {lintRule} from 'unified-lint-rule'
import {pointEnd, pointStart} from 'unist-util-position'
import {visit} from 'unist-util-visit'
import {SKIP, visitParents} from 'unist-util-visit-parents'
import {location} from 'vfile-location'
const remarkLintNoBlockquoteWithoutMarker = lintRule(
@ -106,35 +138,81 @@ const remarkLintNoBlockquoteWithoutMarker = lintRule(
const value = String(file)
const loc = location(file)
visit(tree, 'blockquote', function (node) {
let index = -1
// Only paragraphs can be lazy.
visitParents(tree, 'paragraph', function (node, parents) {
let expected = 0
while (++index < node.children.length) {
const child = node.children[index]
const start = pointStart(child)
const end = pointEnd(child)
for (const parent of parents) {
if (parent.type === 'blockquote') {
expected++
}
// All known parents that only use whitespace for indent.
else if (
parent.type === 'containerDirective' ||
parent.type === 'footnoteDefinition' ||
parent.type === 'list' ||
parent.type === 'listItem' ||
parent.type === 'root'
) {
// Empty.
/* c8 ignore next 3 -- exit on unknown nodes. */
} else {
return SKIP
}
}
if (child.type === 'paragraph' && start && end) {
const column = start.column
let line = start.line
if (!expected) return SKIP
// Skip past the first line.
while (++line <= end.line) {
const offset = loc.toOffset({line, column})
const end = pointEnd(node)
const start = pointStart(node)
if (
typeof offset !== 'number' ||
/>[\t ]+$/.test(value.slice(offset - 5, offset))
) {
continue
}
if (!end || !start) return SKIP
// Roughly here.
file.message('Missing marker in block quote', {
line,
column: column - 2
})
let line = start.line
while (++line <= end.line) {
// Skip first line.
const lineStart = loc.toOffset({line, column: 1})
assert(lineStart !== undefined) // Always defined.
let actual = 0
let index = lineStart
while (index < value.length) {
const code = value.charCodeAt(index)
if (code === 9 || code === 32) {
// Fine.
} else if (code === 62 /* `>` */) {
actual++
} else {
break
}
index++
}
const point = loc.toPoint(index)
assert(point) // Always defined.
const difference = expected - actual
// Roughly here.
if (difference) {
file.message(
'Unexpected `' +
actual +
'` block quote ' +
pluralize('marker', actual) +
' before paragraph line, expected `' +
expected +
'` ' +
pluralize('marker', expected) +
', add `' +
difference +
'` ' +
pluralize('marker', difference),
{ancestors: [...parents, node], place: point}
)
}
}
})

View File

@ -33,9 +33,12 @@
],
"dependencies": {
"@types/mdast": "^4.0.0",
"devlop": "^1.0.0",
"mdast-util-directive": "^3.0.0",
"pluralize": "^8.0.0",
"unified-lint-rule": "^2.0.0",
"unist-util-position": "^5.0.0",
"unist-util-visit": "^5.0.0",
"unist-util-visit-parents": "^6.0.0",
"vfile-location": "^5.0.0"
},
"scripts": {},
@ -48,7 +51,8 @@
"xo": {
"prettier": true,
"rules": {
"capitalized-comments": "off"
"capitalized-comments": "off",
"unicorn/prefer-code-point": "off"
}
}
}

View File

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

View File

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

View File

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

View File

@ -150,32 +150,266 @@ It has a `join` option to configure more complex cases.
###### In
```markdown
Foo…␊␊…Bar.
# Planets
Mercury.
Venus.
```
###### Out
No messages.
##### `empty-document.md`
###### Out
No messages.
##### `not-ok.md`
###### In
```markdown
Foo…␊␊␊…Bar␊␊␊
# Planets
Mercury.
Venus.
```
###### Out
```text
4:1: Remove 1 line before node
4:5: Remove 2 lines after node
4:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line
8:1: Unexpected `3` blank lines before node, expected up to `1` blank line, remove `2` blank lines
```
##### `initial.md`
###### In
```markdown
␊Mercury.
```
###### Out
```text
2:1: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line
```
##### `final-one.md`
###### In
```markdown
Mercury.␊
```
###### Out
No messages.
##### `final-more.md`
###### In
```markdown
Mercury.␊␊
```
###### Out
```text
1:9: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line
```
##### `empty-document.md`
###### Out
No messages.
##### `block-quote.md`
###### In
```markdown
> Mercury.
Venus.
>
> Earth.
>
```
###### Out
```text
6:3: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line
6:9: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line
```
##### `directive.md`
###### In
> 👉 **Note**: this example uses
> directives ([`remark-directive`][github-remark-directive]).
```markdown
:::mercury
Venus.
Earth.
:::
```
###### Out
```text
5:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line
```
##### `footnote.md`
###### In
> 👉 **Note**: this example uses
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
[^x]:
Mercury.
Venus.
[^y]:
Earth.
Mars.
```
###### Out
```text
8:5: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line
11:5: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line
```
##### `jsx.md`
###### In
> 👉 **Note**: this example uses
> MDX ([`remark-mdx`][github-remark-mdx]).
```mdx
<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
@ -247,8 +481,14 @@ abide by its terms.
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
[github-remark-directive]: https://github.com/remarkjs/remark-directive
[github-remark-gfm]: https://github.com/remarkjs/remark-gfm
[github-remark-lint]: https://github.com/remarkjs/remark-lint
[github-remark-mdx]: https://mdxjs.com/packages/remark-mdx/
[github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify
[github-unified-transformer]: https://github.com/unifiedjs/unified#transformer

View File

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

View File

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

View File

@ -140,8 +140,8 @@ identifiers.
###### In
```markdown
[alpha]: alpha.com
[bravo]: bravo.com
[mercury]: https://example.com/mercury/
[venus]: https://example.com/venus/
```
###### Out
@ -153,14 +153,14 @@ No messages.
###### In
```markdown
[alpha]: alpha.com
[bravo]: alpha.com
[mercury]: https://example.com/mercury/
[venus]: https://example.com/mercury/
```
###### Out
```text
2:1-2:19: Do not use different definitions with the same URL (1:1)
2:1-2:38: Unexpected definition with an already defined URL (as `mercury`), expected unique URLs
```
## Compatibility

View File

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

View File

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

View File

@ -143,8 +143,8 @@ Its a mistake when the same identifier is defined multiple times.
###### In
```markdown
[foo]: bar
[baz]: qux
[mercury]: https://example.com/mercury/
[venus]: https://example.com/venus/
```
###### Out
@ -156,14 +156,14 @@ No messages.
###### In
```markdown
[foo]: bar
[foo]: qux
[mercury]: https://example.com/mercury/
[mercury]: https://example.com/venus/
```
###### Out
```text
2:1-2:11: Do not use definitions with the same identifier (1:1)
2:1-2:38: Unexpected definition with an already defined identifier (`mercury`), expected unique identifiers
```
##### `gfm.md`
@ -174,16 +174,20 @@ No messages.
> GFM ([`remark-gfm`][github-remark-gfm]).
```markdown
GFM footnote definitions are checked too[^a].
Mercury[^mercury].
[^a]: alpha
[^a]: bravo
[^mercury]:
Mercury is the first planet from the Sun and the smallest in the Solar
System.
[^mercury]:
Venus is the second planet from the Sun.
```
###### Out
```text
4:1-4:12: Do not use footnote definitions with the same identifier (3:1)
7:1-7:12: Unexpected footnote definition with an already defined identifier (`mercury`), expected unique identifiers
```
## Compatibility

View File

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

View File

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

View File

@ -141,21 +141,19 @@ section.
###### In
```markdown
## Alpha
# Planets
### Bravo
## Venus
## Charlie
### Discovery
### Bravo
## Mars
### Delta
### Discovery
#### Bravo
### Phobos
#### Echo
##### Bravo
#### Discovery
```
###### Out
@ -167,39 +165,41 @@ No messages.
###### In
```markdown
## Foxtrot
# Planets
### Golf
## Mars
### Golf
### Discovery
### Discovery
```
###### Out
```text
5:1-5:9: Do not use headings with similar content per section (3:1)
7:1-7:14: Unexpected heading with equivalent text in section, expected unique headings
```
##### `not-ok-tolerant-heading-increment.md`
##### `tolerant-heading-increment.md`
###### In
```markdown
# Alpha
# Planets
#### Bravo
#### Discovery
###### Charlie
###### Phobos
#### Bravo
#### Discovery
###### Delta
###### Deimos
```
###### Out
```text
7:1-7:11: Do not use headings with similar content per section (3:1)
7:1-7:15: Unexpected heading with equivalent text in section, expected unique headings
```
##### `mdx.mdx`
@ -212,14 +212,16 @@ No messages.
```mdx
MDX is supported <em>too</em>.
<h2>Alpha</h2>
<h2>Alpha</h2>
<h1>Planets</h1>
<h2>Mars</h2>
<h3>Discovery</h3>
<h3>Discovery</h3>
```
###### Out
```text
4:1-4:15: Do not use headings with similar content per section (3:1)
6:1-6:19: Unexpected heading with equivalent text in section, expected unique headings
```
## Compatibility

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,13 +30,14 @@
* @author Titus Wormer
* @copyright 2015 Titus Wormer
* @license MIT
*
* @example
* {"name": "plug-ins.md"}
*
* @example
* {"name": "plug--ins.md", "label": "output", "positionless": true}
*
* 1:1: Do not use consecutive dashes in a file name
* 1:1: Unexpected consecutive dashes in a file name, expected `-`
*/
/**
@ -58,7 +59,7 @@ const remarkLintNoFileNameConsecutiveDashes = lintRule(
*/
function (_, file) {
if (file.stem && /-{2,}/.test(file.stem)) {
file.message('Do not use consecutive dashes in a file name')
file.message('Unexpected consecutive dashes in a file name, expected `-`')
}
}
)

View File

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

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