eslint-markdown 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/build/configs/all.d.ts +2 -0
- package/build/configs/stylistic.d.ts +1 -0
- package/build/core/ast/escape-string-regexp.d.ts +22 -0
- package/build/core/ast/index.d.ts +2 -1
- package/build/rules/index.d.ts +3 -1
- package/build/rules/no-consecutive-blank-line.d.ts +60 -0
- package/build/rules/no-control-character.d.ts +4 -2
- package/build/rules/{heading-id.d.ts → require-heading-id.d.ts} +3 -3
- package/package.json +1 -1
- package/src/configs/all.js +2 -0
- package/src/configs/stylistic.js +1 -0
- package/src/core/ast/escape-string-regexp.js +56 -0
- package/src/core/ast/index.js +2 -1
- package/src/rules/index.js +4 -2
- package/src/rules/no-consecutive-blank-line.js +199 -0
- package/src/rules/no-control-character.js +21 -2
- package/src/rules/require-heading-id.js +239 -0
- package/src/rules/heading-id.js +0 -153
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
[](https://www.npmjs.com/package/eslint-markdown)
|
|
9
9
|
[](https://www.npmjs.com/package/eslint-markdown)
|
|
10
|
+
[](https://www.npmjs.com/package/eslint-markdown)
|
|
10
11
|
|
|
11
12
|
> [!IMPORTANT]
|
|
12
13
|
>
|
package/build/configs/all.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export default function all(plugin: ESLint.Plugin): {
|
|
|
24
24
|
readonly 'md/consistent-strong-style': "error";
|
|
25
25
|
readonly 'md/consistent-thematic-break-style': "error";
|
|
26
26
|
readonly 'md/consistent-unordered-list-style': "error";
|
|
27
|
+
readonly 'md/no-consecutive-blank-line': "error";
|
|
27
28
|
readonly 'md/no-control-character': "error";
|
|
28
29
|
readonly 'md/no-curly-quote': "error";
|
|
29
30
|
readonly 'md/no-double-punctuation': "error";
|
|
@@ -34,6 +35,7 @@ export default function all(plugin: ESLint.Plugin): {
|
|
|
34
35
|
readonly 'md/no-irregular-whitespace': "error";
|
|
35
36
|
readonly 'md/no-tab': "error";
|
|
36
37
|
readonly 'md/no-url-trailing-slash': "error";
|
|
38
|
+
readonly 'md/require-heading-id': "error";
|
|
37
39
|
readonly 'md/require-image-title': "error";
|
|
38
40
|
readonly 'md/require-link-title': "error";
|
|
39
41
|
};
|
|
@@ -21,6 +21,7 @@ export default function stylistic(plugin: ESLint.Plugin): {
|
|
|
21
21
|
readonly 'md/consistent-strong-style': "error";
|
|
22
22
|
readonly 'md/consistent-thematic-break-style': "error";
|
|
23
23
|
readonly 'md/consistent-unordered-list-style': "error";
|
|
24
|
+
readonly 'md/no-consecutive-blank-line': "error";
|
|
24
25
|
readonly 'md/no-tab': "error";
|
|
25
26
|
};
|
|
26
27
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility to escape `RegExp` special characters.
|
|
3
|
+
* @see https://github.com/sindresorhus/escape-string-regexp/tree/v5.0.0
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Escape `RegExp` special characters.
|
|
7
|
+
*
|
|
8
|
+
* You can also use this to escape a string that is inserted into the middle of a regex, for example, into a character class.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} string A string to escape.
|
|
11
|
+
* @returns {string} An escaped string.
|
|
12
|
+
* @example
|
|
13
|
+
* ```js
|
|
14
|
+
* import escapeStringRegexp from 'path/to/escape-string-regexp.js';
|
|
15
|
+
*
|
|
16
|
+
* const escapedString = escapeStringRegexp('How much $ for a 🦄?');
|
|
17
|
+
* //=> 'How much \\$ for a 🦄\\?'
|
|
18
|
+
*
|
|
19
|
+
* new RegExp(escapedString);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export default function escapeStringRegexp(string: string): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
import escapeStringRegexp from './escape-string-regexp.js';
|
|
1
2
|
import getElementsByTagName from './html.js';
|
|
2
3
|
import isBlankLine from './is-blank-line.js';
|
|
3
4
|
import SkipRanges from './skip-ranges.js';
|
|
4
|
-
export { getElementsByTagName, isBlankLine, SkipRanges };
|
|
5
|
+
export { escapeStringRegexp, getElementsByTagName, isBlankLine, SkipRanges };
|
package/build/rules/index.d.ts
CHANGED
|
@@ -9,7 +9,8 @@ declare const _default: {
|
|
|
9
9
|
'consistent-strong-style': import("../core/types.js").RuleModule<import("./consistent-strong-style.js").RuleOptions, "style">;
|
|
10
10
|
'consistent-thematic-break-style': import("../core/types.js").RuleModule<import("./consistent-thematic-break-style.js").RuleOptions, "style">;
|
|
11
11
|
'consistent-unordered-list-style': import("../core/types.js").RuleModule<import("./consistent-unordered-list-style.js").RuleOptions, "style">;
|
|
12
|
-
'no-
|
|
12
|
+
'no-consecutive-blank-line': import("../core/types.js").RuleModule<import("./no-consecutive-blank-line.js").RuleOptions, "noConsecutiveBlankLine">;
|
|
13
|
+
'no-control-character': import("../core/types.js").RuleModule<import("./no-control-character.js").RuleOptions, import("./no-control-character.js").MessageIds>;
|
|
13
14
|
'no-curly-quote': import("../core/types.js").RuleModule<import("./no-curly-quote.js").RuleOptions, "noCurlyQuote">;
|
|
14
15
|
'no-double-punctuation': import("../core/types.js").RuleModule<import("./no-double-punctuation.js").RuleOptions, import("./no-double-punctuation.js").MessageIds>;
|
|
15
16
|
'no-double-space': import("../core/types.js").RuleModule<import("./no-double-space.js").RuleOptions, import("./no-double-space.js").MessageIds>;
|
|
@@ -19,6 +20,7 @@ declare const _default: {
|
|
|
19
20
|
'no-irregular-whitespace': import("../core/types.js").RuleModule<import("./no-irregular-whitespace.js").RuleOptions, "noIrregularWhitespace">;
|
|
20
21
|
'no-tab': import("../core/types.js").RuleModule<import("./no-tab.js").RuleOptions, "noTab">;
|
|
21
22
|
'no-url-trailing-slash': import("../core/types.js").RuleModule<[], "noUrlTrailingSlash">;
|
|
23
|
+
'require-heading-id': import("../core/types.js").RuleModule<import("./require-heading-id.js").RuleOptions, import("./require-heading-id.js").MessageIds>;
|
|
22
24
|
'require-image-title': import("../core/types.js").RuleModule<import("./require-image-title.js").RuleOptions, "requireImageTitle">;
|
|
23
25
|
'require-link-title': import("../core/types.js").RuleModule<import("./require-link-title.js").RuleOptions, "requireLinkTitle">;
|
|
24
26
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "layout";
|
|
4
|
+
docs: {
|
|
5
|
+
description: string;
|
|
6
|
+
url: string;
|
|
7
|
+
recommended: boolean;
|
|
8
|
+
stylistic: true;
|
|
9
|
+
};
|
|
10
|
+
fixable: "whitespace";
|
|
11
|
+
schema: {
|
|
12
|
+
type: "object";
|
|
13
|
+
properties: {
|
|
14
|
+
max: {
|
|
15
|
+
type: "integer";
|
|
16
|
+
minimum: number;
|
|
17
|
+
};
|
|
18
|
+
skipCode: {
|
|
19
|
+
oneOf: ({
|
|
20
|
+
type: "boolean";
|
|
21
|
+
items?: never;
|
|
22
|
+
uniqueItems?: never;
|
|
23
|
+
} | {
|
|
24
|
+
type: "array";
|
|
25
|
+
items: {
|
|
26
|
+
type: "string";
|
|
27
|
+
};
|
|
28
|
+
uniqueItems: true;
|
|
29
|
+
})[];
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
additionalProperties: false;
|
|
33
|
+
}[];
|
|
34
|
+
defaultOptions: [{
|
|
35
|
+
max: number;
|
|
36
|
+
skipCode: true;
|
|
37
|
+
}];
|
|
38
|
+
messages: {
|
|
39
|
+
noConsecutiveBlankLine: string;
|
|
40
|
+
};
|
|
41
|
+
language: string;
|
|
42
|
+
dialects: string[];
|
|
43
|
+
};
|
|
44
|
+
create(context: import("@eslint/core").RuleContext<{
|
|
45
|
+
LangOptions: import("@eslint/markdown").MarkdownLanguageOptions;
|
|
46
|
+
Code: import("@eslint/markdown").MarkdownSourceCode;
|
|
47
|
+
RuleOptions: RuleOptions;
|
|
48
|
+
Node: import("mdast").Node;
|
|
49
|
+
MessageIds: "noConsecutiveBlankLine";
|
|
50
|
+
}>): {
|
|
51
|
+
code(node: import("mdast").Code): void;
|
|
52
|
+
'root:exit'(): void;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export default _default;
|
|
56
|
+
export type RuleOptions = [{
|
|
57
|
+
max: number;
|
|
58
|
+
skipCode: boolean | string[];
|
|
59
|
+
}];
|
|
60
|
+
export type MessageIds = "noConsecutiveBlankLine";
|
|
@@ -7,6 +7,7 @@ declare const _default: {
|
|
|
7
7
|
recommended: boolean;
|
|
8
8
|
stylistic: false;
|
|
9
9
|
};
|
|
10
|
+
hasSuggestions: true;
|
|
10
11
|
schema: {
|
|
11
12
|
type: "object";
|
|
12
13
|
properties: {
|
|
@@ -43,6 +44,7 @@ declare const _default: {
|
|
|
43
44
|
}];
|
|
44
45
|
messages: {
|
|
45
46
|
noControlCharacter: string;
|
|
47
|
+
suggestRemove: string;
|
|
46
48
|
};
|
|
47
49
|
language: string;
|
|
48
50
|
dialects: string[];
|
|
@@ -52,7 +54,7 @@ declare const _default: {
|
|
|
52
54
|
Code: import("@eslint/markdown").MarkdownSourceCode;
|
|
53
55
|
RuleOptions: RuleOptions;
|
|
54
56
|
Node: import("mdast").Node;
|
|
55
|
-
MessageIds:
|
|
57
|
+
MessageIds: MessageIds;
|
|
56
58
|
}>): {
|
|
57
59
|
code(node: import("mdast").Code): void;
|
|
58
60
|
inlineCode(node: import("mdast").InlineCode): void;
|
|
@@ -65,4 +67,4 @@ export type RuleOptions = [{
|
|
|
65
67
|
skipCode: boolean | string[];
|
|
66
68
|
skipInlineCode: boolean;
|
|
67
69
|
}];
|
|
68
|
-
export type MessageIds = "noControlCharacter";
|
|
70
|
+
export type MessageIds = "noControlCharacter" | "suggestRemove";
|
|
@@ -22,7 +22,7 @@ declare const _default: {
|
|
|
22
22
|
rightDelimiter: {
|
|
23
23
|
type: "string";
|
|
24
24
|
};
|
|
25
|
-
|
|
25
|
+
allowDepths: {
|
|
26
26
|
type: "array";
|
|
27
27
|
items: {
|
|
28
28
|
enum: number[];
|
|
@@ -36,7 +36,7 @@ declare const _default: {
|
|
|
36
36
|
defaultOptions: ["always", {
|
|
37
37
|
leftDelimiter: string;
|
|
38
38
|
rightDelimiter: string;
|
|
39
|
-
|
|
39
|
+
allowDepths: never[];
|
|
40
40
|
}];
|
|
41
41
|
messages: {
|
|
42
42
|
headingIdAlways: string;
|
|
@@ -59,7 +59,7 @@ export default _default;
|
|
|
59
59
|
export type RuleOptions = ["always" | "never", {
|
|
60
60
|
leftDelimiter: string;
|
|
61
61
|
rightDelimiter: string;
|
|
62
|
-
|
|
62
|
+
allowDepths: Heading["depth"][];
|
|
63
63
|
}];
|
|
64
64
|
export type MessageIds = "headingIdAlways" | "headingIdNever";
|
|
65
65
|
import type { Heading } from 'mdast';
|
package/package.json
CHANGED
package/src/configs/all.js
CHANGED
|
@@ -48,6 +48,7 @@ export default function all(plugin) {
|
|
|
48
48
|
'md/consistent-strong-style': 'error',
|
|
49
49
|
'md/consistent-thematic-break-style': 'error',
|
|
50
50
|
'md/consistent-unordered-list-style': 'error',
|
|
51
|
+
'md/no-consecutive-blank-line': 'error',
|
|
51
52
|
'md/no-control-character': 'error',
|
|
52
53
|
'md/no-curly-quote': 'error',
|
|
53
54
|
'md/no-double-punctuation': 'error',
|
|
@@ -58,6 +59,7 @@ export default function all(plugin) {
|
|
|
58
59
|
'md/no-irregular-whitespace': 'error',
|
|
59
60
|
'md/no-tab': 'error',
|
|
60
61
|
'md/no-url-trailing-slash': 'error',
|
|
62
|
+
'md/require-heading-id': 'error',
|
|
61
63
|
'md/require-image-title': 'error',
|
|
62
64
|
'md/require-link-title': 'error',
|
|
63
65
|
},
|
package/src/configs/stylistic.js
CHANGED
|
@@ -45,6 +45,7 @@ export default function stylistic(plugin) {
|
|
|
45
45
|
'md/consistent-strong-style': 'error',
|
|
46
46
|
'md/consistent-thematic-break-style': 'error',
|
|
47
47
|
'md/consistent-unordered-list-style': 'error',
|
|
48
|
+
'md/no-consecutive-blank-line': 'error',
|
|
48
49
|
'md/no-tab': 'error',
|
|
49
50
|
},
|
|
50
51
|
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility to escape `RegExp` special characters.
|
|
3
|
+
* @see https://github.com/sindresorhus/escape-string-regexp/tree/v5.0.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* MIT License
|
|
8
|
+
*
|
|
9
|
+
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
|
10
|
+
*
|
|
11
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
* in the Software without restriction, including without limitation the rights
|
|
14
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
* furnished to do so, subject to the following conditions:
|
|
17
|
+
*
|
|
18
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
* copies or substantial portions of the Software.
|
|
20
|
+
*
|
|
21
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
* SOFTWARE.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// --------------------------------------------------------------------------------
|
|
31
|
+
// Export
|
|
32
|
+
// --------------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Escape `RegExp` special characters.
|
|
36
|
+
*
|
|
37
|
+
* You can also use this to escape a string that is inserted into the middle of a regex, for example, into a character class.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} string A string to escape.
|
|
40
|
+
* @returns {string} An escaped string.
|
|
41
|
+
* @example
|
|
42
|
+
* ```js
|
|
43
|
+
* import escapeStringRegexp from 'path/to/escape-string-regexp.js';
|
|
44
|
+
*
|
|
45
|
+
* const escapedString = escapeStringRegexp('How much $ for a 🦄?');
|
|
46
|
+
* //=> 'How much \\$ for a 🦄\\?'
|
|
47
|
+
*
|
|
48
|
+
* new RegExp(escapedString);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export default function escapeStringRegexp(string) {
|
|
52
|
+
// Escape characters with special meaning either inside or outside character sets.
|
|
53
|
+
// Use a simple backslash escape when it's always valid, and a `\xnn` escape
|
|
54
|
+
// when the simpler form would be disallowed by Unicode patterns' stricter grammar.
|
|
55
|
+
return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d');
|
|
56
|
+
}
|
package/src/core/ast/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import escapeStringRegexp from './escape-string-regexp.js';
|
|
1
2
|
import getElementsByTagName from './html.js';
|
|
2
3
|
import isBlankLine from './is-blank-line.js';
|
|
3
4
|
import SkipRanges from './skip-ranges.js';
|
|
4
5
|
|
|
5
|
-
export { getElementsByTagName, isBlankLine, SkipRanges };
|
|
6
|
+
export { escapeStringRegexp, getElementsByTagName, isBlankLine, SkipRanges };
|
package/src/rules/index.js
CHANGED
|
@@ -14,8 +14,8 @@ import consistentStrongStyle from './consistent-strong-style.js';
|
|
|
14
14
|
import consistentThematicBreakStyle from './consistent-thematic-break-style.js';
|
|
15
15
|
import consistentUnorderedListStyle from './consistent-unordered-list-style.js';
|
|
16
16
|
// import enCapitalization from './en-capitalization.js';
|
|
17
|
-
// import headingId from './heading-id.js';
|
|
18
17
|
// import noBoldParagraph from './no-bold-paragraph.js';
|
|
18
|
+
import noConsecutiveBlankLine from './no-consecutive-blank-line.js';
|
|
19
19
|
import noControlCharacter from './no-control-character.js';
|
|
20
20
|
import noCurlyQuote from './no-curly-quote.js';
|
|
21
21
|
import noDoublePunctuation from './no-double-punctuation.js';
|
|
@@ -26,6 +26,7 @@ import noIrregularDash from './no-irregular-dash.js';
|
|
|
26
26
|
import noIrregularWhitespace from './no-irregular-whitespace.js';
|
|
27
27
|
import noTab from './no-tab.js';
|
|
28
28
|
import noUrlTrailingSlash from './no-url-trailing-slash.js';
|
|
29
|
+
import requireHeadingId from './require-heading-id.js';
|
|
29
30
|
import requireImageTitle from './require-image-title.js';
|
|
30
31
|
import requireLinkTitle from './require-link-title.js';
|
|
31
32
|
|
|
@@ -42,8 +43,8 @@ export default {
|
|
|
42
43
|
'consistent-thematic-break-style': consistentThematicBreakStyle,
|
|
43
44
|
'consistent-unordered-list-style': consistentUnorderedListStyle,
|
|
44
45
|
// 'en-capitalization': enCapitalization,
|
|
45
|
-
// 'heading-id': headingId,
|
|
46
46
|
// 'no-bold-paragraph': noBoldParagraph,
|
|
47
|
+
'no-consecutive-blank-line': noConsecutiveBlankLine,
|
|
47
48
|
'no-control-character': noControlCharacter,
|
|
48
49
|
'no-curly-quote': noCurlyQuote,
|
|
49
50
|
'no-double-punctuation': noDoublePunctuation,
|
|
@@ -54,6 +55,7 @@ export default {
|
|
|
54
55
|
'no-irregular-whitespace': noIrregularWhitespace,
|
|
55
56
|
'no-tab': noTab,
|
|
56
57
|
'no-url-trailing-slash': noUrlTrailingSlash,
|
|
58
|
+
'require-heading-id': requireHeadingId,
|
|
57
59
|
'require-image-title': requireImageTitle,
|
|
58
60
|
'require-link-title': requireLinkTitle,
|
|
59
61
|
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow consecutive blank lines.
|
|
3
|
+
* @author lumir(lumirlumir)
|
|
4
|
+
* @see https://github.com/DavidAnson/markdownlint/blob/main/lib/md012.mjs
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// --------------------------------------------------------------------------------
|
|
8
|
+
// Import
|
|
9
|
+
// --------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
import { isBlankLine, SkipRanges } from '../core/ast/index.js';
|
|
12
|
+
import { URL_RULE_DOCS } from '../core/constants.js';
|
|
13
|
+
|
|
14
|
+
// --------------------------------------------------------------------------------
|
|
15
|
+
// Typedef
|
|
16
|
+
// --------------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @import { RuleModule } from '../core/types.js';
|
|
20
|
+
* @typedef {[{ max: number, skipCode: boolean | string[] }]} RuleOptions
|
|
21
|
+
* @typedef {'noConsecutiveBlankLine'} MessageIds
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// --------------------------------------------------------------------------------
|
|
25
|
+
// Rule Definition
|
|
26
|
+
// --------------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** @type {RuleModule<RuleOptions, MessageIds>} */
|
|
29
|
+
export default {
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'layout',
|
|
32
|
+
|
|
33
|
+
docs: {
|
|
34
|
+
description: 'Disallow consecutive blank lines',
|
|
35
|
+
url: URL_RULE_DOCS('no-consecutive-blank-line'),
|
|
36
|
+
recommended: false,
|
|
37
|
+
stylistic: true,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
fixable: 'whitespace',
|
|
41
|
+
|
|
42
|
+
schema: [
|
|
43
|
+
{
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
max: {
|
|
47
|
+
type: 'integer',
|
|
48
|
+
minimum: 1,
|
|
49
|
+
},
|
|
50
|
+
skipCode: {
|
|
51
|
+
oneOf: [
|
|
52
|
+
{
|
|
53
|
+
type: 'boolean',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'array',
|
|
57
|
+
items: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
},
|
|
60
|
+
uniqueItems: true,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
|
|
69
|
+
defaultOptions: [
|
|
70
|
+
{
|
|
71
|
+
max: 1,
|
|
72
|
+
skipCode: true,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
|
|
76
|
+
messages: {
|
|
77
|
+
noConsecutiveBlankLine:
|
|
78
|
+
'More than {{ max }} consecutive blank line(s) are not allowed.',
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
language: 'markdown',
|
|
82
|
+
|
|
83
|
+
dialects: ['commonmark', 'gfm'],
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
create(context) {
|
|
87
|
+
const { options, sourceCode } = context;
|
|
88
|
+
const [{ max, skipCode }] = options;
|
|
89
|
+
const { lines, text } = sourceCode;
|
|
90
|
+
|
|
91
|
+
const skipRanges = new SkipRanges();
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
code(node) {
|
|
95
|
+
if (
|
|
96
|
+
Array.isArray(skipCode) ? node.lang && skipCode.includes(node.lang) : skipCode
|
|
97
|
+
)
|
|
98
|
+
skipRanges.push(sourceCode.getRange(node)); // Store range information of `Code`.
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
'root:exit'() {
|
|
102
|
+
let consecutiveBlankLineCount = 0;
|
|
103
|
+
|
|
104
|
+
for (let currentLineIdx = 0; currentLineIdx < lines.length; currentLineIdx++) {
|
|
105
|
+
const startLoc = /** @type {const} */ ({
|
|
106
|
+
line: currentLineIdx + 1,
|
|
107
|
+
column: 1,
|
|
108
|
+
});
|
|
109
|
+
const endLoc = /** @type {const} */ ({
|
|
110
|
+
line: currentLineIdx + 2,
|
|
111
|
+
column: 1,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (
|
|
115
|
+
skipRanges.includes(sourceCode.getIndexFromLoc(startLoc)) ||
|
|
116
|
+
!isBlankLine(lines[currentLineIdx])
|
|
117
|
+
) {
|
|
118
|
+
consecutiveBlankLineCount = 0;
|
|
119
|
+
} else {
|
|
120
|
+
consecutiveBlankLineCount++;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (max < consecutiveBlankLineCount) {
|
|
124
|
+
const lastAllowedBlankLineIdx =
|
|
125
|
+
currentLineIdx - consecutiveBlankLineCount + max;
|
|
126
|
+
|
|
127
|
+
context.report({
|
|
128
|
+
loc: {
|
|
129
|
+
start: startLoc,
|
|
130
|
+
end: endLoc,
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
data: {
|
|
134
|
+
max,
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
messageId: 'noConsecutiveBlankLine',
|
|
138
|
+
|
|
139
|
+
fix(fixer) {
|
|
140
|
+
/*
|
|
141
|
+
* When the consecutive blank-line run reaches EOF, the fixer must remove the
|
|
142
|
+
* whole excess tail at once instead of removing only the current line.
|
|
143
|
+
*
|
|
144
|
+
* `currentLineIdx + 1 === lines.length` means the current line is the final
|
|
145
|
+
* logical line in `sourceCode.lines`. In a case like `foo\n\n\n ` with
|
|
146
|
+
* `max: 1`, the final line is still a blank line, but it may contain spaces.
|
|
147
|
+
* If we only remove from the current line start, those trailing spaces can
|
|
148
|
+
* survive and produce `foo\n ` instead of the intended `foo\n`.
|
|
149
|
+
*
|
|
150
|
+
* The start of the removal range is calculated from the last blank line that
|
|
151
|
+
* is still allowed by `max`:
|
|
152
|
+
*
|
|
153
|
+
* currentLineIdx - consecutiveBlankLineCount
|
|
154
|
+
* -> zero-based index of the first line in the current blank-line run
|
|
155
|
+
*
|
|
156
|
+
* + max
|
|
157
|
+
* -> zero-based index of the last blank line we want to keep
|
|
158
|
+
*
|
|
159
|
+
* + 1
|
|
160
|
+
* -> convert the zero-based line index to ESLint's one-based loc line
|
|
161
|
+
*
|
|
162
|
+
* The column is `lines[lastAllowedBlankLineIdx].length + 1`, which points to the
|
|
163
|
+
* end of that allowed blank line. The range then ends at `text.length`, so
|
|
164
|
+
* everything after the allowed blank line is removed, including remaining blank
|
|
165
|
+
* lines, line endings, and any spaces on the final blank line.
|
|
166
|
+
*
|
|
167
|
+
* Example with `foo\n\n\n ` and `max: 1`:
|
|
168
|
+
*
|
|
169
|
+
* lines = ['foo', '', '', ' ']
|
|
170
|
+
* currentLineIdx = 3
|
|
171
|
+
* consecutiveBlankLineCount = 3
|
|
172
|
+
* max = 1
|
|
173
|
+
*
|
|
174
|
+
* lastAllowedBlankLineIdx = 3 - 3 + 1 = 1
|
|
175
|
+
*
|
|
176
|
+
* So the fixer removes from the end of line 2 to EOF, producing `foo\n`.
|
|
177
|
+
*/
|
|
178
|
+
if (currentLineIdx + 1 === lines.length) {
|
|
179
|
+
return fixer.removeRange([
|
|
180
|
+
sourceCode.getIndexFromLoc({
|
|
181
|
+
line: lastAllowedBlankLineIdx + 1,
|
|
182
|
+
column: lines[lastAllowedBlankLineIdx].length + 1,
|
|
183
|
+
}),
|
|
184
|
+
text.length,
|
|
185
|
+
]);
|
|
186
|
+
} else {
|
|
187
|
+
return fixer.removeRange([
|
|
188
|
+
sourceCode.getIndexFromLoc(startLoc),
|
|
189
|
+
sourceCode.getIndexFromLoc(endLoc),
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
};
|
|
@@ -17,7 +17,7 @@ import { URL_RULE_DOCS } from '../core/constants.js';
|
|
|
17
17
|
/**
|
|
18
18
|
* @import { RuleModule } from '../core/types.js';
|
|
19
19
|
* @typedef {[{ allow: string[], skipCode: boolean | string[], skipInlineCode: boolean }]} RuleOptions
|
|
20
|
-
* @typedef {'noControlCharacter'} MessageIds
|
|
20
|
+
* @typedef {'noControlCharacter' | 'suggestRemove'} MessageIds
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
// --------------------------------------------------------------------------------
|
|
@@ -43,6 +43,8 @@ export default {
|
|
|
43
43
|
stylistic: false,
|
|
44
44
|
},
|
|
45
45
|
|
|
46
|
+
hasSuggestions: true,
|
|
47
|
+
|
|
46
48
|
schema: [
|
|
47
49
|
{
|
|
48
50
|
type: 'object',
|
|
@@ -86,6 +88,7 @@ export default {
|
|
|
86
88
|
|
|
87
89
|
messages: {
|
|
88
90
|
noControlCharacter: 'Control character `{{ controlCharacter }}` is not allowed.',
|
|
91
|
+
suggestRemove: 'Remove control character `{{ controlCharacter }}`.',
|
|
89
92
|
},
|
|
90
93
|
|
|
91
94
|
language: 'markdown',
|
|
@@ -124,6 +127,8 @@ export default {
|
|
|
124
127
|
|
|
125
128
|
if (skipRanges.includes(startOffset)) continue;
|
|
126
129
|
|
|
130
|
+
const controlCharacterCode = `U+${controlCharacter.codePointAt(0)?.toString(16).toUpperCase().padStart(4, '0')}`;
|
|
131
|
+
|
|
127
132
|
context.report({
|
|
128
133
|
loc: {
|
|
129
134
|
start: sourceCode.getLocFromIndex(startOffset),
|
|
@@ -131,10 +136,24 @@ export default {
|
|
|
131
136
|
},
|
|
132
137
|
|
|
133
138
|
data: {
|
|
134
|
-
controlCharacter:
|
|
139
|
+
controlCharacter: controlCharacterCode,
|
|
135
140
|
},
|
|
136
141
|
|
|
137
142
|
messageId: 'noControlCharacter',
|
|
143
|
+
|
|
144
|
+
suggest: [
|
|
145
|
+
{
|
|
146
|
+
messageId: 'suggestRemove',
|
|
147
|
+
|
|
148
|
+
data: {
|
|
149
|
+
controlCharacter: controlCharacterCode,
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
fix(fixer) {
|
|
153
|
+
return fixer.removeRange([startOffset, endOffset]);
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
],
|
|
138
157
|
});
|
|
139
158
|
}
|
|
140
159
|
},
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to enforce the use of heading IDs.
|
|
3
|
+
* @author lumir(lumirlumir)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// --------------------------------------------------------------------------------
|
|
7
|
+
// Import
|
|
8
|
+
// --------------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
import { escapeStringRegexp } from '../core/ast/index.js';
|
|
11
|
+
import { URL_RULE_DOCS } from '../core/constants.js';
|
|
12
|
+
|
|
13
|
+
// --------------------------------------------------------------------------------
|
|
14
|
+
// Typedef
|
|
15
|
+
// --------------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @import { Heading } from 'mdast';
|
|
19
|
+
* @import { RuleModule } from '../core/types.js';
|
|
20
|
+
* @typedef {['always' | 'never', { leftDelimiter: string, rightDelimiter: string, allowDepths: Heading['depth'][] }]} RuleOptions
|
|
21
|
+
* @typedef {'headingIdAlways' | 'headingIdNever'} MessageIds
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// --------------------------------------------------------------------------------
|
|
25
|
+
// Rule Definition
|
|
26
|
+
// --------------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/** @type {RuleModule<RuleOptions, MessageIds>} */
|
|
29
|
+
export default {
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'problem',
|
|
32
|
+
|
|
33
|
+
docs: {
|
|
34
|
+
description: 'Enforce the use of heading IDs',
|
|
35
|
+
url: URL_RULE_DOCS('require-heading-id'),
|
|
36
|
+
recommended: false,
|
|
37
|
+
stylistic: false,
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
fixable: 'code',
|
|
41
|
+
|
|
42
|
+
schema: [
|
|
43
|
+
{
|
|
44
|
+
enum: ['always', 'never'],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
leftDelimiter: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
},
|
|
52
|
+
rightDelimiter: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
},
|
|
55
|
+
allowDepths: {
|
|
56
|
+
type: 'array',
|
|
57
|
+
items: {
|
|
58
|
+
enum: [1, 2, 3, 4, 5, 6],
|
|
59
|
+
},
|
|
60
|
+
uniqueItems: true,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
|
|
67
|
+
defaultOptions: [
|
|
68
|
+
'always',
|
|
69
|
+
{
|
|
70
|
+
leftDelimiter: '{',
|
|
71
|
+
rightDelimiter: '}',
|
|
72
|
+
allowDepths: [],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
|
|
76
|
+
messages: {
|
|
77
|
+
headingIdAlways: 'Headings should have an ID attribute.',
|
|
78
|
+
headingIdNever:
|
|
79
|
+
'Headings should not have an ID attribute. Remove the `{{ headingId }}`.',
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
language: 'markdown',
|
|
83
|
+
|
|
84
|
+
dialects: ['commonmark', 'gfm'],
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
create(context) {
|
|
88
|
+
const { sourceCode } = context;
|
|
89
|
+
const [mode, { leftDelimiter, rightDelimiter, allowDepths }] = context.options;
|
|
90
|
+
|
|
91
|
+
const escapedLeftDelimiter = escapeStringRegexp(leftDelimiter);
|
|
92
|
+
const escapedRightDelimiter = escapeStringRegexp(rightDelimiter);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* We don't use the `[ \t]*$` pattern at the end of the regex because trailing
|
|
96
|
+
* whitespace is already removed from a `heading` node's child `text` node.
|
|
97
|
+
*/
|
|
98
|
+
const headingIdRegex = new RegExp(
|
|
99
|
+
`(?<leadingSpaces>[ \t]+)(?<headingId>${escapedLeftDelimiter}#[^${escapedRightDelimiter}]+${escapedRightDelimiter})$`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
heading(node) {
|
|
104
|
+
// If the heading's depth is included in `allowDepths`, skip it.
|
|
105
|
+
if (allowDepths.includes(node.depth)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* Instead of using deep recursive traversal to find the final `text` node,
|
|
111
|
+
* we simply access the node's last child directly with shallow traversal.
|
|
112
|
+
*
|
|
113
|
+
* This is because in the "Not OK" case, the `text` node is located
|
|
114
|
+
* in the last position while DFS(Depth First Search) traversal,
|
|
115
|
+
* but is located under `emphasis` or `strong` nodes.
|
|
116
|
+
*
|
|
117
|
+
* This is the situation we don't want to support.
|
|
118
|
+
* There must be pure ` #{custom-id}` text at the end of the heading
|
|
119
|
+
* without being wrapped by other nodes.
|
|
120
|
+
*
|
|
121
|
+
* OK:
|
|
122
|
+
*
|
|
123
|
+
* ```md
|
|
124
|
+
* # heading {#custom-id}
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
127
|
+
* Not OK:
|
|
128
|
+
*
|
|
129
|
+
* ```md
|
|
130
|
+
* # heading *{#custom-id}*
|
|
131
|
+
* ^ ^
|
|
132
|
+
*
|
|
133
|
+
* # heading **{#custom-id}**
|
|
134
|
+
* ^^ ^^
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
const lastChildNode = node.children.at(-1);
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* ATX headings and closed ATX headings that contain no content have no child nodes.
|
|
141
|
+
*
|
|
142
|
+
* ATX Headings:
|
|
143
|
+
*
|
|
144
|
+
* ```md
|
|
145
|
+
* #
|
|
146
|
+
*
|
|
147
|
+
* ##
|
|
148
|
+
*
|
|
149
|
+
* ###
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* ATX Closed Headings:
|
|
153
|
+
*
|
|
154
|
+
* ```md
|
|
155
|
+
* # #
|
|
156
|
+
*
|
|
157
|
+
* ## ##
|
|
158
|
+
*
|
|
159
|
+
* ### ###
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
if (!lastChildNode) {
|
|
163
|
+
// If there are no child nodes, intentionally skip it.
|
|
164
|
+
// This is not the part we want to report.
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* Missing heading IDs are reported on the last character of the last child
|
|
170
|
+
* node rather than as a zero-width location (i.e. `column === endColumn`)
|
|
171
|
+
* at the end of that node, because some editors such as VSCode render
|
|
172
|
+
* zero-width diagnostics incorrectly.
|
|
173
|
+
*/
|
|
174
|
+
const [lastChildNodeStartOffset, lastChildNodeEndOffset] =
|
|
175
|
+
sourceCode.getRange(lastChildNode);
|
|
176
|
+
|
|
177
|
+
// If the last child node is not a `text` node, report an error.
|
|
178
|
+
if (lastChildNode.type !== 'text') {
|
|
179
|
+
if (mode === 'always') {
|
|
180
|
+
context.report({
|
|
181
|
+
loc: {
|
|
182
|
+
start: sourceCode.getLocFromIndex(lastChildNodeEndOffset - 1),
|
|
183
|
+
end: sourceCode.getLocFromIndex(lastChildNodeEndOffset),
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
messageId: 'headingIdAlways',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const match = headingIdRegex.exec(sourceCode.getText(lastChildNode));
|
|
194
|
+
|
|
195
|
+
if (!match || !match.groups) {
|
|
196
|
+
if (mode === 'always') {
|
|
197
|
+
context.report({
|
|
198
|
+
loc: {
|
|
199
|
+
start: sourceCode.getLocFromIndex(lastChildNodeEndOffset - 1),
|
|
200
|
+
end: sourceCode.getLocFromIndex(lastChildNodeEndOffset),
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
messageId: 'headingIdAlways',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (mode === 'never') {
|
|
211
|
+
const { leadingSpaces, headingId } = match.groups;
|
|
212
|
+
const leadingSpacesStartOffset = lastChildNodeStartOffset + match.index;
|
|
213
|
+
const headingIdStartOffset = leadingSpacesStartOffset + leadingSpaces.length;
|
|
214
|
+
const headingIdEndOffset = headingIdStartOffset + headingId.length;
|
|
215
|
+
|
|
216
|
+
context.report({
|
|
217
|
+
loc: {
|
|
218
|
+
start: sourceCode.getLocFromIndex(headingIdStartOffset),
|
|
219
|
+
end: sourceCode.getLocFromIndex(headingIdEndOffset),
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
data: {
|
|
223
|
+
headingId,
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
messageId: 'headingIdNever',
|
|
227
|
+
|
|
228
|
+
fix(fixer) {
|
|
229
|
+
return fixer.replaceTextRange(
|
|
230
|
+
[leadingSpacesStartOffset, headingIdEndOffset],
|
|
231
|
+
'',
|
|
232
|
+
);
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
};
|
package/src/rules/heading-id.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Rule to enforce the use of heading IDs.
|
|
3
|
-
* @author lumir(lumirlumir)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// @ts-nocheck -- TODO
|
|
7
|
-
|
|
8
|
-
// --------------------------------------------------------------------------------
|
|
9
|
-
// Import
|
|
10
|
-
// --------------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
import { URL_RULE_DOCS } from '../core/constants.js';
|
|
13
|
-
|
|
14
|
-
// --------------------------------------------------------------------------------
|
|
15
|
-
// Typedefs
|
|
16
|
-
// --------------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @import { Heading } from 'mdast';
|
|
20
|
-
* @import { RuleModule } from '../core/types.js';
|
|
21
|
-
* @typedef {['always' | 'never', { leftDelimiter: string, rightDelimiter: string, ignoreDepth: Heading['depth'][] }]} RuleOptions
|
|
22
|
-
* @typedef {'headingIdAlways' | 'headingIdNever'} MessageIds
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
// --------------------------------------------------------------------------------
|
|
26
|
-
// Rule Definition
|
|
27
|
-
// --------------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
/** @type {RuleModule<RuleOptions, MessageIds>} */
|
|
30
|
-
export default {
|
|
31
|
-
meta: {
|
|
32
|
-
type: 'problem',
|
|
33
|
-
|
|
34
|
-
docs: {
|
|
35
|
-
description: 'Enforce the use of heading IDs',
|
|
36
|
-
url: URL_RULE_DOCS('heading-id'),
|
|
37
|
-
recommended: false,
|
|
38
|
-
stylistic: false,
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
fixable: 'code',
|
|
42
|
-
|
|
43
|
-
schema: [
|
|
44
|
-
{
|
|
45
|
-
enum: ['always', 'never'],
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: 'object',
|
|
49
|
-
properties: {
|
|
50
|
-
leftDelimiter: {
|
|
51
|
-
type: 'string',
|
|
52
|
-
},
|
|
53
|
-
rightDelimiter: {
|
|
54
|
-
type: 'string',
|
|
55
|
-
},
|
|
56
|
-
ignoreDepth: {
|
|
57
|
-
type: 'array',
|
|
58
|
-
items: {
|
|
59
|
-
enum: [1, 2, 3, 4, 5, 6],
|
|
60
|
-
},
|
|
61
|
-
uniqueItems: true,
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
additionalProperties: false,
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
|
|
68
|
-
defaultOptions: [
|
|
69
|
-
'always',
|
|
70
|
-
{
|
|
71
|
-
leftDelimiter: '{',
|
|
72
|
-
rightDelimiter: '}',
|
|
73
|
-
ignoreDepth: [],
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
|
|
77
|
-
messages: {
|
|
78
|
-
headingIdAlways: 'Headings should have an ID attribute.',
|
|
79
|
-
headingIdNever:
|
|
80
|
-
'Headings should not have an ID attribute. Remove the `{{ headingId }}`.',
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
language: 'markdown',
|
|
84
|
-
|
|
85
|
-
dialects: ['commonmark', 'gfm'],
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
create(context) {
|
|
89
|
-
return {
|
|
90
|
-
heading(node) {
|
|
91
|
-
const [mode, { leftDelimiter, rightDelimiter, ignoreDepth }] = context.options;
|
|
92
|
-
|
|
93
|
-
if (ignoreDepth.includes(node.depth)) return;
|
|
94
|
-
|
|
95
|
-
const regex = new RegExp(
|
|
96
|
-
`${leftDelimiter}#[^${rightDelimiter}]+${rightDelimiter}[ \t]*$`,
|
|
97
|
-
);
|
|
98
|
-
const match = context.sourceCode.getText(node).match(regex);
|
|
99
|
-
|
|
100
|
-
if (mode === 'always' && match === null) {
|
|
101
|
-
context.report({
|
|
102
|
-
loc: {
|
|
103
|
-
start: {
|
|
104
|
-
line: node.position.start.line,
|
|
105
|
-
column: node.position.end.column,
|
|
106
|
-
},
|
|
107
|
-
end: {
|
|
108
|
-
line: node.position.start.line,
|
|
109
|
-
column: node.position.end.column,
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
|
|
113
|
-
messageId: 'headingIdAlways',
|
|
114
|
-
});
|
|
115
|
-
} else if (mode === 'never' && match !== null) {
|
|
116
|
-
const headingIdLength = match[0].length;
|
|
117
|
-
|
|
118
|
-
const matchIndexStart = match.index;
|
|
119
|
-
const matchIndexEnd = matchIndexStart + headingIdLength;
|
|
120
|
-
|
|
121
|
-
context.report({
|
|
122
|
-
loc: {
|
|
123
|
-
start: {
|
|
124
|
-
line: node.position.start.line,
|
|
125
|
-
column: node.position.start.column + matchIndexStart,
|
|
126
|
-
},
|
|
127
|
-
end: {
|
|
128
|
-
line: node.position.start.line,
|
|
129
|
-
column: node.position.start.column + matchIndexEnd,
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
|
|
133
|
-
data: {
|
|
134
|
-
headingId: match[0],
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
messageId: 'headingIdNever',
|
|
138
|
-
|
|
139
|
-
fix(fixer) {
|
|
140
|
-
return fixer.replaceTextRange(
|
|
141
|
-
[
|
|
142
|
-
node.position.start.offset + matchIndexStart,
|
|
143
|
-
node.position.start.offset + matchIndexEnd,
|
|
144
|
-
],
|
|
145
|
-
'',
|
|
146
|
-
);
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
},
|
|
153
|
-
};
|