eslint-markdown 0.9.1 → 0.10.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.
@@ -34,6 +34,7 @@ export default function all(plugin: ESLint.Plugin): {
34
34
  readonly 'md/no-irregular-whitespace': "error";
35
35
  readonly 'md/no-tab': "error";
36
36
  readonly 'md/no-url-trailing-slash': "error";
37
+ readonly 'md/require-heading-id': "error";
37
38
  readonly 'md/require-image-title': "error";
38
39
  readonly 'md/require-link-title': "error";
39
40
  };
@@ -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 };
@@ -9,7 +9,7 @@ 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-control-character': import("../core/types.js").RuleModule<import("./no-control-character.js").RuleOptions, "noControlCharacter">;
12
+ 'no-control-character': import("../core/types.js").RuleModule<import("./no-control-character.js").RuleOptions, import("./no-control-character.js").MessageIds>;
13
13
  'no-curly-quote': import("../core/types.js").RuleModule<import("./no-curly-quote.js").RuleOptions, "noCurlyQuote">;
14
14
  'no-double-punctuation': import("../core/types.js").RuleModule<import("./no-double-punctuation.js").RuleOptions, import("./no-double-punctuation.js").MessageIds>;
15
15
  'no-double-space': import("../core/types.js").RuleModule<import("./no-double-space.js").RuleOptions, import("./no-double-space.js").MessageIds>;
@@ -19,6 +19,7 @@ declare const _default: {
19
19
  'no-irregular-whitespace': import("../core/types.js").RuleModule<import("./no-irregular-whitespace.js").RuleOptions, "noIrregularWhitespace">;
20
20
  'no-tab': import("../core/types.js").RuleModule<import("./no-tab.js").RuleOptions, "noTab">;
21
21
  'no-url-trailing-slash': import("../core/types.js").RuleModule<[], "noUrlTrailingSlash">;
22
+ 'require-heading-id': import("../core/types.js").RuleModule<import("./require-heading-id.js").RuleOptions, import("./require-heading-id.js").MessageIds>;
22
23
  'require-image-title': import("../core/types.js").RuleModule<import("./require-image-title.js").RuleOptions, "requireImageTitle">;
23
24
  'require-link-title': import("../core/types.js").RuleModule<import("./require-link-title.js").RuleOptions, "requireLinkTitle">;
24
25
  };
@@ -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: "noControlCharacter";
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
- ignoreDepth: {
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
- ignoreDepth: never[];
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
- ignoreDepth: Heading["depth"][];
62
+ allowDepths: Heading["depth"][];
63
63
  }];
64
64
  export type MessageIds = "headingIdAlways" | "headingIdNever";
65
65
  import type { Heading } from 'mdast';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-markdown",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Lint your Markdown with ESLint. Additional rules for use with `@eslint/markdown`.🛠️",
@@ -58,6 +58,7 @@ export default function all(plugin) {
58
58
  'md/no-irregular-whitespace': 'error',
59
59
  'md/no-tab': 'error',
60
60
  'md/no-url-trailing-slash': 'error',
61
+ 'md/require-heading-id': 'error',
61
62
  'md/require-image-title': 'error',
62
63
  'md/require-link-title': 'error',
63
64
  },
@@ -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
+ }
@@ -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 };
@@ -14,7 +14,6 @@ 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';
19
18
  import noControlCharacter from './no-control-character.js';
20
19
  import noCurlyQuote from './no-curly-quote.js';
@@ -26,6 +25,7 @@ import noIrregularDash from './no-irregular-dash.js';
26
25
  import noIrregularWhitespace from './no-irregular-whitespace.js';
27
26
  import noTab from './no-tab.js';
28
27
  import noUrlTrailingSlash from './no-url-trailing-slash.js';
28
+ import requireHeadingId from './require-heading-id.js';
29
29
  import requireImageTitle from './require-image-title.js';
30
30
  import requireLinkTitle from './require-link-title.js';
31
31
 
@@ -42,7 +42,6 @@ export default {
42
42
  'consistent-thematic-break-style': consistentThematicBreakStyle,
43
43
  'consistent-unordered-list-style': consistentUnorderedListStyle,
44
44
  // 'en-capitalization': enCapitalization,
45
- // 'heading-id': headingId,
46
45
  // 'no-bold-paragraph': noBoldParagraph,
47
46
  'no-control-character': noControlCharacter,
48
47
  'no-curly-quote': noCurlyQuote,
@@ -54,6 +53,7 @@ export default {
54
53
  'no-irregular-whitespace': noIrregularWhitespace,
55
54
  'no-tab': noTab,
56
55
  'no-url-trailing-slash': noUrlTrailingSlash,
56
+ 'require-heading-id': requireHeadingId,
57
57
  'require-image-title': requireImageTitle,
58
58
  'require-link-title': requireLinkTitle,
59
59
  };
@@ -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: `U+${controlCharacter.codePointAt(0)?.toString(16).toUpperCase().padStart(4, '0')}`,
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
+ };
@@ -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
- };