mirror of
https://github.com/remarkjs/remark-lint.git
synced 2024-08-17 07:40:31 +03:00
Add better messages, rewrite and improve rules
This commit is contained in:
parent
ccea69188b
commit
45aeac273a
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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": {
|
||||
|
@ -171,36 +171,48 @@ Due to this, it’s recommended to configure this rule with `2`.
|
||||
|
||||
## Examples
|
||||
|
||||
##### `ok.md`
|
||||
|
||||
When configured with `4`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
> Hello
|
||||
|
||||
Paragraph.
|
||||
|
||||
> World
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `ok.md`
|
||||
##### `ok-2.md`
|
||||
|
||||
When configured with `2`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
> Hello
|
||||
> Mercury.
|
||||
|
||||
Paragraph.
|
||||
Venus.
|
||||
|
||||
> World
|
||||
> Earth.
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `ok-4.md`
|
||||
|
||||
When configured with `4`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
> Mercury.
|
||||
|
||||
Venus.
|
||||
|
||||
> Earth.
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `ok-tab.md`
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
>␉Mercury.
|
||||
```
|
||||
|
||||
###### Out
|
||||
@ -212,22 +224,32 @@ No messages.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
> Hello
|
||||
> Mercury.
|
||||
|
||||
Paragraph.
|
||||
Venus.
|
||||
|
||||
> World
|
||||
> Earth.
|
||||
|
||||
Paragraph.
|
||||
Mars.
|
||||
|
||||
> World
|
||||
> Jupiter
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
5:5: Remove 1 space between block quote and content
|
||||
9:3: Add 1 space between block quote and content
|
||||
5:5: Unexpected `4` spaces between block quote marker and content, expected `3` spaces, remove `1` space
|
||||
9:3: Unexpected `2` spaces between block quote marker and content, expected `3` spaces, add `1` space
|
||||
```
|
||||
|
||||
##### `not-ok-options.md`
|
||||
|
||||
When configured with `'🌍'`.
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1: Unexpected value `🌍` for `options`, expected `number` or `'consistent'`
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
@ -10,6 +10,8 @@
|
||||
*
|
||||
* You can use this package to check that the style of GFM tasklists is
|
||||
* consistent.
|
||||
* Task lists are a GFM feature enabled with
|
||||
* [`remark-gfm`][github-remark-gfm].
|
||||
*
|
||||
* ## API
|
||||
*
|
||||
@ -63,6 +65,7 @@
|
||||
* [api-options]: #options
|
||||
* [api-remark-lint-checkbox-character-style]: #unifieduseremarklintcheckboxcharacterstyle-options
|
||||
* [api-styles]: #styles
|
||||
* [github-remark-gfm]: https://github.com/remarkjs/remark-gfm
|
||||
* [github-remark-stringify]: https://github.com/remarkjs/remark/tree/main/packages/remark-stringify
|
||||
* [github-unified-transformer]: https://github.com/unifiedjs/unified#transformer
|
||||
*
|
||||
@ -70,55 +73,60 @@
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer
|
||||
* @license MIT
|
||||
* @example
|
||||
* {"name": "ok.md", "config": {"checked": "x"}, "gfm": true}
|
||||
*
|
||||
* - [x] List item
|
||||
* - [x] List item
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok.md", "config": {"checked": "X"}, "gfm": true}
|
||||
* {"config": {"checked": "x"}, "gfm": true, "name": "ok-x.md"}
|
||||
*
|
||||
* - [X] List item
|
||||
* - [X] List item
|
||||
* - [x] Mercury.
|
||||
* - [x] Venus.
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok.md", "config": {"unchecked": " "}, "gfm": true}
|
||||
* {"config": {"checked": "X"}, "gfm": true, "name": "ok-x-upper.md"}
|
||||
*
|
||||
* - [ ] List item
|
||||
* - [ ] List item
|
||||
* - [X] Mercury.
|
||||
* - [X] Venus.
|
||||
*
|
||||
* @example
|
||||
* {"config": {"unchecked": " "}, "gfm": true, "name": "ok-space.md"}
|
||||
*
|
||||
* - [ ] Mercury.
|
||||
* - [ ] Venus.
|
||||
* - [ ]␠␠
|
||||
* - [ ]
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok.md", "config": {"unchecked": "\t"}, "gfm": true}
|
||||
* {"config": {"unchecked": "\t"}, "gfm": true, "name": "ok-tab.md"}
|
||||
*
|
||||
* - [␉] List item
|
||||
* - [␉] List item
|
||||
* - [␉] Mercury.
|
||||
* - [␉] Venus.
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input", "gfm": true}
|
||||
* {"label": "input", "gfm": true, "name": "not-ok-default.md"}
|
||||
*
|
||||
* - [x] List item
|
||||
* - [X] List item
|
||||
* - [ ] List item
|
||||
* - [␉] List item
|
||||
* - [x] Mercury.
|
||||
* - [X] Venus.
|
||||
* - [ ] Earth.
|
||||
* - [␉] Mars.
|
||||
* @example
|
||||
* {"label": "output", "gfm": true, "name": "not-ok-default.md"}
|
||||
*
|
||||
* 2:5: Unexpected checked checkbox value `X`, expected `x`
|
||||
* 4:5: Unexpected unchecked checkbox value `\t`, expected ` `
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output", "gfm": true}
|
||||
* {"config": "🌍", "label": "output", "name": "not-ok-option.md", "positionless": true}
|
||||
*
|
||||
* 2:5: Checked checkboxes should use `x` as a marker
|
||||
* 4:5: Unchecked checkboxes should use ` ` as a marker
|
||||
* 1:1: Unexpected value `🌍` for `options`, expected an object or `'consistent'`
|
||||
*
|
||||
* @example
|
||||
* {"config": {"unchecked": "💩"}, "name": "not-ok.md", "label": "output", "positionless": true, "gfm": true}
|
||||
* {"config": {"unchecked": "🌍"}, "label": "output", "name": "not-ok-option-unchecked.md", "positionless": true}
|
||||
*
|
||||
* 1:1: Incorrect unchecked checkbox marker `💩`: use either `'\t'`, or `' '`
|
||||
* 1:1: Unexpected value `🌍` for `options.unchecked`, expected `'\t'`, `' '`, or `'consistent'`
|
||||
*
|
||||
* @example
|
||||
* {"config": {"checked": "💩"}, "name": "not-ok.md", "label": "output", "positionless": true, "gfm": true}
|
||||
* {"config": {"checked": "🌍"}, "label": "output", "name": "not-ok-option-checked.md", "positionless": true}
|
||||
*
|
||||
* 1:1: Incorrect checked checkbox marker `💩`: use either `'x'`, or `'X'`
|
||||
* 1:1: Unexpected value `🌍` for `options.checked`, expected `'X'`, `'x'`, or `'consistent'`
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -139,7 +147,8 @@
|
||||
|
||||
import {lintRule} from 'unified-lint-rule'
|
||||
import {pointStart} from 'unist-util-position'
|
||||
import {visit} from 'unist-util-visit'
|
||||
import {visitParents} from 'unist-util-visit-parents'
|
||||
import {VFileMessage} from 'vfile-message'
|
||||
|
||||
const remarkLintCheckboxCharacterStyle = lintRule(
|
||||
{
|
||||
@ -156,77 +165,115 @@ const remarkLintCheckboxCharacterStyle = lintRule(
|
||||
*/
|
||||
function (tree, file, options) {
|
||||
const value = String(file)
|
||||
/** @type {'X' | 'x' | 'consistent'} */
|
||||
let checked = 'consistent'
|
||||
/** @type {'\x09' | ' ' | 'consistent'} */
|
||||
let unchecked = 'consistent'
|
||||
/** @type {'X' | 'x' | undefined} */
|
||||
let checkedExpected
|
||||
/** @type {VFileMessage | undefined} */
|
||||
let checkedConsistentCause
|
||||
/** @type {'\t' | ' ' | undefined} */
|
||||
let uncheckedExpected
|
||||
/** @type {VFileMessage | undefined} */
|
||||
let uncheckedConsistentCause
|
||||
|
||||
if (options && typeof options === 'object') {
|
||||
checked = options.checked || 'consistent'
|
||||
unchecked = options.unchecked || 'consistent'
|
||||
}
|
||||
if (options === null || options === undefined || options === 'consistent') {
|
||||
// Empty.
|
||||
} else if (typeof options === 'object') {
|
||||
if (options.checked === 'X' || options.checked === 'x') {
|
||||
checkedExpected = options.checked
|
||||
} else if (options.checked && options.checked !== 'consistent') {
|
||||
file.fail(
|
||||
'Unexpected value `' +
|
||||
options.checked +
|
||||
"` for `options.checked`, expected `'X'`, `'x'`, or `'consistent'`"
|
||||
)
|
||||
}
|
||||
|
||||
if (unchecked !== 'consistent' && unchecked !== ' ' && unchecked !== '\t') {
|
||||
if (options.unchecked === '\t' || options.unchecked === ' ') {
|
||||
uncheckedExpected = options.unchecked
|
||||
} else if (options.unchecked && options.unchecked !== 'consistent') {
|
||||
file.fail(
|
||||
'Unexpected value `' +
|
||||
options.unchecked +
|
||||
"` for `options.unchecked`, expected `'\\t'`, `' '`, or `'consistent'`"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
file.fail(
|
||||
'Incorrect unchecked checkbox marker `' +
|
||||
unchecked +
|
||||
"`: use either `'\\t'`, or `' '`"
|
||||
'Unexpected value `' +
|
||||
options +
|
||||
"` for `options`, expected an object or `'consistent'`"
|
||||
)
|
||||
}
|
||||
|
||||
if (checked !== 'consistent' && checked !== 'x' && checked !== 'X') {
|
||||
file.fail(
|
||||
'Incorrect checked checkbox marker `' +
|
||||
checked +
|
||||
"`: use either `'x'`, or `'X'`"
|
||||
)
|
||||
}
|
||||
|
||||
visit(tree, 'listItem', function (node) {
|
||||
visitParents(tree, 'listItem', function (node, parents) {
|
||||
const head = node.children[0]
|
||||
const point = pointStart(head)
|
||||
const headStart = pointStart(head)
|
||||
|
||||
// Exit early for items without checkbox.
|
||||
// A list item cannot be checked and empty, according to GFM.
|
||||
if (
|
||||
!point ||
|
||||
!head ||
|
||||
!headStart ||
|
||||
typeof node.checked !== 'boolean' ||
|
||||
typeof point.offset !== 'number'
|
||||
typeof headStart.offset !== 'number'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Move back to before `] `.
|
||||
point.offset -= 2
|
||||
point.column -= 2
|
||||
headStart.offset -= 2
|
||||
headStart.column -= 2
|
||||
|
||||
// Assume we start with a checkbox, because well, `checked` is set.
|
||||
const match = /\[([\t Xx])]/.exec(
|
||||
value.slice(point.offset - 2, point.offset + 1)
|
||||
value.slice(headStart.offset - 2, headStart.offset + 1)
|
||||
)
|
||||
|
||||
/* c8 ignore next 2 -- failsafe so we don’t crash if there actually isn’t
|
||||
* a checkbox. */
|
||||
if (!match) return
|
||||
|
||||
const style = node.checked ? checked : unchecked
|
||||
const actual = match[1]
|
||||
const actualDisplay = actual === '\t' ? '\\t' : actual
|
||||
const expected = node.checked ? checkedExpected : uncheckedExpected
|
||||
const expectedDisplay = expected === '\t' ? '\\t' : expected
|
||||
|
||||
if (!expected) {
|
||||
const cause = new VFileMessage(
|
||||
(node.checked ? 'C' : 'Unc') +
|
||||
"hecked checkbox style `'" +
|
||||
actualDisplay +
|
||||
"'` first defined for `'consistent'` here",
|
||||
{
|
||||
ancestors: [...parents, node],
|
||||
place: headStart,
|
||||
ruleId: 'checkbox-character-style',
|
||||
source: 'remark-lint'
|
||||
}
|
||||
)
|
||||
|
||||
if (style === 'consistent') {
|
||||
if (node.checked) {
|
||||
// @ts-expect-error: valid marker.
|
||||
checked = match[1]
|
||||
checkedExpected = /** @type {'X' | 'x'} */ (actual)
|
||||
checkedConsistentCause = cause
|
||||
} else {
|
||||
// @ts-expect-error: valid marker.
|
||||
unchecked = match[1]
|
||||
uncheckedExpected = /** @type {'\t' | ' '} */ (actual)
|
||||
uncheckedConsistentCause = cause
|
||||
}
|
||||
} else if (match[1] !== style) {
|
||||
} else if (actual !== expected) {
|
||||
file.message(
|
||||
(node.checked ? 'Checked' : 'Unchecked') +
|
||||
' checkboxes should use `' +
|
||||
style +
|
||||
'` as a marker',
|
||||
point
|
||||
'Unexpected ' +
|
||||
(node.checked ? '' : 'un') +
|
||||
'checked checkbox value `' +
|
||||
actualDisplay +
|
||||
'`, expected `' +
|
||||
expectedDisplay +
|
||||
'`',
|
||||
{
|
||||
ancestors: [...parents, node],
|
||||
cause: node.checked
|
||||
? checkedConsistentCause
|
||||
: uncheckedConsistentCause,
|
||||
place: headStart
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -55,38 +55,48 @@
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer
|
||||
* @license MIT
|
||||
* @example
|
||||
* {"name": "ok.md", "gfm": true}
|
||||
*
|
||||
* - [ ] List item
|
||||
* + [x] List Item
|
||||
* * [X] List item
|
||||
* - [ ] List item
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input", "gfm": true}
|
||||
* {"gfm": true, "name": "ok.md"}
|
||||
*
|
||||
* - [ ] List item
|
||||
* + [x] List item
|
||||
* * [X] List item
|
||||
* - [ ] List item
|
||||
* - [ ] Mercury.
|
||||
* + [x] Venus.
|
||||
* * [X] Earth.
|
||||
* - [ ] Mars.
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output", "gfm": true}
|
||||
* {"gfm": true, "label": "input", "name": "not-ok.md"}
|
||||
*
|
||||
* 2:7-2:8: Checkboxes should be followed by a single character
|
||||
* 3:7-3:9: Checkboxes should be followed by a single character
|
||||
* 4:7-4:10: Checkboxes should be followed by a single character
|
||||
* - [ ] Mercury.
|
||||
* + [x] Venus.
|
||||
* * [X] Earth.
|
||||
* - [ ] Mars.
|
||||
* @example
|
||||
* {"gfm": true, "label": "output", "name": "not-ok.md"}
|
||||
*
|
||||
* 2:8: Unexpected `2` spaces between checkbox and content, expected `1` space, remove `1` space
|
||||
* 3:9: Unexpected `3` spaces between checkbox and content, expected `1` space, remove `2` spaces
|
||||
* 4:10: Unexpected `4` spaces between checkbox and content, expected `1` space, remove `3` spaces
|
||||
*
|
||||
* @example
|
||||
* {"gfm": true, "label": "input", "name": "tab.md"}
|
||||
*
|
||||
* - [ ]␉Mercury.
|
||||
* + [x]␉␉Venus.
|
||||
* @example
|
||||
* {"gfm": true, "label": "output", "name": "tab.md"}
|
||||
*
|
||||
* 2:8: Unexpected `2` spaces between checkbox and content, expected `1` space, remove `1` space
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('mdast').Root} Root
|
||||
*/
|
||||
|
||||
import pluralize from 'pluralize'
|
||||
import {lintRule} from 'unified-lint-rule'
|
||||
import {pointStart} from 'unist-util-position'
|
||||
import {visit} from 'unist-util-visit'
|
||||
import {location} from 'vfile-location'
|
||||
import {visitParents} from 'unist-util-visit-parents'
|
||||
|
||||
const remarkLintCheckboxContentIndent = lintRule(
|
||||
{
|
||||
@ -101,45 +111,59 @@ const remarkLintCheckboxContentIndent = lintRule(
|
||||
*/
|
||||
function (tree, file) {
|
||||
const value = String(file)
|
||||
const loc = location(file)
|
||||
|
||||
visit(tree, 'listItem', function (node) {
|
||||
visitParents(tree, 'listItem', function (node, parents) {
|
||||
const head = node.children[0]
|
||||
const point = pointStart(head)
|
||||
const headStart = pointStart(head)
|
||||
|
||||
// Exit early for items without checkbox.
|
||||
// A list item cannot be checked and empty, according to GFM.
|
||||
// A list item cannot be checked and empty according to GFM.
|
||||
if (
|
||||
!point ||
|
||||
!head ||
|
||||
!headStart ||
|
||||
typeof node.checked !== 'boolean' ||
|
||||
typeof point.offset !== 'number'
|
||||
typeof headStart.offset !== 'number'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Assume we start with a checkbox, because well, `checked` is set.
|
||||
// Assume we start with a checkbox as `checked` is set.
|
||||
const match = /\[([\t xX])]/.exec(
|
||||
value.slice(point.offset - 4, point.offset + 1)
|
||||
value.slice(headStart.offset - 4, headStart.offset + 1)
|
||||
)
|
||||
|
||||
/* c8 ignore next -- make sure we don’t crash if there actually isn’t a checkbox. */
|
||||
if (!match) return
|
||||
|
||||
// Move past checkbox.
|
||||
const initial = point.offset
|
||||
let final = initial
|
||||
let final = headStart.offset
|
||||
let code = value.charCodeAt(final)
|
||||
|
||||
while (/[\t ]/.test(value.charAt(final))) final++
|
||||
while (code === 9 || code === 32) {
|
||||
final++
|
||||
code = value.charCodeAt(final)
|
||||
}
|
||||
|
||||
if (final - initial > 0) {
|
||||
const start = loc.toPoint(initial)
|
||||
const end = loc.toPoint(final)
|
||||
const size = final - headStart.offset
|
||||
|
||||
if (size) {
|
||||
file.message(
|
||||
'Checkboxes should be followed by a single character',
|
||||
/* c8 ignore next -- we get here if we have offsets. */
|
||||
start && end ? {start, end} : undefined
|
||||
'Unexpected `' +
|
||||
(size + 1) +
|
||||
'` ' +
|
||||
pluralize('space', size + 1) +
|
||||
' between checkbox and content, expected `1` space, remove `' +
|
||||
size +
|
||||
'` ' +
|
||||
pluralize('space', size),
|
||||
{
|
||||
ancestors: [...parents, node],
|
||||
place: {
|
||||
line: headStart.line,
|
||||
column: headStart.column + size,
|
||||
offset: headStart.offset + size
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -146,7 +146,7 @@ Due to this, it’s recommended to use lowercase and turn this rule on.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[example]: http://example.com "Example Domain"
|
||||
[mercury]: http://example.com "Mercury"
|
||||
```
|
||||
|
||||
###### Out
|
||||
@ -158,13 +158,13 @@ No messages.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Example]: http://example.com "Example Domain"
|
||||
[Mercury]: http://example.com "Mercury"
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1-1:47: Do not use uppercase characters in definition labels
|
||||
1:1-1:40: Unexpected uppercase characters in definition label, expected lowercase
|
||||
```
|
||||
|
||||
##### `gfm.md`
|
||||
@ -175,13 +175,15 @@ No messages.
|
||||
> GFM ([`remark-gfm`][github-remark-gfm]).
|
||||
|
||||
```markdown
|
||||
[^X]: Footnote definitions (from GFM) are checked too.
|
||||
[^Mercury]:
|
||||
**Mercury** is the first planet from the Sun and the smallest
|
||||
in the Solar System.
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1-1:55: Do not use uppercase characters in definition labels
|
||||
1:1-3:25: Unexpected uppercase characters in footnote definition label, expected lowercase
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
@ -41,31 +41,41 @@
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer
|
||||
* @license MIT
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok.md"}
|
||||
*
|
||||
* [example domain]: http://example.com "Example Domain"
|
||||
* [planet mercury]: http://example.com
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input"}
|
||||
* {"label": "input", "name": "not-ok-consecutive.md"}
|
||||
*
|
||||
* [example␠␠␠␠domain]: http://example.com "Example Domain"
|
||||
* [planet␠␠␠␠mercury]: http://example.com
|
||||
* @example
|
||||
* {"label": "output", "name": "not-ok-consecutive.md"}
|
||||
*
|
||||
* 1:1-1:40: Unexpected `4` consecutive spaces in definition label, expected `1` space, remove `3` spaces
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output"}
|
||||
* {"label": "input", "name": "not-ok-non-space.md"}
|
||||
*
|
||||
* 1:1-1:57: Do not use consecutive whitespace in definition labels
|
||||
* [pla␉net␊mer␍cury]: http://e.com
|
||||
* @example
|
||||
* {"label": "output", "name": "not-ok-non-space.md"}
|
||||
*
|
||||
* 1:1-3:20: Unexpected non-space whitespace character `\t` in definition label, expected `1` space, replace it
|
||||
* 1:1-3:20: Unexpected non-space whitespace character `\n` in definition label, expected `1` space, replace it
|
||||
* 1:1-3:20: Unexpected non-space whitespace character `\r` in definition label, expected `1` space, replace it
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('mdast').Root} Root
|
||||
*/
|
||||
|
||||
import {longestStreak} from 'longest-streak'
|
||||
import pluralize from 'pluralize'
|
||||
import {lintRule} from 'unified-lint-rule'
|
||||
import {pointStart, pointEnd} from 'unist-util-position'
|
||||
import {visit} from 'unist-util-visit'
|
||||
|
||||
const label = /^\s*\[((?:\\[\s\S]|[^[\]])+)]/
|
||||
import {visitParents} from 'unist-util-visit-parents'
|
||||
|
||||
const remarkLintDefinitionSpacing = lintRule(
|
||||
{
|
||||
@ -79,27 +89,35 @@ const remarkLintDefinitionSpacing = lintRule(
|
||||
* Nothing.
|
||||
*/
|
||||
function (tree, file) {
|
||||
const value = String(file)
|
||||
visitParents(tree, function (node, parents) {
|
||||
if (node.type === 'definition' && node.position && node.label) {
|
||||
const size = longestStreak(node.label, ' ')
|
||||
|
||||
visit(tree, function (node) {
|
||||
if (node.type === 'definition') {
|
||||
const end = pointEnd(node)
|
||||
const start = pointStart(node)
|
||||
if (size > 1) {
|
||||
file.message(
|
||||
'Unexpected `' +
|
||||
size +
|
||||
'` consecutive spaces in definition label, expected `1` space, remove `' +
|
||||
(size - 1) +
|
||||
'` ' +
|
||||
pluralize('space', size - 1),
|
||||
{ancestors: [...parents, node], place: node.position}
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
end &&
|
||||
start &&
|
||||
typeof end.offset === 'number' &&
|
||||
typeof start.offset === 'number'
|
||||
) {
|
||||
const match = value.slice(start.offset, end.offset).match(label)
|
||||
/** @type {Array<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}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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": {
|
||||
|
@ -150,25 +150,41 @@ Due to this, it’s recommended to use one space and turn this rule on.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[example domain]: http://example.com "Example Domain"
|
||||
[planet mercury]: http://example.com
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `not-ok.md`
|
||||
##### `not-ok-consecutive.md`
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[example␠␠␠␠domain]: http://example.com "Example Domain"
|
||||
[planet␠␠␠␠mercury]: http://example.com
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1-1:57: Do not use consecutive whitespace in definition labels
|
||||
1:1-1:40: Unexpected `4` consecutive spaces in definition label, expected `1` space, remove `3` spaces
|
||||
```
|
||||
|
||||
##### `not-ok-non-space.md`
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[pla␉net␊mer␍cury]: http://e.com
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1-3:20: Unexpected non-space whitespace character `\t` in definition label, expected `1` space, replace it
|
||||
1:1-3:20: Unexpected non-space whitespace character `\n` in definition label, expected `1` space, replace it
|
||||
1:1-3:20: Unexpected non-space whitespace character `\r` in definition label, expected `1` space, replace it
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
@ -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'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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` don’t 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}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -165,8 +165,8 @@ It’s recommended to instead use a certain flag for plain text (such as
|
||||
###### In
|
||||
|
||||
````markdown
|
||||
```alpha
|
||||
bravo()
|
||||
```markdown
|
||||
# Mercury
|
||||
```
|
||||
````
|
||||
|
||||
@ -180,17 +180,17 @@ No messages.
|
||||
|
||||
````markdown
|
||||
```
|
||||
alpha()
|
||||
mercury()
|
||||
```
|
||||
````
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1-3:4: Missing code language flag
|
||||
1:1-3:4: Unexpected missing fenced code language flag in info string, expected keyword
|
||||
```
|
||||
|
||||
##### `ok.md`
|
||||
##### `ok-allow-empty.md`
|
||||
|
||||
When configured with `{ allowEmpty: true }`.
|
||||
|
||||
@ -198,7 +198,7 @@ When configured with `{ allowEmpty: true }`.
|
||||
|
||||
````markdown
|
||||
```
|
||||
alpha()
|
||||
mercury()
|
||||
```
|
||||
````
|
||||
|
||||
@ -206,7 +206,7 @@ alpha()
|
||||
|
||||
No messages.
|
||||
|
||||
##### `not-ok.md`
|
||||
##### `not-ok-allow-empty.md`
|
||||
|
||||
When configured with `{ allowEmpty: false }`.
|
||||
|
||||
@ -214,25 +214,25 @@ When configured with `{ allowEmpty: false }`.
|
||||
|
||||
````markdown
|
||||
```
|
||||
alpha()
|
||||
mercury()
|
||||
```
|
||||
````
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1-3:4: Missing code language flag
|
||||
1:1-3:4: Unexpected missing fenced code language flag in info string, expected keyword
|
||||
```
|
||||
|
||||
##### `ok.md`
|
||||
##### `ok-array.md`
|
||||
|
||||
When configured with `[ 'alpha' ]`.
|
||||
When configured with `[ 'markdown' ]`.
|
||||
|
||||
###### In
|
||||
|
||||
````markdown
|
||||
```alpha
|
||||
bravo()
|
||||
```markdown
|
||||
# Mercury
|
||||
```
|
||||
````
|
||||
|
||||
@ -240,15 +240,15 @@ bravo()
|
||||
|
||||
No messages.
|
||||
|
||||
##### `ok.md`
|
||||
##### `ok-options.md`
|
||||
|
||||
When configured with `{ flags: [ 'alpha' ] }`.
|
||||
When configured with `{ flags: [ 'markdown' ] }`.
|
||||
|
||||
###### In
|
||||
|
||||
````markdown
|
||||
```alpha
|
||||
bravo()
|
||||
```markdown
|
||||
# Mercury
|
||||
```
|
||||
````
|
||||
|
||||
@ -256,22 +256,50 @@ bravo()
|
||||
|
||||
No messages.
|
||||
|
||||
##### `not-ok.md`
|
||||
##### `not-ok-array.md`
|
||||
|
||||
When configured with `[ 'charlie' ]`.
|
||||
When configured with `[ 'markdown' ]`.
|
||||
|
||||
###### In
|
||||
|
||||
````markdown
|
||||
```alpha
|
||||
bravo()
|
||||
```javascript
|
||||
mercury()
|
||||
```
|
||||
````
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1-3:4: Incorrect code language flag
|
||||
1:1-3:4: Unexpected fenced code language flag `javascript` in info string, expected `markdown`
|
||||
```
|
||||
|
||||
##### `not-ok-long-array.md`
|
||||
|
||||
When configured with `[ 'javascript', 'markdown', 'mdx', 'typescript' ]`.
|
||||
|
||||
###### In
|
||||
|
||||
````markdown
|
||||
```html
|
||||
<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
|
||||
|
@ -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'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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/
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -48,7 +48,8 @@
|
||||
"xo": {
|
||||
"prettier": true,
|
||||
"rules": {
|
||||
"capitalized-comments": "off"
|
||||
"capitalized-comments": "off",
|
||||
"unicorn/prefer-code-point": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,8 +148,8 @@ Due to this, it’s recommended to turn this rule on.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
Lorem ipsum␠␠
|
||||
dolor sit amet
|
||||
**Mercury** is the first planet from the Sun␠␠
|
||||
and the smallest in the Solar System.
|
||||
```
|
||||
|
||||
###### Out
|
||||
@ -161,14 +161,33 @@ No messages.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
Lorem ipsum␠␠␠
|
||||
dolor sit amet.
|
||||
**Mercury** is the first planet from the Sun␠␠␠
|
||||
and the smallest in the Solar System.
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:12-2:1: Use two spaces for hard line breaks
|
||||
1:45-2:1: Unexpected `3` spaces for hard break, expected `2` spaces
|
||||
```
|
||||
|
||||
##### `containers.md`
|
||||
|
||||
###### In
|
||||
|
||||
> 👉 **Note**: this example uses
|
||||
> GFM ([`remark-gfm`][github-remark-gfm]).
|
||||
|
||||
```markdown
|
||||
[^mercury]:
|
||||
> * > * **Mercury** is the first planet from the Sun␠␠␠
|
||||
> > and the smallest in the Solar System.
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
2:57-3:1: Unexpected `3` spaces for hard break, expected `2` spaces
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
@ -240,6 +259,8 @@ abide by its terms.
|
||||
|
||||
[github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
|
||||
|
||||
[github-remark-gfm]: https://github.com/remarkjs/remark-gfm
|
||||
|
||||
[github-remark-lint]: https://github.com/remarkjs/remark-lint
|
||||
|
||||
[github-unified-transformer]: https://github.com/unifiedjs/unified#transformer
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -153,9 +153,25 @@ it’s recommended that this rule is turned on.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
# Alpha
|
||||
# Mercury
|
||||
|
||||
## Bravo
|
||||
## Nomenclature
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `also-ok.md`
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
#### Impact basins and craters
|
||||
|
||||
#### Plains
|
||||
|
||||
#### Compressional features
|
||||
```
|
||||
|
||||
###### Out
|
||||
@ -167,15 +183,23 @@ No messages.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
# Charlie
|
||||
# Mercury
|
||||
|
||||
### Delta
|
||||
### Internal structure
|
||||
|
||||
### Surface geology
|
||||
|
||||
## Observation history
|
||||
|
||||
#### Mariner 10
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
3:1-3:10: Heading levels should increment by one level at a time
|
||||
3:1-3:23: Unexpected heading rank `3`, exected rank `2`
|
||||
5:1-5:20: Unexpected heading rank `3`, exected rank `2`
|
||||
9:1-9:16: Unexpected heading rank `4`, exected rank `3`
|
||||
```
|
||||
|
||||
##### `html.md`
|
||||
@ -183,16 +207,23 @@ No messages.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
In markdown, <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
|
||||
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -55,7 +55,7 @@
|
||||
*
|
||||
* ## Fix
|
||||
*
|
||||
* [`remark-stringify`][github-remark-stringify] always uses Unix linebreaks.
|
||||
* [`remark-stringify`][github-remark-stringify] always uses Unix line endings.
|
||||
*
|
||||
* [api-options]: #options
|
||||
* [api-remark-lint-linebreak-style]: #unifieduseremarklintlinebreakstyle-options
|
||||
@ -67,35 +67,56 @@
|
||||
* @author Titus Wormer
|
||||
* @copyright 2017 Titus Wormer
|
||||
* @license MIT
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok-consistent-as-windows.md"}
|
||||
*
|
||||
* Alpha␍␊Bravo␍␊
|
||||
* Mercury␍␊and␍␊Venus.
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok-consistent-as-unix.md"}
|
||||
*
|
||||
* Alpha␊Bravo␊
|
||||
* Mercury␊and␊Venus.
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok-unix.md", "label": "input", "config": "unix", "positionless": true}
|
||||
* {"config": "unix", "label": "input", "name": "not-ok-unix.md", "positionless": true}
|
||||
*
|
||||
* Alpha␍␊
|
||||
* Mercury.␍␊
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok-unix.md", "label": "output", "config": "unix"}
|
||||
* {"config": "unix", "label": "output", "name": "not-ok-unix.md", "positionless": true}
|
||||
*
|
||||
* 1:7: Expected linebreaks to be unix (`\n`), not windows (`\r\n`)
|
||||
* 1:10: Unexpected windows (`\r\n`) line ending, expected unix (`\n`) line endings
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok-windows.md", "label": "input", "config": "windows", "positionless": true}
|
||||
* {"config": "windows", "label": "input", "name": "not-ok-windows.md", "positionless": true}
|
||||
*
|
||||
* Alpha␊
|
||||
* Mercury.␊
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok-windows.md", "label": "output", "config": "windows"}
|
||||
* {"config": "windows", "label": "output", "name": "not-ok-windows.md", "positionless": true}
|
||||
*
|
||||
* 1:6: Expected linebreaks to be windows (`\r\n`), not unix (`\n`)
|
||||
* 1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
|
||||
*
|
||||
* @example
|
||||
* {"config": "🌍", "label": "output", "name": "not-ok-options.md", "positionless": true}
|
||||
*
|
||||
* 1:1: Unexpected value `🌍` for `options`, expected `'unix'`, `'windows'`, or `'consistent'`
|
||||
*
|
||||
* @example
|
||||
* {"config": "windows", "label": "input", "name": "many.md", "positionless": true}
|
||||
*
|
||||
* Mercury.␊Venus.␊Earth.␊Mars.␊Jupiter.␊Saturn.␊Uranus.␊Neptune.␊
|
||||
*
|
||||
* @example
|
||||
* {"config": "windows", "label": "output", "name": "many.md", "positionless": true}
|
||||
*
|
||||
* 1:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
|
||||
* 2:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
|
||||
* 3:7: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
|
||||
* 4:6: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
|
||||
* 5:9: Unexpected unix (`\n`) line ending, expected windows (`\r\n`) line endings
|
||||
* 6:8: Unexpected large number of incorrect line endings, stopping
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -110,10 +131,12 @@
|
||||
* Styles.
|
||||
*/
|
||||
|
||||
import {ok as assert} from 'devlop'
|
||||
import {lintRule} from 'unified-lint-rule'
|
||||
import {location} from 'vfile-location'
|
||||
import {VFileMessage} from 'vfile-message'
|
||||
|
||||
const escaped = {unix: '\\n', windows: '\\r\\n'}
|
||||
const max = 5
|
||||
|
||||
const remarkLintLinebreakStyle = lintRule(
|
||||
{
|
||||
@ -129,28 +152,60 @@ const remarkLintLinebreakStyle = lintRule(
|
||||
* Nothing.
|
||||
*/
|
||||
function (_, file, options) {
|
||||
let option = options || 'consistent'
|
||||
const value = String(file)
|
||||
const toPoint = location(value).toPoint
|
||||
let index = value.indexOf('\n')
|
||||
/** @type {VFileMessage | undefined} */
|
||||
let cause
|
||||
/** @type {Style | undefined} */
|
||||
let expected
|
||||
|
||||
if (options === null || options === undefined || options === 'consistent') {
|
||||
// Empty.
|
||||
} else if (options === 'unix' || options === 'windows') {
|
||||
expected = options
|
||||
} else {
|
||||
file.fail(
|
||||
'Unexpected value `' +
|
||||
options +
|
||||
"` for `options`, expected `'unix'`, `'windows'`, or `'consistent'`"
|
||||
)
|
||||
}
|
||||
|
||||
let messages = 0
|
||||
|
||||
while (index !== -1) {
|
||||
const type = value.charAt(index - 1) === '\r' ? 'windows' : 'unix'
|
||||
const actual = value.charAt(index - 1) === '\r' ? 'windows' : 'unix'
|
||||
const place = toPoint(index)
|
||||
assert(place) // Always defined.
|
||||
|
||||
if (option === 'consistent') {
|
||||
option = type
|
||||
} else if (option !== type) {
|
||||
file.message(
|
||||
'Expected linebreaks to be ' +
|
||||
option +
|
||||
' (`' +
|
||||
escaped[option] +
|
||||
'`), not ' +
|
||||
type +
|
||||
' (`' +
|
||||
escaped[type] +
|
||||
'`)',
|
||||
toPoint(index)
|
||||
if (expected) {
|
||||
if (expected !== actual) {
|
||||
if (messages === max) {
|
||||
file.info(
|
||||
'Unexpected large number of incorrect line endings, stopping',
|
||||
{place}
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
file.message(
|
||||
'Unexpected ' +
|
||||
displayStyle(actual) +
|
||||
' line ending, expected ' +
|
||||
displayStyle(expected) +
|
||||
' line endings',
|
||||
{cause, place}
|
||||
)
|
||||
messages++
|
||||
}
|
||||
} else {
|
||||
expected = actual
|
||||
cause = new VFileMessage(
|
||||
'Line ending style ' +
|
||||
displayStyle(expected) +
|
||||
" first defined for `'consistent'` here",
|
||||
{place, ruleId: 'linebreak-style', source: 'remark-lint'}
|
||||
)
|
||||
}
|
||||
|
||||
@ -160,3 +215,13 @@ const remarkLintLinebreakStyle = lintRule(
|
||||
)
|
||||
|
||||
export default remarkLintLinebreakStyle
|
||||
|
||||
/**
|
||||
* @param {Style} style
|
||||
* Style.
|
||||
* @returns {string}
|
||||
* Display.
|
||||
*/
|
||||
function displayStyle(style) {
|
||||
return style === 'unix' ? 'unix (`\\n`)' : 'windows (`\\r\\n`)'
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -3,7 +3,8 @@
|
||||
*
|
||||
* ## What is this?
|
||||
*
|
||||
* This package checks the style of link title markers.
|
||||
* This package checks the style of link (*and* image and definition) title
|
||||
* markers.
|
||||
*
|
||||
* ## When should I use this?
|
||||
*
|
||||
@ -59,7 +60,7 @@
|
||||
*
|
||||
* ## Fix
|
||||
*
|
||||
* [`remark-stringify`][github-remark-stringify] formats titles with double
|
||||
* [`remark-stringify`][github-remark-stringify] formats titles with double
|
||||
* quotes by default.
|
||||
* Pass `quote: "'"` to use single quotes.
|
||||
* There is no option to use parens.
|
||||
@ -74,82 +75,90 @@
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer
|
||||
* @license MIT
|
||||
* @example
|
||||
* {"name": "ok.md", "config": "\""}
|
||||
*
|
||||
* [Example](http://example.com#without-title)
|
||||
* [Example](http://example.com "Example Domain")
|
||||
* ![Example](http://example.com "Example Domain")
|
||||
*
|
||||
* [Example]: http://example.com "Example Domain"
|
||||
*
|
||||
* You can use parens in URLs if they’re not a title (see GH-166):
|
||||
*
|
||||
* [Example](#Heading-(optional))
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input", "config": "\""}
|
||||
* {"name": "ok-consistent.md"}
|
||||
*
|
||||
* [Example]: http://example.com 'Example Domain'
|
||||
* [Mercury](http://example.com/mercury/),
|
||||
* [Venus](http://example.com/venus/ "Go to Venus"), and
|
||||
* ![Earth](http://example.com/earth/ "Go to Earth").
|
||||
*
|
||||
* [Mars]: http://example.com/mars/ "Go to Mars"
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output", "config": "\""}
|
||||
* {"label": "input", "name": "not-ok-consistent.md"}
|
||||
*
|
||||
* 1:31-1:47: Titles should use `"` as a quote
|
||||
* [Mercury](http://example.com/mercury/ "Go to Mercury") and
|
||||
* ![Venus](http://example.com/venus/ 'Go to Venus').
|
||||
*
|
||||
* [Earth]: http://example.com/earth/ (Go to Earth)
|
||||
* @example
|
||||
* {"label": "output", "name": "not-ok-consistent.md"}
|
||||
*
|
||||
* 2:1-2:50: Unexpected title markers `'`, expected `"`
|
||||
* 4:1-4:49: Unexpected title markers `'('` and `')'`, expected `"`
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok.md", "config": "'"}
|
||||
* {"config": "\"", "name": "ok-double.md"}
|
||||
*
|
||||
* [Example](http://example.com#without-title)
|
||||
* [Example](http://example.com 'Example Domain')
|
||||
* ![Example](http://example.com 'Example Domain')
|
||||
*
|
||||
* [Example]: http://example.com 'Example Domain'
|
||||
* [Mercury](http://example.com/mercury/ "Go to Mercury").
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input", "config": "'"}
|
||||
* {"config": "\"", "label": "input", "name": "not-ok-double.md"}
|
||||
*
|
||||
* [Example]: http://example.com "Example Domain"
|
||||
* [Mercury](http://example.com/mercury/ 'Go to Mercury').
|
||||
* @example
|
||||
* {"config": "\"", "label": "output", "name": "not-ok-double.md"}
|
||||
*
|
||||
* 1:1-1:55: Unexpected title markers `'`, expected `"`
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output", "config": "'"}
|
||||
* {"config": "'", "name": "ok-single.md"}
|
||||
*
|
||||
* 1:31-1:47: Titles should use `'` as a quote
|
||||
* [Mercury](http://example.com/mercury/ 'Go to Mercury').
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok.md", "config": "()"}
|
||||
* {"config": "'", "label": "input", "name": "not-ok-single.md"}
|
||||
*
|
||||
* [Example](http://example.com#without-title)
|
||||
* [Example](http://example.com (Example Domain))
|
||||
* ![Example](http://example.com (Example Domain))
|
||||
* [Mercury](http://example.com/mercury/ "Go to Mercury").
|
||||
* @example
|
||||
* {"config": "'", "label": "output", "name": "not-ok-single.md"}
|
||||
*
|
||||
* [Example]: http://example.com (Example Domain)
|
||||
* 1:1-1:55: Unexpected title markers `"`, expected `'`
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input", "config": "()"}
|
||||
* {"config": "()", "name": "ok-paren.md"}
|
||||
*
|
||||
* [Example](http://example.com 'Example Domain')
|
||||
* [Mercury](http://example.com/mercury/ (Go to Mercury)).
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output", "config": "()"}
|
||||
* {"config": "()", "label": "input", "name": "not-ok-paren.md"}
|
||||
*
|
||||
* 1:30-1:46: Titles should use `()` as a quote
|
||||
* [Mercury](http://example.com/mercury/ "Go to Mercury").
|
||||
* @example
|
||||
* {"config": "()", "label": "output", "name": "not-ok-paren.md"}
|
||||
*
|
||||
* 1:1-1:55: Unexpected title markers `"`, expected `'('` and `')'`
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input"}
|
||||
* {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true}
|
||||
*
|
||||
* [Example](http://example.com "Example Domain")
|
||||
* [Example](http://example.com 'Example Domain')
|
||||
* 1:1: Unexpected value `🌍` for `options`, expected `'"'`, `"'"`, `'()'`, or `'consistent'`
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output"}
|
||||
* {"config": "\"", "name": "ok-parens-in-url.md"}
|
||||
*
|
||||
* 2:30-2:46: Titles should use `"` as a quote
|
||||
* Parens in URLs work correctly:
|
||||
*
|
||||
* [Mercury](http://example.com/(mercury) "Go to Mercury") and
|
||||
* [Venus](http://example.com/(venus)).
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "config": "💩", "label": "output", "positionless": true}
|
||||
* {"config": "\"", "name": "ok-whitespace.md"}
|
||||
*
|
||||
* 1:1: Incorrect link title style marker `💩`: use either `'consistent'`, `'"'`, `'\''`, or `'()'`
|
||||
* Trailing whitespace works correctly:
|
||||
*
|
||||
* [Mercury](http://example.com/mercury/␠"Go to Mercury"␠).
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -165,15 +174,9 @@
|
||||
*/
|
||||
|
||||
import {lintRule} from 'unified-lint-rule'
|
||||
import {pointEnd, pointStart} from 'unist-util-position'
|
||||
import {visit} from 'unist-util-visit'
|
||||
import {location} from 'vfile-location'
|
||||
|
||||
const markers = {
|
||||
'"': '"',
|
||||
"'": "'",
|
||||
')': '('
|
||||
}
|
||||
import {pointEnd} from 'unist-util-position'
|
||||
import {visitParents} from 'unist-util-visit-parents'
|
||||
import {VFileMessage} from 'vfile-message'
|
||||
|
||||
const remarkLintLinkTitleStyle = lintRule(
|
||||
{
|
||||
@ -190,78 +193,88 @@ const remarkLintLinkTitleStyle = lintRule(
|
||||
*/
|
||||
function (tree, file, options) {
|
||||
const value = String(file)
|
||||
const loc = location(file)
|
||||
const option = options || 'consistent'
|
||||
// @ts-expect-error: allow `(` too, even though untyped.
|
||||
let look = option === '()' || option === '(' ? ')' : option
|
||||
/** @type {Style | undefined} */
|
||||
let expected
|
||||
/** @type {VFileMessage | undefined} */
|
||||
let cause
|
||||
|
||||
if (look !== 'consistent' && !Object.hasOwn(markers, look)) {
|
||||
if (options === null || options === undefined || options === 'consistent') {
|
||||
// Empty.
|
||||
/* c8 ignore next 3 */
|
||||
// @ts-expect-error: to do: remove.
|
||||
} else if (options === '(') {
|
||||
expected = '()'
|
||||
} else if (options === '"' || options === "'" || options === '()') {
|
||||
expected = options
|
||||
} else {
|
||||
file.fail(
|
||||
'Incorrect link title style marker `' +
|
||||
look +
|
||||
"`: use either `'consistent'`, `'\"'`, `'\\''`, or `'()'`"
|
||||
'Unexpected value `' +
|
||||
options +
|
||||
"` for `options`, expected `'\"'`, `\"'\"`, `'()'`, or `'consistent'`"
|
||||
)
|
||||
}
|
||||
|
||||
visit(tree, function (node) {
|
||||
visitParents(tree, function (node, parents) {
|
||||
if (
|
||||
node.type === 'definition' ||
|
||||
node.type === 'image' ||
|
||||
node.type === 'link'
|
||||
) {
|
||||
const tail =
|
||||
'children' in node
|
||||
? node.children[node.children.length - 1]
|
||||
: undefined
|
||||
const begin = tail ? pointEnd(tail) : pointStart(node)
|
||||
// Exit w/o title.
|
||||
if (!node.title) return
|
||||
|
||||
const end = pointEnd(node)
|
||||
let endIndex = end ? end.offset : undefined
|
||||
|
||||
if (
|
||||
!begin ||
|
||||
!end ||
|
||||
typeof begin.offset !== 'number' ||
|
||||
typeof end.offset !== 'number'
|
||||
) {
|
||||
return
|
||||
// Exit w/o position.
|
||||
if (!endIndex) return
|
||||
|
||||
// `)`
|
||||
if (node.type !== 'definition') endIndex--
|
||||
|
||||
// Whitespace.
|
||||
let before = value.charCodeAt(endIndex - 1)
|
||||
while (before === 9 || before === 32) {
|
||||
endIndex--
|
||||
before = value.charCodeAt(endIndex - 1)
|
||||
}
|
||||
|
||||
let last = end.offset - 1
|
||||
/** @type {Style | undefined} */
|
||||
const actual =
|
||||
before === 34 /* `"` */
|
||||
? '"'
|
||||
: before === 39 /* `'` */
|
||||
? "'"
|
||||
: before === 41 /* `)` */
|
||||
? '()'
|
||||
: /* c8 ignore next -- we should find a correct marker. */
|
||||
undefined
|
||||
|
||||
if (node.type !== 'definition') {
|
||||
last--
|
||||
}
|
||||
/* c8 ignore next -- we should find a correct marker. */
|
||||
if (!actual) return
|
||||
|
||||
const final = /** @type {keyof markers} */ (value.charAt(last))
|
||||
|
||||
// Exit if the final marker is not a known marker.
|
||||
if (!(final in markers)) {
|
||||
return
|
||||
}
|
||||
|
||||
const initial = markers[final]
|
||||
|
||||
// Find the starting delimiter
|
||||
const first = value.lastIndexOf(initial, last - 1)
|
||||
|
||||
// Exit if there’s no starting delimiter, the starting delimiter is before
|
||||
// the start of the node, or if it’s not preceded by whitespace.
|
||||
if (first <= begin.offset || !/\s/.test(value.charAt(first - 1))) {
|
||||
return
|
||||
}
|
||||
|
||||
if (look === 'consistent') {
|
||||
look = final
|
||||
} else if (look !== final) {
|
||||
const start = loc.toPoint(first)
|
||||
const end = loc.toPoint(last + 1)
|
||||
/* c8 ignore next -- we get here if we have offsets. */
|
||||
const place = start && end ? {start, end} : undefined
|
||||
|
||||
file.message(
|
||||
'Titles should use `' +
|
||||
(look === ')' ? '()' : look) +
|
||||
'` as a quote',
|
||||
place
|
||||
if (expected) {
|
||||
if (actual !== expected) {
|
||||
file.message(
|
||||
'Unexpected title markers ' +
|
||||
displayStyle(actual) +
|
||||
', expected ' +
|
||||
displayStyle(expected),
|
||||
{ancestors: [...parents, node], cause, place: node.position}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
expected = actual
|
||||
cause = new VFileMessage(
|
||||
'Title marker style ' +
|
||||
displayStyle(expected) +
|
||||
" first defined for `'consistent'` here",
|
||||
{
|
||||
ancestors: [...parents, node],
|
||||
place: node.position,
|
||||
ruleId: 'link-title-style',
|
||||
source: 'remark-lint'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -270,3 +283,13 @@ const remarkLintLinkTitleStyle = lintRule(
|
||||
)
|
||||
|
||||
export default remarkLintLinkTitleStyle
|
||||
|
||||
/**
|
||||
* @param {Style} style
|
||||
* Style.
|
||||
* @returns {string}
|
||||
* Display.
|
||||
*/
|
||||
function displayStyle(style) {
|
||||
return style === '"' ? '`"`' : style === "'" ? "`'`" : "`'('` and `')'`"
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,8 @@
|
||||
|
||||
## What is this?
|
||||
|
||||
This package checks the style of link title markers.
|
||||
This package checks the style of link (*and* image and definition) title
|
||||
markers.
|
||||
|
||||
## When should I use this?
|
||||
|
||||
@ -174,143 +175,179 @@ markdown, so it’s recommended to configure this rule with `'"'`.
|
||||
|
||||
## Fix
|
||||
|
||||
[`remark-stringify`][github-remark-stringify] formats titles with double
|
||||
[`remark-stringify`][github-remark-stringify] formats titles with double
|
||||
quotes by default.
|
||||
Pass `quote: "'"` to use single quotes.
|
||||
There is no option to use parens.
|
||||
|
||||
## Examples
|
||||
|
||||
##### `ok.md`
|
||||
##### `ok-consistent.md`
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Mercury](http://example.com/mercury/),
|
||||
[Venus](http://example.com/venus/ "Go to Venus"), and
|
||||
![Earth](http://example.com/earth/ "Go to Earth").
|
||||
|
||||
[Mars]: http://example.com/mars/ "Go to Mars"
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `not-ok-consistent.md`
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Mercury](http://example.com/mercury/ "Go to Mercury") and
|
||||
![Venus](http://example.com/venus/ 'Go to Venus').
|
||||
|
||||
[Earth]: http://example.com/earth/ (Go to Earth)
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
2:1-2:50: Unexpected title markers `'`, expected `"`
|
||||
4:1-4:49: Unexpected title markers `'('` and `')'`, expected `"`
|
||||
```
|
||||
|
||||
##### `ok-double.md`
|
||||
|
||||
When configured with `'"'`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Example](http://example.com#without-title)
|
||||
[Example](http://example.com "Example Domain")
|
||||
![Example](http://example.com "Example Domain")
|
||||
|
||||
[Example]: http://example.com "Example Domain"
|
||||
|
||||
You can use parens in URLs if they’re not a title (see GH-166):
|
||||
|
||||
[Example](#Heading-(optional))
|
||||
[Mercury](http://example.com/mercury/ "Go to Mercury").
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `not-ok.md`
|
||||
##### `not-ok-double.md`
|
||||
|
||||
When configured with `'"'`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Example]: http://example.com 'Example Domain'
|
||||
[Mercury](http://example.com/mercury/ 'Go to Mercury').
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:31-1:47: Titles should use `"` as a quote
|
||||
1:1-1:55: Unexpected title markers `'`, expected `"`
|
||||
```
|
||||
|
||||
##### `ok.md`
|
||||
##### `ok-single.md`
|
||||
|
||||
When configured with `"'"`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Example](http://example.com#without-title)
|
||||
[Example](http://example.com 'Example Domain')
|
||||
![Example](http://example.com 'Example Domain')
|
||||
|
||||
[Example]: http://example.com 'Example Domain'
|
||||
[Mercury](http://example.com/mercury/ 'Go to Mercury').
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `not-ok.md`
|
||||
##### `not-ok-single.md`
|
||||
|
||||
When configured with `"'"`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Example]: http://example.com "Example Domain"
|
||||
[Mercury](http://example.com/mercury/ "Go to Mercury").
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:31-1:47: Titles should use `'` as a quote
|
||||
1:1-1:55: Unexpected title markers `"`, expected `'`
|
||||
```
|
||||
|
||||
##### `ok.md`
|
||||
##### `ok-paren.md`
|
||||
|
||||
When configured with `'()'`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Example](http://example.com#without-title)
|
||||
[Example](http://example.com (Example Domain))
|
||||
![Example](http://example.com (Example Domain))
|
||||
|
||||
[Example]: http://example.com (Example Domain)
|
||||
[Mercury](http://example.com/mercury/ (Go to Mercury)).
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `not-ok.md`
|
||||
##### `not-ok-paren.md`
|
||||
|
||||
When configured with `'()'`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Example](http://example.com 'Example Domain')
|
||||
[Mercury](http://example.com/mercury/ "Go to Mercury").
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:30-1:46: Titles should use `()` as a quote
|
||||
1:1-1:55: Unexpected title markers `"`, expected `'('` and `')'`
|
||||
```
|
||||
|
||||
##### `not-ok.md`
|
||||
|
||||
When configured with `'🌍'`.
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1: Unexpected value `🌍` for `options`, expected `'"'`, `"'"`, `'()'`, or `'consistent'`
|
||||
```
|
||||
|
||||
##### `ok-parens-in-url.md`
|
||||
|
||||
When configured with `'"'`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[Example](http://example.com "Example Domain")
|
||||
[Example](http://example.com 'Example Domain')
|
||||
Parens in URLs work correctly:
|
||||
|
||||
[Mercury](http://example.com/(mercury) "Go to Mercury") and
|
||||
[Venus](http://example.com/(venus)).
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
2:30-2:46: Titles should use `"` as a quote
|
||||
No messages.
|
||||
|
||||
##### `ok-whitespace.md`
|
||||
|
||||
When configured with `'"'`.
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
Trailing whitespace works correctly:
|
||||
|
||||
[Mercury](http://example.com/mercury/␠"Go to Mercury"␠).
|
||||
```
|
||||
|
||||
##### `not-ok.md`
|
||||
|
||||
When configured with `'💩'`.
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1: Incorrect link title style marker `💩`: use either `'consistent'`, `'"'`, `'\''`, or `'()'`
|
||||
```
|
||||
No messages.
|
||||
|
||||
## Compatibility
|
||||
|
||||
|
@ -30,9 +30,9 @@
|
||||
* While it is possible to use an indent to align ordered lists on their marker:
|
||||
*
|
||||
* ```markdown
|
||||
* 1. One
|
||||
* 10. Ten
|
||||
* 100. Hundred
|
||||
* 1. Mercury
|
||||
* 10. Venus
|
||||
* 100. Earth
|
||||
* ```
|
||||
*
|
||||
* …such a style is uncommon and hard to maintain as adding a 10th item
|
||||
@ -56,34 +56,33 @@
|
||||
* @example
|
||||
* {"name": "ok.md"}
|
||||
*
|
||||
* Paragraph.
|
||||
* Mercury.
|
||||
*
|
||||
* * List item
|
||||
* * List item
|
||||
* * Venus.
|
||||
* * Earth.
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input"}
|
||||
* {"label": "input", "name": "not-ok.md"}
|
||||
*
|
||||
* Paragraph.
|
||||
* Mercury.
|
||||
*
|
||||
* ␠* List item
|
||||
* ␠* List item
|
||||
* ␠* Venus.
|
||||
* ␠* Earth.
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output"}
|
||||
* {"label": "output", "name": "not-ok.md"}
|
||||
*
|
||||
* 3:2: Incorrect indentation before bullet: remove 1 space
|
||||
* 4:2: Incorrect indentation before bullet: remove 1 space
|
||||
* 3:2: Unexpected `1` space before list item, expected `0` spaces, remove them
|
||||
* 4:2: Unexpected `1` space before list item, expected `0` spaces, remove them
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('mdast').Root} Root
|
||||
*/
|
||||
|
||||
import plural from 'pluralize'
|
||||
import pluralize from 'pluralize'
|
||||
import {lintRule} from 'unified-lint-rule'
|
||||
import {pointStart} from 'unist-util-position'
|
||||
import {visit} from 'unist-util-visit'
|
||||
|
||||
const remarkLintListItemBulletIndent = lintRule(
|
||||
{
|
||||
@ -97,34 +96,37 @@ const remarkLintListItemBulletIndent = lintRule(
|
||||
* Nothing.
|
||||
*/
|
||||
function (tree, file) {
|
||||
visit(tree, 'list', function (list, _, grandparent) {
|
||||
let index = -1
|
||||
const pointStartGrandparent = pointStart(grandparent)
|
||||
const treeStart = pointStart(tree)
|
||||
|
||||
while (++index < list.children.length) {
|
||||
const item = list.children[index]
|
||||
const itemStart = pointStart(item)
|
||||
// Unknown containers are not supported.
|
||||
if (!tree || tree.type !== 'root' || !treeStart) return
|
||||
|
||||
if (
|
||||
grandparent &&
|
||||
pointStartGrandparent &&
|
||||
itemStart &&
|
||||
grandparent.type === 'root'
|
||||
) {
|
||||
const indent = itemStart.column - pointStartGrandparent.column
|
||||
for (const child of tree.children) {
|
||||
if (child.type !== 'list') continue
|
||||
|
||||
if (indent) {
|
||||
file.message(
|
||||
'Incorrect indentation before bullet: remove ' +
|
||||
indent +
|
||||
' ' +
|
||||
plural('space', indent),
|
||||
itemStart
|
||||
)
|
||||
}
|
||||
const list = child
|
||||
|
||||
for (const item of list.children) {
|
||||
const place = pointStart(item)
|
||||
|
||||
/* c8 ignore next 2 -- doesn’t happen in tests as the whole tree is
|
||||
* generated. */
|
||||
if (!place) continue
|
||||
|
||||
const actual = place.column - treeStart.column
|
||||
|
||||
if (actual) {
|
||||
file.message(
|
||||
'Unexpected `' +
|
||||
actual +
|
||||
'` ' +
|
||||
pluralize('space', actual) +
|
||||
' before list item, expected `0` spaces, remove them',
|
||||
{ancestors: [tree, list, item], place}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -5,6 +5,8 @@
|
||||
* ## What is this?
|
||||
*
|
||||
* This package checks the indent of list item content.
|
||||
* It checks the first thing in a list item and makes sure that all other
|
||||
* children have the same indent.
|
||||
*
|
||||
* ## When should I use this?
|
||||
*
|
||||
@ -42,32 +44,76 @@
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer
|
||||
* @license MIT
|
||||
* @example
|
||||
* {"name": "ok.md", "gfm": true}
|
||||
*
|
||||
* 1.␠[x] Alpha
|
||||
* ␠␠␠1. Bravo
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input", "gfm": true}
|
||||
* {"name": "ok.md"}
|
||||
*
|
||||
* 1.␠[x] Charlie
|
||||
* ␠␠␠␠1. Delta
|
||||
* 1.␠Mercury.
|
||||
* ␠␠␠***
|
||||
* ␠␠␠* Venus.
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output", "gfm": true}
|
||||
* {"label": "input", "name": "not-ok.md"}
|
||||
*
|
||||
* 2:5: Don’t use mixed indentation for children, remove 1 space
|
||||
* 1.␠Mercury.
|
||||
* ␠␠␠␠␠***
|
||||
* ␠␠␠␠* Venus.
|
||||
* @example
|
||||
* {"label": "output", "name": "not-ok.md"}
|
||||
*
|
||||
* 2:6: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces
|
||||
* 3:5: Unexpected unaligned list item child, expected to align with first child, remove `1` space
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok-more.md"}
|
||||
*
|
||||
* *␠␠␠Mercury.
|
||||
* ␠␠␠␠***
|
||||
*
|
||||
* @example
|
||||
* {"label": "input", "name": "not-ok-more.md"}
|
||||
*
|
||||
* *␠␠␠Mercury.
|
||||
* ␠␠␠␠␠␠***
|
||||
* @example
|
||||
* {"label": "output", "name": "not-ok-more.md"}
|
||||
*
|
||||
* 2:7: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces
|
||||
*
|
||||
* @example
|
||||
* {"label": "input", "gfm": true, "name": "gfm-nok.md"}
|
||||
*
|
||||
* 1.␠[x] Mercury
|
||||
* ␠␠␠␠␠***
|
||||
* ␠␠␠␠* Venus
|
||||
* @example
|
||||
* {"label": "output", "gfm": true, "name": "gfm-nok.md"}
|
||||
*
|
||||
* 2:6: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces
|
||||
* 3:5: Unexpected unaligned list item child, expected to align with first child, remove `1` space
|
||||
*
|
||||
* @example
|
||||
* {"label": "input", "name": "initial-blank.md"}
|
||||
*
|
||||
* *
|
||||
* ␠␠␠␠␠asd
|
||||
*
|
||||
* ␠␠***
|
||||
* @example
|
||||
* {"label": "output", "name": "initial-blank.md"}
|
||||
*
|
||||
* 4:3: Unexpected unaligned list item child, expected to align with first child, add `3` spaces
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('mdast').Root} Root
|
||||
*/
|
||||
|
||||
import plural from 'pluralize'
|
||||
import pluralize from 'pluralize'
|
||||
import {lintRule} from 'unified-lint-rule'
|
||||
import {pointStart} from 'unist-util-position'
|
||||
import {visit} from 'unist-util-visit'
|
||||
import {visitParents} from 'unist-util-visit-parents'
|
||||
import {VFileMessage} from 'vfile-message'
|
||||
|
||||
const remarkLintListItemContentIndent = lintRule(
|
||||
{
|
||||
@ -82,57 +128,67 @@ const remarkLintListItemContentIndent = lintRule(
|
||||
*/
|
||||
function (tree, file) {
|
||||
const value = String(file)
|
||||
/** @type {VFileMessage | undefined} */
|
||||
let cause
|
||||
|
||||
visit(tree, 'listItem', function (node) {
|
||||
visitParents(tree, 'listItem', function (node, parents) {
|
||||
let index = -1
|
||||
/** @type {number | undefined} */
|
||||
let style
|
||||
let expected
|
||||
|
||||
while (++index < node.children.length) {
|
||||
const item = node.children[index]
|
||||
const begin = pointStart(item)
|
||||
const child = node.children[index]
|
||||
const childStart = pointStart(child)
|
||||
|
||||
if (!begin || typeof begin.offset !== 'number') {
|
||||
if (!childStart || typeof childStart.offset !== 'number') {
|
||||
continue
|
||||
}
|
||||
|
||||
let column = begin.column
|
||||
let actual = childStart.column
|
||||
|
||||
// Get indentation for the first child.
|
||||
// Only the first item can have a checkbox, so here we remove that from
|
||||
// the column.
|
||||
if (index === 0) {
|
||||
// If there’s a checkbox before the content, look backwards to find
|
||||
// the start of that checkbox.
|
||||
if (typeof node.checked === 'boolean') {
|
||||
let char = begin.offset - 1
|
||||
// Only the first item can have a checkbox,
|
||||
// when it’s a paragraph,
|
||||
// so here we remove that from the column.
|
||||
if (index === 0 && typeof node.checked === 'boolean') {
|
||||
let beforeIndex = childStart.offset - 1
|
||||
|
||||
while (char > 0 && value.charAt(char) !== '[') {
|
||||
char--
|
||||
}
|
||||
|
||||
column -= begin.offset - char
|
||||
while (
|
||||
beforeIndex > 0 &&
|
||||
value.charCodeAt(beforeIndex) !== 91 /* `[` */
|
||||
) {
|
||||
beforeIndex--
|
||||
}
|
||||
|
||||
style = column
|
||||
|
||||
continue
|
||||
actual -= childStart.offset - beforeIndex
|
||||
}
|
||||
|
||||
// Warn for violating children.
|
||||
if (style && column !== style) {
|
||||
const diff = style - column
|
||||
const abs = Math.abs(diff)
|
||||
if (expected) {
|
||||
// Warn for violating children.
|
||||
if (actual !== expected) {
|
||||
const difference = expected - actual
|
||||
const differenceAbsolute = Math.abs(difference)
|
||||
|
||||
file.message(
|
||||
'Don’t use mixed indentation for children, ' +
|
||||
/* c8 ignore next -- hard to test, I couldn’t find it at least. */
|
||||
(diff > 0 ? 'add' : 'remove') +
|
||||
' ' +
|
||||
abs +
|
||||
' ' +
|
||||
plural('space', abs),
|
||||
{line: begin.line, column}
|
||||
file.message(
|
||||
'Unexpected unaligned list item child, expected to align with first child, ' +
|
||||
(difference > 0 ? 'add' : 'remove') +
|
||||
' `' +
|
||||
differenceAbsolute +
|
||||
'` ' +
|
||||
pluralize('space', differenceAbsolute),
|
||||
{ancestors: [...parents, node, child], cause, place: childStart}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
expected = actual
|
||||
cause = new VFileMessage(
|
||||
'Alignment of first child first defined here',
|
||||
{
|
||||
ancestors: [...parents, node, child],
|
||||
place: childStart,
|
||||
ruleId: 'list-item-content-indent',
|
||||
source: 'remark-lint'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,8 @@ consistent.
|
||||
## What is this?
|
||||
|
||||
This package checks the indent of list item content.
|
||||
It checks the first thing in a list item and makes sure that all other
|
||||
children have the same indent.
|
||||
|
||||
## When should I use this?
|
||||
|
||||
@ -151,12 +153,10 @@ Further children should align with it.
|
||||
|
||||
###### In
|
||||
|
||||
> 👉 **Note**: this example uses
|
||||
> GFM ([`remark-gfm`][github-remark-gfm]).
|
||||
|
||||
```markdown
|
||||
1.␠[x] Alpha
|
||||
␠␠␠1. Bravo
|
||||
1.␠Mercury.
|
||||
␠␠␠***
|
||||
␠␠␠* Venus.
|
||||
```
|
||||
|
||||
###### Out
|
||||
@ -167,18 +167,82 @@ No messages.
|
||||
|
||||
###### In
|
||||
|
||||
> 👉 **Note**: this example uses
|
||||
> GFM ([`remark-gfm`][github-remark-gfm]).
|
||||
|
||||
```markdown
|
||||
1.␠[x] Charlie
|
||||
␠␠␠␠1. Delta
|
||||
1.␠Mercury.
|
||||
␠␠␠␠␠***
|
||||
␠␠␠␠* Venus.
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
2:5: Don’t use mixed indentation for children, remove 1 space
|
||||
2:6: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces
|
||||
3:5: Unexpected unaligned list item child, expected to align with first child, remove `1` space
|
||||
```
|
||||
|
||||
##### `ok-more.md`
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
*␠␠␠Mercury.
|
||||
␠␠␠␠***
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
No messages.
|
||||
|
||||
##### `not-ok-more.md`
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
*␠␠␠Mercury.
|
||||
␠␠␠␠␠␠***
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
2:7: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces
|
||||
```
|
||||
|
||||
##### `gfm-nok.md`
|
||||
|
||||
###### In
|
||||
|
||||
> 👉 **Note**: this example uses
|
||||
> GFM ([`remark-gfm`][github-remark-gfm]).
|
||||
|
||||
```markdown
|
||||
1.␠[x] Mercury
|
||||
␠␠␠␠␠***
|
||||
␠␠␠␠* Venus
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
2:6: Unexpected unaligned list item child, expected to align with first child, remove `2` spaces
|
||||
3:5: Unexpected unaligned list item child, expected to align with first child, remove `1` space
|
||||
```
|
||||
|
||||
##### `initial-blank.md`
|
||||
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
*
|
||||
␠␠␠␠␠asd
|
||||
|
||||
␠␠***
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
4:3: Unexpected unaligned list item child, expected to align with first child, add `3` spaces
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
@ -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}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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 | can’t | 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}
|
||||
*
|
||||
* 0123456789␍␊0123456789␊01234␍␊01234␊
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok-mixed-line-endings.md", "config": 10, "label": "input", "positionless": true}
|
||||
* {"config": 10, "label": "input", "name": "not-ok-mixed-line-endings.md", "positionless": true}
|
||||
*
|
||||
* 012345678901␍␊012345678901␊01234567890␍␊01234567890␊
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok-mixed-line-endings.md", "config": 10, "label": "output", "positionless": true}
|
||||
* {"config": 10, "label": "output", "name": "not-ok-mixed-line-endings.md", "positionless": true}
|
||||
*
|
||||
* 1:13: Line must be at most 10 characters
|
||||
* 2:13: Line must be at most 10 characters
|
||||
* 3:12: Line must be at most 10 characters
|
||||
* 4:12: Line must be at most 10 characters
|
||||
* 1:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
|
||||
* 2:13: Unexpected `12` character line, expected at most `10` characters, remove `2` characters
|
||||
* 3:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character
|
||||
* 4:12: Unexpected `11` character line, expected at most `10` characters, remove `1` character
|
||||
*
|
||||
* @example
|
||||
* {"config": "🌍", "label": "output", "name": "not-ok.md", "positionless": true}
|
||||
*
|
||||
* 1:1: Unexpected value `🌍` for `options`, expected `number`
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -125,9 +157,10 @@
|
||||
|
||||
/// <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 there’s a following child.
|
||||
if (
|
||||
next &&
|
||||
nextStart &&
|
||||
nextStart.line === start.line &&
|
||||
// Either something with children:
|
||||
(!('value' in next) ||
|
||||
// Or with whitespace:
|
||||
/[ \t]/.test(next.value))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
let line = start.line - 1
|
||||
while (line < end.line) {
|
||||
lines[line++] = ''
|
||||
}
|
||||
}
|
||||
|
||||
const next = parent.children[index + 1]
|
||||
const nextStart = pointStart(next)
|
||||
|
||||
// Not allowing when there’s whitespace after the link.
|
||||
if (
|
||||
next &&
|
||||
nextStart &&
|
||||
nextStart.line === initial.line &&
|
||||
(!('value' in next) || /^(.+?[ \t].+?)/.test(next.value))
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
allowList(initial.line - 1, final.line)
|
||||
}
|
||||
})
|
||||
|
||||
@ -210,29 +264,25 @@ const remarkLintMaximumLineLength = lintRule(
|
||||
let index = -1
|
||||
|
||||
while (++index < lines.length) {
|
||||
const lineLength = lines[index].length
|
||||
const actualBytes = lines[index].length
|
||||
const actualCharacters = Array.from(lines[index]).length
|
||||
const difference = actualCharacters - expected
|
||||
|
||||
if (lineLength > option) {
|
||||
file.message('Line must be at most ' + option + ' characters', {
|
||||
line: index + 1,
|
||||
column: lineLength + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowlist from `initial` to `final`, zero-based.
|
||||
*
|
||||
* @param {number} initial
|
||||
* Initial line.
|
||||
* @param {number} final
|
||||
* Final line.
|
||||
* @returns {undefined}
|
||||
* Nothing.
|
||||
*/
|
||||
function allowList(initial, final) {
|
||||
while (initial < final) {
|
||||
lines[initial++] = ''
|
||||
if (difference > 0) {
|
||||
file.message(
|
||||
'Unexpected `' +
|
||||
actualCharacters +
|
||||
'` character line, expected at most `' +
|
||||
expected +
|
||||
'` characters, remove `' +
|
||||
difference +
|
||||
'` ' +
|
||||
pluralize('character', difference),
|
||||
{
|
||||
line: index + 1,
|
||||
column: actualBytes + 1
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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 | can’t | 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
|
||||
|
@ -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}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -41,37 +41,207 @@
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer
|
||||
* @license MIT
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok.md"}
|
||||
*
|
||||
* Foo…␊␊…Bar.
|
||||
* # Planets
|
||||
*
|
||||
* Mercury.
|
||||
*
|
||||
* Venus.
|
||||
*
|
||||
* @example
|
||||
* {"label": "input", "name": "not-ok.md"}
|
||||
*
|
||||
* # Planets
|
||||
*
|
||||
*
|
||||
* Mercury.
|
||||
*
|
||||
*
|
||||
*
|
||||
* Venus.
|
||||
* @example
|
||||
* {"label": "output", "name": "not-ok.md"}
|
||||
*
|
||||
* 4:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line
|
||||
* 8:1: Unexpected `3` blank lines before node, expected up to `1` blank line, remove `2` blank lines
|
||||
*
|
||||
* @example
|
||||
* {"label": "input", "name": "initial.md"}
|
||||
*
|
||||
* ␊Mercury.
|
||||
* @example
|
||||
* {"label": "output", "name": "initial.md"}
|
||||
*
|
||||
* 2:1: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line
|
||||
*
|
||||
* @example
|
||||
* {"name": "final-one.md"}
|
||||
*
|
||||
* Mercury.␊
|
||||
*
|
||||
* @example
|
||||
* {"label": "input", "name": "final-more.md"}
|
||||
*
|
||||
* Mercury.␊␊
|
||||
* @example
|
||||
* {"label": "output", "name": "final-more.md"}
|
||||
*
|
||||
* 1:9: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line
|
||||
*
|
||||
* @example
|
||||
* {"name": "empty-document.md"}
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input"}
|
||||
* {"label": "input", "name": "block-quote.md"}
|
||||
*
|
||||
* Foo…␊␊␊…Bar␊␊␊
|
||||
* > Mercury.
|
||||
*
|
||||
* Venus.
|
||||
*
|
||||
* >
|
||||
* > Earth.
|
||||
* >
|
||||
* @example
|
||||
* {"label": "output", "name": "block-quote.md"}
|
||||
*
|
||||
* 6:3: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line
|
||||
* 6:9: Unexpected `1` blank line after node, expected `0` blank lines, remove `1` blank line
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output"}
|
||||
* {"directive": true, "label": "input", "name": "directive.md"}
|
||||
*
|
||||
* 4:1: Remove 1 line before node
|
||||
* 4:5: Remove 2 lines after node
|
||||
* :::mercury
|
||||
* Venus.
|
||||
*
|
||||
*
|
||||
* Earth.
|
||||
* :::
|
||||
* @example
|
||||
* {"directive": true, "label": "output", "name": "directive.md"}
|
||||
*
|
||||
* 5:1: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line
|
||||
*
|
||||
* @example
|
||||
* {"gfm": true, "label": "input", "name": "footnote.md"}
|
||||
*
|
||||
* [^x]:
|
||||
* Mercury.
|
||||
*
|
||||
* Venus.
|
||||
*
|
||||
* [^y]:
|
||||
*
|
||||
* Earth.
|
||||
*
|
||||
*
|
||||
* Mars.
|
||||
* @example
|
||||
* {"gfm": true, "label": "output", "name": "footnote.md"}
|
||||
*
|
||||
* 8:5: Unexpected `1` blank line before node, expected `0` blank lines, remove `1` blank line
|
||||
* 11:5: Unexpected `2` blank lines before node, expected up to `1` blank line, remove `1` blank line
|
||||
*
|
||||
* @example
|
||||
* {"label": "input", "mdx": true, "name": "jsx.md"}
|
||||
*
|
||||
* <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}
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -143,8 +143,8 @@ It’s a mistake when the same identifier is defined multiple times.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[foo]: bar
|
||||
[baz]: qux
|
||||
[mercury]: https://example.com/mercury/
|
||||
[venus]: https://example.com/venus/
|
||||
```
|
||||
|
||||
###### Out
|
||||
@ -156,14 +156,14 @@ No messages.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[foo]: bar
|
||||
[foo]: qux
|
||||
[mercury]: https://example.com/mercury/
|
||||
[mercury]: https://example.com/venus/
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
2:1-2:11: Do not use definitions with the same identifier (1:1)
|
||||
2:1-2:38: Unexpected definition with an already defined identifier (`mercury`), expected unique identifiers
|
||||
```
|
||||
|
||||
##### `gfm.md`
|
||||
@ -174,16 +174,20 @@ No messages.
|
||||
> GFM ([`remark-gfm`][github-remark-gfm]).
|
||||
|
||||
```markdown
|
||||
GFM footnote definitions are checked too[^a].
|
||||
Mercury[^mercury].
|
||||
|
||||
[^a]: alpha
|
||||
[^a]: bravo
|
||||
[^mercury]:
|
||||
Mercury is the first planet from the Sun and the smallest in the Solar
|
||||
System.
|
||||
|
||||
[^mercury]:
|
||||
Venus is the second planet from the Sun.
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
4:1-4:12: Do not use footnote definitions with the same identifier (3:1)
|
||||
7:1-7:12: Unexpected footnote definition with an already defined identifier (`mercury`), expected unique identifiers
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -38,38 +38,40 @@
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer
|
||||
* @license MIT
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok.md"}
|
||||
*
|
||||
* # Foo
|
||||
* # Mercury
|
||||
*
|
||||
* Bar.
|
||||
* **Mercury** is the first planet from the Sun and the smallest in the Solar
|
||||
* System.
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input"}
|
||||
* {"label": "input", "name": "not-ok.md"}
|
||||
*
|
||||
* *Foo*
|
||||
* **Mercury**
|
||||
*
|
||||
* Bar.
|
||||
* **Mercury** is the first planet from the Sun and the smallest in the Solar
|
||||
* System.
|
||||
*
|
||||
* __Qux__
|
||||
*
|
||||
* Quux.
|
||||
* *Venus*
|
||||
*
|
||||
* **Venus** is the second planet from the Sun.
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output"}
|
||||
* {"label": "output", "name": "not-ok.md"}
|
||||
*
|
||||
* 1:1-1:6: Don’t use emphasis to introduce a section, use a heading
|
||||
* 5:1-5:8: Don’t use emphasis to introduce a section, use a heading
|
||||
* 1:1-1:12: Unexpected strong introducing a section, expected a heading instead
|
||||
* 6:1-6:8: Unexpected emphasis introducing a section, expected a heading instead
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('mdast').Root} Root
|
||||
* @typedef {import('mdast').RootContent} RootContent
|
||||
*/
|
||||
|
||||
import {lintRule} from 'unified-lint-rule'
|
||||
import {visit} from 'unist-util-visit'
|
||||
import {position} from 'unist-util-position'
|
||||
import {visitParents} from 'unist-util-visit-parents'
|
||||
|
||||
const remarkLintNoEmphasisAsHeading = lintRule(
|
||||
{
|
||||
@ -83,31 +85,37 @@ const remarkLintNoEmphasisAsHeading = lintRule(
|
||||
* Nothing.
|
||||
*/
|
||||
function (tree, file) {
|
||||
visit(tree, 'paragraph', function (node, index, parent) {
|
||||
visitParents(tree, 'paragraph', function (node, parents) {
|
||||
const parent = parents.at(-1)
|
||||
|
||||
if (!node.position || !parent) {
|
||||
return
|
||||
}
|
||||
|
||||
// Next sibling needs to be a paragraph.
|
||||
const siblings = /** @type {Array<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(
|
||||
'Don’t use emphasis to introduce a section, use a heading',
|
||||
place
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
file.message(
|
||||
'Unexpected ' +
|
||||
head.type +
|
||||
' introducing a section, expected a heading instead',
|
||||
{ancestors: [...parents, node, head], place: node.position}
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -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": {
|
||||
|
@ -147,9 +147,10 @@ It’s recommended to use actual headings instead.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
# Foo
|
||||
# Mercury
|
||||
|
||||
Bar.
|
||||
**Mercury** is the first planet from the Sun and the smallest in the Solar
|
||||
System.
|
||||
```
|
||||
|
||||
###### Out
|
||||
@ -161,20 +162,21 @@ No messages.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
*Foo*
|
||||
**Mercury**
|
||||
|
||||
Bar.
|
||||
**Mercury** is the first planet from the Sun and the smallest in the Solar
|
||||
System.
|
||||
|
||||
__Qux__
|
||||
*Venus*
|
||||
|
||||
Quux.
|
||||
**Venus** is the second planet from the Sun.
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1-1:6: Don’t use emphasis to introduce a section, use a heading
|
||||
5:1-5:8: Don’t use emphasis to introduce a section, use a heading
|
||||
1:1-1:12: Unexpected strong introducing a section, expected a heading instead
|
||||
6:1-6:8: Unexpected emphasis introducing a section, expected a heading instead
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
@ -38,32 +38,31 @@
|
||||
* @author Titus Wormer
|
||||
* @copyright 2015 Titus Wormer
|
||||
* @license MIT
|
||||
*
|
||||
* @example
|
||||
* {"name": "ok.md"}
|
||||
*
|
||||
* [alpha](http://bravo.com).
|
||||
* [Mercury](http://example.com/mercury/).
|
||||
*
|
||||
* ![charlie](http://delta.com/echo.png "foxtrot").
|
||||
* ![Venus](http://example.com/venus/ "Go to Venus").
|
||||
*
|
||||
* [golf][hotel].
|
||||
*
|
||||
* [india]: http://juliett.com
|
||||
* [earth]: http://example.com/earth/
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "input"}
|
||||
* {"label": "input", "name": "not-ok.md"}
|
||||
*
|
||||
* [alpha]().
|
||||
* [Mercury]().
|
||||
*
|
||||
* ![bravo](#).
|
||||
* ![Venus](#).
|
||||
*
|
||||
* [charlie]: <>
|
||||
* [earth]: <>
|
||||
*
|
||||
* @example
|
||||
* {"name": "not-ok.md", "label": "output"}
|
||||
* {"label": "output", "name": "not-ok.md"}
|
||||
*
|
||||
* 1:1-1:10: Don’t use links without URL
|
||||
* 3:1-3:12: Don’t use images without URL
|
||||
* 5:1-5:14: Don’t use definitions without URL
|
||||
* 1:1-1:12: Unexpected empty link URL referencing the current document, expected URL
|
||||
* 3:1-3:12: Unexpected empty image URL referencing the current document, expected URL
|
||||
* 5:1-5:12: Unexpected empty definition URL referencing the current document, expected URL
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -71,8 +70,7 @@
|
||||
*/
|
||||
|
||||
import {lintRule} from 'unified-lint-rule'
|
||||
import {position} from 'unist-util-position'
|
||||
import {visit} from 'unist-util-visit'
|
||||
import {visitParents} from 'unist-util-visit-parents'
|
||||
|
||||
const remarkLintNoEmptyUrl = lintRule(
|
||||
{
|
||||
@ -86,17 +84,20 @@ const remarkLintNoEmptyUrl = lintRule(
|
||||
* Nothing.
|
||||
*/
|
||||
function (tree, file) {
|
||||
visit(tree, function (node) {
|
||||
const place = position(node)
|
||||
|
||||
visitParents(tree, function (node, parents) {
|
||||
if (
|
||||
(node.type === 'definition' ||
|
||||
node.type === 'image' ||
|
||||
node.type === 'link') &&
|
||||
place &&
|
||||
node.position &&
|
||||
(!node.url || node.url === '#' || node.url === '?')
|
||||
) {
|
||||
file.message('Don’t use ' + node.type + 's without URL', place)
|
||||
file.message(
|
||||
'Unexpected empty ' +
|
||||
node.type +
|
||||
' URL referencing the current document, expected URL',
|
||||
{ancestors: [...parents, node], place: node.position}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -143,13 +143,11 @@ It’s recommended to fill them out.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[alpha](http://bravo.com).
|
||||
[Mercury](http://example.com/mercury/).
|
||||
|
||||
![charlie](http://delta.com/echo.png "foxtrot").
|
||||
![Venus](http://example.com/venus/ "Go to Venus").
|
||||
|
||||
[golf][hotel].
|
||||
|
||||
[india]: http://juliett.com
|
||||
[earth]: http://example.com/earth/
|
||||
```
|
||||
|
||||
###### Out
|
||||
@ -161,19 +159,19 @@ No messages.
|
||||
###### In
|
||||
|
||||
```markdown
|
||||
[alpha]().
|
||||
[Mercury]().
|
||||
|
||||
![bravo](#).
|
||||
![Venus](#).
|
||||
|
||||
[charlie]: <>
|
||||
[earth]: <>
|
||||
```
|
||||
|
||||
###### Out
|
||||
|
||||
```text
|
||||
1:1-1:10: Don’t use links without URL
|
||||
3:1-3:12: Don’t use images without URL
|
||||
5:1-5:14: Don’t use definitions without URL
|
||||
1:1-1:12: Unexpected empty link URL referencing the current document, expected URL
|
||||
3:1-3:12: Unexpected empty image URL referencing the current document, expected URL
|
||||
5:1-5:12: Unexpected empty definition URL referencing the current document, expected URL
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
@ -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'
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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 `-`')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user