eslint-markdown 0.4.0 → 0.6.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 CHANGED
@@ -25,6 +25,12 @@ For full documentation, see the [official documentation of the `eslint-markdown`
25
25
  - [Migration Guide](https://eslint-markdown.lumir.page/docs/get-started/migration)
26
26
  - [Rules](https://eslint-markdown.lumir.page/docs/rules)
27
27
 
28
+ ## Compatibility
29
+
30
+ This [`eslint-markdown`](https://github.com/lumirlumir/npm-eslint-markdown#readme) plugin does not include any rules that overlap with ESLint's built-in Markdown rules provided by [`@eslint/markdown`](https://github.com/eslint/markdown#readme).
31
+
32
+ So, we **highly recommend** using the `eslint-markdown` plugin alongside ESLint's built-in Markdown support, `@eslint/markdown`.
33
+
28
34
  ## Code of Conduct
29
35
 
30
36
  See [Code of Conduct](https://github.com/lumirlumir/.github/blob/main/CODE_OF_CONDUCT.md#contributor-covenant-code-of-conduct).
@@ -17,8 +17,10 @@ export default function all(plugin: ESLint.Plugin): {
17
17
  readonly 'md/allow-image-url': "error";
18
18
  readonly 'md/allow-link-url': "error";
19
19
  readonly 'md/code-lang-shorthand': "error";
20
+ readonly 'md/consistent-code-style': "error";
20
21
  readonly 'md/consistent-delete-style': "error";
21
22
  readonly 'md/consistent-emphasis-style': "error";
23
+ readonly 'md/consistent-inline-code-style': "error";
22
24
  readonly 'md/consistent-strong-style': "error";
23
25
  readonly 'md/consistent-thematic-break-style': "error";
24
26
  readonly 'md/consistent-unordered-list-style': "error";
@@ -14,8 +14,10 @@ export default function stylistic(plugin: ESLint.Plugin): {
14
14
  };
15
15
  readonly language: "markdown/gfm";
16
16
  readonly rules: {
17
+ readonly 'md/consistent-code-style': "error";
17
18
  readonly 'md/consistent-delete-style': "error";
18
19
  readonly 'md/consistent-emphasis-style': "error";
20
+ readonly 'md/consistent-inline-code-style': "error";
19
21
  readonly 'md/consistent-strong-style': "error";
20
22
  readonly 'md/consistent-thematic-break-style': "error";
21
23
  readonly 'md/consistent-unordered-list-style': "error";
@@ -1,3 +1,4 @@
1
1
  import getElementsByTagName from './html.js';
2
+ import isBlankLine from './is-blank-line.js';
2
3
  import SkipRanges from './skip-ranges.js';
3
- export { getElementsByTagName, SkipRanges };
4
+ export { getElementsByTagName, isBlankLine, SkipRanges };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Check if a line is blank.
3
+ * @param {string} str Line string.
4
+ * @returns {boolean} `true` if the line is blank. `false` otherwise.
5
+ */
6
+ export default function isBlankLine(str: string): boolean;
@@ -1,4 +1,4 @@
1
- /** @satisfies {string} */
1
+ /** @type {'eslint-markdown'} */
2
2
  export const PKG_NAME: "eslint-markdown";
3
3
  /** @satisfies {string} */
4
4
  export const PKG_VERSION: string;
@@ -8,6 +8,6 @@ export default function ruleTester(ruleName: string, rule: RuleModule<RuleOption
8
8
  export type RuleOptions = MarkdownRuleDefinitionTypeOptions["RuleOptions"];
9
9
  export type MessageIds = MarkdownRuleDefinitionTypeOptions["MessageIds"];
10
10
  export type Tests = Parameters<RuleTester["run"]>[2];
11
- import type { RuleModule } from '../types.js';
11
+ import type { RuleModule } from './types.js';
12
12
  import type { MarkdownRuleDefinitionTypeOptions } from '@eslint/markdown';
13
13
  import { RuleTester } from 'eslint';
@@ -0,0 +1,71 @@
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
+ schema: {
11
+ type: "object";
12
+ properties: {
13
+ style: {
14
+ enum: string[];
15
+ };
16
+ blankLineAbove: {
17
+ oneOf: ({
18
+ enum: false[];
19
+ type?: never;
20
+ minimum?: never;
21
+ } | {
22
+ type: "integer";
23
+ minimum: number;
24
+ enum?: never;
25
+ })[];
26
+ };
27
+ blankLineBelow: {
28
+ oneOf: ({
29
+ enum: false[];
30
+ type?: never;
31
+ minimum?: never;
32
+ } | {
33
+ type: "integer";
34
+ minimum: number;
35
+ enum?: never;
36
+ })[];
37
+ };
38
+ };
39
+ additionalProperties: false;
40
+ }[];
41
+ defaultOptions: [{
42
+ style: "consistent";
43
+ blankLineAbove: false;
44
+ blankLineBelow: false;
45
+ }];
46
+ messages: {
47
+ style: string;
48
+ blankLineAbove: string;
49
+ blankLineBelow: string;
50
+ };
51
+ language: string;
52
+ dialects: string[];
53
+ };
54
+ create(context: import("@eslint/core").RuleContext<{
55
+ LangOptions: import("@eslint/markdown").MarkdownLanguageOptions;
56
+ Code: import("@eslint/markdown").MarkdownSourceCode;
57
+ RuleOptions: RuleOptions;
58
+ Node: import("mdast").Node;
59
+ MessageIds: MessageIds;
60
+ }>): {
61
+ code(node: import("mdast").Code): void;
62
+ };
63
+ };
64
+ export default _default;
65
+ export type CodeStyle = "indent" | "fence-backtick" | "fence-tilde";
66
+ export type RuleOptions = [{
67
+ style: "consistent" | CodeStyle;
68
+ blankLineAbove: number | false;
69
+ blankLineBelow: number | false;
70
+ }];
71
+ export type MessageIds = "style" | "blankLineAbove" | "blankLineBelow";
@@ -0,0 +1,29 @@
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
+ messages: {
12
+ style: string;
13
+ };
14
+ language: string;
15
+ dialects: string[];
16
+ };
17
+ create(context: import("@eslint/core").RuleContext<{
18
+ LangOptions: import("@eslint/markdown").MarkdownLanguageOptions;
19
+ Code: import("@eslint/markdown").MarkdownSourceCode;
20
+ RuleOptions: [];
21
+ Node: import("mdast").Node;
22
+ MessageIds: "style";
23
+ }>): {
24
+ inlineCode(node: import("mdast").InlineCode): void;
25
+ };
26
+ };
27
+ export default _default;
28
+ export type RuleOptions = [];
29
+ export type MessageIds = "style";
@@ -39,9 +39,9 @@ declare const _default: {
39
39
  };
40
40
  };
41
41
  export default _default;
42
- export type UnorderedListMarker = "*" | "+" | "-";
42
+ export type UnorderedListStyle = "*" | "+" | "-";
43
43
  export type RuleOptions = [{
44
- style: "consistent" | "sublist" | UnorderedListMarker;
44
+ style: "consistent" | "sublist" | UnorderedListStyle;
45
45
  }];
46
46
  export type MessageIds = "style";
47
47
  import type { ListItem } from 'mdast';
@@ -2,8 +2,10 @@ declare const _default: {
2
2
  'allow-image-url': import("../core/types.js").RuleModule<import("./allow-image-url.js").RuleOptions, import("./allow-image-url.js").MessageIds>;
3
3
  'allow-link-url': import("../core/types.js").RuleModule<import("./allow-link-url.js").RuleOptions, import("./allow-link-url.js").MessageIds>;
4
4
  'code-lang-shorthand': import("../core/types.js").RuleModule<import("./code-lang-shorthand.js").RuleOptions, "codeLangShorthand">;
5
+ 'consistent-code-style': import("../core/types.js").RuleModule<import("./consistent-code-style.js").RuleOptions, import("./consistent-code-style.js").MessageIds>;
5
6
  'consistent-delete-style': import("../core/types.js").RuleModule<import("./consistent-delete-style.js").RuleOptions, "style">;
6
7
  'consistent-emphasis-style': import("../core/types.js").RuleModule<import("./consistent-emphasis-style.js").RuleOptions, "style">;
8
+ 'consistent-inline-code-style': import("../core/types.js").RuleModule<[], "style">;
7
9
  'consistent-strong-style': import("../core/types.js").RuleModule<import("./consistent-strong-style.js").RuleOptions, "style">;
8
10
  'consistent-thematic-break-style': import("../core/types.js").RuleModule<import("./consistent-thematic-break-style.js").RuleOptions, "style">;
9
11
  'consistent-unordered-list-style': import("../core/types.js").RuleModule<import("./consistent-unordered-list-style.js").RuleOptions, "style">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-markdown",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Lint your Markdown with ESLint.🛠️",
6
6
  "exports": {
@@ -40,6 +40,7 @@
40
40
  "gfm"
41
41
  ],
42
42
  "author": "루밀LuMir <rpfos@naver.com> (https://github.com/lumirlumir)",
43
+ "funding": "https://github.com/sponsors/lumirlumir",
43
44
  "license": "MIT",
44
45
  "homepage": "https://eslint-markdown.lumir.page",
45
46
  "repository": {
@@ -78,6 +79,6 @@
78
79
  },
79
80
  "devDependencies": {
80
81
  "@types/mdast": "^4.0.4",
81
- "eslint": "^9.39.2"
82
+ "eslint": "^9.39.4"
82
83
  }
83
84
  }
@@ -41,8 +41,10 @@ export default function all(plugin) {
41
41
  'md/allow-image-url': 'error',
42
42
  'md/allow-link-url': 'error',
43
43
  'md/code-lang-shorthand': 'error',
44
+ 'md/consistent-code-style': 'error',
44
45
  'md/consistent-delete-style': 'error',
45
46
  'md/consistent-emphasis-style': 'error',
47
+ 'md/consistent-inline-code-style': 'error',
46
48
  'md/consistent-strong-style': 'error',
47
49
  'md/consistent-thematic-break-style': 'error',
48
50
  'md/consistent-unordered-list-style': 'error',
@@ -38,8 +38,10 @@ export default function stylistic(plugin) {
38
38
  },
39
39
  language: 'markdown/gfm',
40
40
  rules: {
41
+ 'md/consistent-code-style': 'error',
41
42
  'md/consistent-delete-style': 'error',
42
43
  'md/consistent-emphasis-style': 'error',
44
+ 'md/consistent-inline-code-style': 'error',
43
45
  'md/consistent-strong-style': 'error',
44
46
  'md/consistent-thematic-break-style': 'error',
45
47
  'md/consistent-unordered-list-style': 'error',
@@ -9,7 +9,7 @@
9
9
  import { parseFragment } from 'parse5';
10
10
 
11
11
  // --------------------------------------------------------------------------------
12
- // Typedefs
12
+ // Typedef
13
13
  // --------------------------------------------------------------------------------
14
14
 
15
15
  /**
@@ -1,4 +1,5 @@
1
1
  import getElementsByTagName from './html.js';
2
+ import isBlankLine from './is-blank-line.js';
2
3
  import SkipRanges from './skip-ranges.js';
3
4
 
4
- export { getElementsByTagName, SkipRanges };
5
+ export { getElementsByTagName, isBlankLine, SkipRanges };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @fileoverview Check if a line is blank.
3
+ * @see https://spec.commonmark.org/0.31.2/#blank-line
4
+ */
5
+
6
+ // --------------------------------------------------------------------------------
7
+ // Helper
8
+ // --------------------------------------------------------------------------------
9
+
10
+ const whitespaceChars = new Set([' ', '\t']);
11
+
12
+ // --------------------------------------------------------------------------------
13
+ // Export
14
+ // --------------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Check if a line is blank.
18
+ * @param {string} str Line string.
19
+ * @returns {boolean} `true` if the line is blank. `false` otherwise.
20
+ */
21
+ export default function isBlankLine(str) {
22
+ // `.length` is cached for performance.
23
+ const strLength = str.length;
24
+
25
+ for (let i = 0; i < strLength; i++) {
26
+ if (!whitespaceChars.has(str[i])) {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ return true;
32
+ }
@@ -6,22 +6,15 @@
6
6
  // Import
7
7
  // --------------------------------------------------------------------------------
8
8
 
9
- import { createRequire } from 'node:module';
10
-
11
- // --------------------------------------------------------------------------------
12
- // Declaration
13
- // --------------------------------------------------------------------------------
14
-
15
- /** @type {{ homepage: string, name: 'eslint-markdown', version: string }} */
16
- const { homepage, name, version } = createRequire(import.meta.url)('../../package.json');
9
+ import pkg from '../../package.json' with { type: 'json' };
17
10
 
18
11
  // --------------------------------------------------------------------------------
19
12
  // Export
20
13
  // --------------------------------------------------------------------------------
21
14
 
15
+ /** @type {'eslint-markdown'} */
16
+ export const PKG_NAME = /** @type {'eslint-markdown'} */ (pkg.name);
22
17
  /** @satisfies {string} */
23
- export const PKG_NAME = name;
24
- /** @satisfies {string} */
25
- export const PKG_VERSION = version;
18
+ export const PKG_VERSION = pkg.version;
26
19
  /** Get the URL for the rule documentation based on the rule name. @param {string} [ruleName] */
27
- export const URL_RULE_DOCS = (ruleName = '') => `${homepage}/docs/rules/${ruleName}`;
20
+ export const URL_RULE_DOCS = (ruleName = '') => `${pkg.homepage}/docs/rules/${ruleName}`;
@@ -17,7 +17,7 @@ import markdown from '@eslint/markdown';
17
17
 
18
18
  /**
19
19
  * @import { MarkdownRuleDefinitionTypeOptions } from '@eslint/markdown';
20
- * @import { RuleModule } from '../types.js';
20
+ * @import { RuleModule } from './types.js';
21
21
  * @typedef {MarkdownRuleDefinitionTypeOptions['RuleOptions']} RuleOptions
22
22
  * @typedef {MarkdownRuleDefinitionTypeOptions['MessageIds']} MessageIds
23
23
  * @typedef {Parameters<RuleTester['run']>[2]} Tests
@@ -76,7 +76,7 @@ export default function ruleTester(ruleName, rule, tests) {
76
76
 
77
77
  it('`meta.docs.description` should exist and follow the convention', () => {
78
78
  ok(meta?.docs?.description);
79
- match(meta?.docs?.description, /^(Enforce|Require|Disallow) .+[^. ]$/);
79
+ match(meta?.docs?.description, /^(?:Enforce|Require|Disallow) .+[^. ]$/);
80
80
  });
81
81
 
82
82
  it('`meta.docs.url` should exist and end with the rule name', () => {
@@ -0,0 +1,249 @@
1
+ /**
2
+ * @fileoverview Rule to enforce consistent code style.
3
+ * @author 루밀LuMir(lumirlumir)
4
+ */
5
+
6
+ /*
7
+ * Note on autofix and suggestion safety:
8
+ * - Converting `fence-backtick` to `fence-tilde` is safe.
9
+ * - Converting `fence-backtick` to `indent` is not safe, as `lang` and `meta` information would be lost.
10
+ * - Converting `fence-tilde` to `fence-backtick` is safe.
11
+ * - Converting `fence-tilde` to `indent` is not safe, as `lang` and `meta` information would be lost.
12
+ * - Converting `indent` to `fence-backtick` is safe.
13
+ * - Converting `indent` to `fence-tilde` is safe.
14
+ */
15
+
16
+ // --------------------------------------------------------------------------------
17
+ // Import
18
+ // --------------------------------------------------------------------------------
19
+
20
+ import { isBlankLine } from '../core/ast/index.js';
21
+ import { URL_RULE_DOCS } from '../core/constants.js';
22
+
23
+ // --------------------------------------------------------------------------------
24
+ // Typedef
25
+ // --------------------------------------------------------------------------------
26
+
27
+ /**
28
+ * @import { RuleModule } from '../core/types.js';
29
+ * @typedef {'indent' | 'fence-backtick' | 'fence-tilde'} CodeStyle
30
+ * @typedef {[{ style: 'consistent' | CodeStyle, blankLineAbove: number | false, blankLineBelow: number | false }]} RuleOptions
31
+ * @typedef {'style' | 'blankLineAbove' | 'blankLineBelow'} MessageIds
32
+ */
33
+
34
+ // --------------------------------------------------------------------------------
35
+ // Helper
36
+ // --------------------------------------------------------------------------------
37
+
38
+ /**
39
+ * Get the current code style based on the given text.
40
+ * @param {string} text The text to determine the code style from.
41
+ * @returns {CodeStyle} The current code style.
42
+ */
43
+ function getCurrentCodeStyle(text) {
44
+ if (text === '`') {
45
+ return 'fence-backtick';
46
+ } else if (text === '~') {
47
+ return 'fence-tilde';
48
+ } else {
49
+ return 'indent';
50
+ }
51
+ }
52
+
53
+ // --------------------------------------------------------------------------------
54
+ // Rule Definition
55
+ // --------------------------------------------------------------------------------
56
+
57
+ /** @type {RuleModule<RuleOptions, MessageIds>} */
58
+ export default {
59
+ meta: {
60
+ type: 'layout',
61
+
62
+ docs: {
63
+ description: 'Enforce consistent code style',
64
+ url: URL_RULE_DOCS('consistent-code-style'),
65
+ recommended: false,
66
+ stylistic: true,
67
+ },
68
+
69
+ // fixable: 'code', // TODO
70
+
71
+ // hasSuggestions: true, // TODO
72
+
73
+ schema: [
74
+ {
75
+ type: 'object',
76
+ properties: {
77
+ style: {
78
+ enum: ['consistent', 'indent', 'fence-backtick', 'fence-tilde'],
79
+ },
80
+ blankLineAbove: {
81
+ oneOf: [
82
+ {
83
+ enum: [false],
84
+ },
85
+ {
86
+ type: 'integer',
87
+ minimum: 1,
88
+ },
89
+ ],
90
+ },
91
+ blankLineBelow: {
92
+ oneOf: [
93
+ {
94
+ enum: [false],
95
+ },
96
+ {
97
+ type: 'integer',
98
+ minimum: 1,
99
+ },
100
+ ],
101
+ },
102
+ },
103
+ additionalProperties: false,
104
+ },
105
+ ],
106
+
107
+ defaultOptions: [
108
+ {
109
+ style: 'consistent',
110
+ blankLineAbove: false,
111
+ blankLineBelow: false,
112
+ },
113
+ ],
114
+
115
+ messages: {
116
+ style: 'Code style should be `{{ style }}`.',
117
+ blankLineAbove:
118
+ 'Code should be surrounded by {{ blankLineAbove }} blank line(s) above.',
119
+ blankLineBelow:
120
+ 'Code should be surrounded by {{ blankLineBelow }} blank line(s) below.',
121
+ },
122
+
123
+ language: 'markdown',
124
+
125
+ dialects: ['commonmark', 'gfm'],
126
+ },
127
+
128
+ create(context) {
129
+ const { sourceCode } = context;
130
+ const { lines } = sourceCode;
131
+ const [{ style, blankLineAbove, blankLineBelow }] = context.options;
132
+
133
+ /** @type {CodeStyle | null} */
134
+ let codeStyle = style === 'consistent' ? null : style;
135
+
136
+ return {
137
+ code(node) {
138
+ // ------------------------------------------------------------------------
139
+ // 1. Check code style consistency.
140
+ // ------------------------------------------------------------------------
141
+
142
+ const [nodeStartOffset] = sourceCode.getRange(node);
143
+ const currentCodeStyle = getCurrentCodeStyle(sourceCode.text[nodeStartOffset]);
144
+
145
+ if (codeStyle === null) {
146
+ codeStyle = currentCodeStyle;
147
+ }
148
+
149
+ if (codeStyle !== currentCodeStyle) {
150
+ context.report({
151
+ node,
152
+
153
+ messageId: 'style',
154
+
155
+ data: {
156
+ style: codeStyle,
157
+ },
158
+ });
159
+ }
160
+
161
+ // ------------------------------------------------------------------------
162
+ // 2. Check blank lines above the code block.
163
+ // ------------------------------------------------------------------------
164
+
165
+ // `markdownlint` doesn't check blank lines above indented code blocks, so we skip this check for the `indent` style.
166
+ if (blankLineAbove !== false && currentCodeStyle !== 'indent') {
167
+ const {
168
+ start: { line: nodeStartLine },
169
+ } = sourceCode.getLoc(node);
170
+ const nodeStartLineIndex = nodeStartLine - 1;
171
+
172
+ for (
173
+ let i = nodeStartLineIndex - 1; // Start checking from the line above the code block.
174
+ i >= nodeStartLineIndex - blankLineAbove; // Check up to the specified number of blank lines.
175
+ i-- // Move upwards through the lines.
176
+ ) {
177
+ const line = lines[i];
178
+
179
+ // If the line is `undefined`, it means we've reached the beginning of the file.
180
+ if (line === undefined) {
181
+ break;
182
+ }
183
+
184
+ // If the line is blank, continue checking the next line. If it's not blank, report the issue.
185
+ if (isBlankLine(line)) {
186
+ continue;
187
+ }
188
+
189
+ context.report({
190
+ node,
191
+
192
+ messageId: 'blankLineAbove',
193
+
194
+ data: {
195
+ blankLineAbove,
196
+ },
197
+ });
198
+
199
+ // No need to check further once we've found a non-blank line.
200
+ break;
201
+ }
202
+ }
203
+
204
+ // ------------------------------------------------------------------------
205
+ // 3. Check blank lines below the code block.
206
+ // ------------------------------------------------------------------------
207
+
208
+ // `markdownlint` doesn't check blank lines below indented code blocks, so we skip this check for the `indent` style.
209
+ if (blankLineBelow !== false && currentCodeStyle !== 'indent') {
210
+ const {
211
+ end: { line: nodeEndLine },
212
+ } = sourceCode.getLoc(node);
213
+ const nodeEndLineIndex = nodeEndLine - 1;
214
+
215
+ for (
216
+ let i = nodeEndLineIndex + 1; // Start checking from the line below the code block.
217
+ i <= nodeEndLineIndex + blankLineBelow; // Check up to the specified number of blank lines.
218
+ i++ // Move downwards through the lines.
219
+ ) {
220
+ const line = lines[i];
221
+
222
+ // If the line is `undefined`, it means we've reached the end of the file.
223
+ if (line === undefined) {
224
+ break;
225
+ }
226
+
227
+ // If the line is blank, continue checking the next line. If it's not blank, report the issue.
228
+ if (isBlankLine(line)) {
229
+ continue;
230
+ }
231
+
232
+ context.report({
233
+ node,
234
+
235
+ messageId: 'blankLineBelow',
236
+
237
+ data: {
238
+ blankLineBelow,
239
+ },
240
+ });
241
+
242
+ // No need to check further once we've found a non-blank line.
243
+ break;
244
+ }
245
+ }
246
+ },
247
+ };
248
+ },
249
+ };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @fileoverview Rule to enforce consistent inline code style.
3
+ * @author 루밀LuMir(lumirlumir)
4
+ * @see https://github.com/DavidAnson/markdownlint/blob/v0.40.0/lib/md038.mjs
5
+ */
6
+
7
+ // --------------------------------------------------------------------------------
8
+ // Import
9
+ // --------------------------------------------------------------------------------
10
+
11
+ import { URL_RULE_DOCS } from '../core/constants.js';
12
+
13
+ // --------------------------------------------------------------------------------
14
+ // Typedef
15
+ // --------------------------------------------------------------------------------
16
+
17
+ /**
18
+ * @import { RuleModule } from '../core/types.js';
19
+ * @typedef {[]} RuleOptions
20
+ * @typedef {'style'} MessageIds
21
+ */
22
+
23
+ // --------------------------------------------------------------------------------
24
+ // Helper
25
+ // --------------------------------------------------------------------------------
26
+
27
+ // `\s` in regular expressions matches whitespace characters beyond `\r`, `\n`, ` `, and `\t`,
28
+ // so we explicitly use `[\r\n \t]` to match those characters to avoid unexpected matches.
29
+ const leadingInlineCodeRegex =
30
+ /^(?<leadingBackticks>`*)(?<leadingSpaces>[\r\n \t]+)(?<firstChar>[^\r\n \t])/;
31
+ const trailingInlineCodeRegex =
32
+ /(?<lastChar>[^\r\n \t])(?<trailingSpaces>[\r\n \t]+)(?<trailingBackticks>`*)$/;
33
+
34
+ // --------------------------------------------------------------------------------
35
+ // Rule Definition
36
+ // --------------------------------------------------------------------------------
37
+
38
+ /** @type {RuleModule<RuleOptions, MessageIds>} */
39
+ export default {
40
+ meta: {
41
+ type: 'layout',
42
+
43
+ docs: {
44
+ description: 'Enforce consistent inline code style',
45
+ url: URL_RULE_DOCS('consistent-inline-code-style'),
46
+ recommended: false,
47
+ stylistic: true,
48
+ },
49
+
50
+ fixable: 'whitespace',
51
+
52
+ messages: {
53
+ style: 'Inline code should not have extra spaces or tabs next to backticks.',
54
+ },
55
+
56
+ language: 'markdown',
57
+
58
+ dialects: ['commonmark', 'gfm'],
59
+ },
60
+
61
+ create(context) {
62
+ const { sourceCode } = context;
63
+
64
+ /**
65
+ * @param {number} startOffset
66
+ * @param {number} endOffset
67
+ */
68
+ function reportStyle(startOffset, endOffset) {
69
+ context.report({
70
+ loc: {
71
+ start: sourceCode.getLocFromIndex(startOffset),
72
+ end: sourceCode.getLocFromIndex(endOffset),
73
+ },
74
+
75
+ messageId: 'style',
76
+
77
+ fix(fixer) {
78
+ return fixer.removeRange([startOffset, endOffset]);
79
+ },
80
+ });
81
+ }
82
+
83
+ return {
84
+ inlineCode(node) {
85
+ // ------------------------------------------------------------------------
86
+ // 1. Extract the text and offsets of the inline code node.
87
+ // ------------------------------------------------------------------------
88
+
89
+ const text = sourceCode.getText(node);
90
+ const [nodeStartOffset, nodeEndOffset] = sourceCode.getRange(node);
91
+
92
+ // ------------------------------------------------------------------------
93
+ // 2. Extract the leading spaces and backticks of the inline code node.
94
+ // ------------------------------------------------------------------------
95
+
96
+ const { leadingBackticks = '', leadingSpaces: leadingSpacesText = '' } =
97
+ text.match(leadingInlineCodeRegex)?.groups ?? {};
98
+ const { leadingSpaces: leadingSpacesValue = '', firstChar = '' } =
99
+ node.value.match(leadingInlineCodeRegex)?.groups ?? {};
100
+
101
+ const startBacktick = firstChar === '`';
102
+ const startPaddingLength = /** @type {0 | 1} */ (
103
+ // `startPaddingLength` is always `0` or `1` because the parser consumes at most one padding space on each side.
104
+ leadingSpacesText.length - leadingSpacesValue.length
105
+ );
106
+ const startBacktickSpaceAdjustment = startBacktick && !startPaddingLength ? 1 : 0;
107
+ const startSpaces = leadingSpacesValue.length > startBacktickSpaceAdjustment;
108
+
109
+ // ------------------------------------------------------------------------
110
+ // 3. Extract the trailing spaces and backticks of the inline code node.
111
+ // ------------------------------------------------------------------------
112
+
113
+ const { trailingSpaces: trailingSpacesText = '', trailingBackticks = '' } =
114
+ text.match(trailingInlineCodeRegex)?.groups ?? {};
115
+ const { lastChar = '', trailingSpaces: trailingSpacesValue = '' } =
116
+ node.value.match(trailingInlineCodeRegex)?.groups ?? {};
117
+
118
+ const endBacktick = lastChar === '`';
119
+ const endPaddingLength = /** @type {0 | 1} */ (
120
+ // `endPaddingLength` is always `0` or `1` because the parser consumes at most one padding space on each side.
121
+ trailingSpacesText.length - trailingSpacesValue.length
122
+ );
123
+ const endBacktickSpaceAdjustment = endBacktick && !endPaddingLength ? 1 : 0;
124
+ const endSpaces = trailingSpacesValue.length > endBacktickSpaceAdjustment;
125
+
126
+ // ------------------------------------------------------------------------
127
+ // 4. Report if there are extra spaces or tabs next to backticks.
128
+ // ------------------------------------------------------------------------
129
+
130
+ const removePadding = startSpaces && endSpaces && !startBacktick && !endBacktick;
131
+
132
+ if (startSpaces) {
133
+ const baseOffset = nodeStartOffset + leadingBackticks.length;
134
+
135
+ reportStyle(
136
+ baseOffset + (removePadding ? 0 : startPaddingLength),
137
+ baseOffset + leadingSpacesText.length - startBacktickSpaceAdjustment,
138
+ );
139
+ }
140
+
141
+ if (endSpaces) {
142
+ const baseOffset = nodeEndOffset - trailingBackticks.length;
143
+
144
+ reportStyle(
145
+ baseOffset - trailingSpacesText.length + endBacktickSpaceAdjustment,
146
+ baseOffset - (removePadding ? 0 : endPaddingLength),
147
+ );
148
+ }
149
+ },
150
+ };
151
+ },
152
+ };
@@ -16,8 +16,8 @@ import { URL_RULE_DOCS } from '../core/constants.js';
16
16
  /**
17
17
  * @import { ListItem } from 'mdast';
18
18
  * @import { RuleModule } from '../core/types.js';
19
- * @typedef {'*' | '+' | '-'} UnorderedListMarker
20
- * @typedef {[{ style: 'consistent' | 'sublist' | UnorderedListMarker }]} RuleOptions
19
+ * @typedef {'*' | '+' | '-'} UnorderedListStyle
20
+ * @typedef {[{ style: 'consistent' | 'sublist' | UnorderedListStyle }]} RuleOptions
21
21
  * @typedef {'style'} MessageIds
22
22
  */
23
23
 
@@ -26,15 +26,15 @@ import { URL_RULE_DOCS } from '../core/constants.js';
26
26
  // --------------------------------------------------------------------------------
27
27
 
28
28
  /**
29
- * Get the next unordered list marker in sequence.
29
+ * Get the next unordered list style in sequence.
30
30
  * Inspired by [`markdownlint`](https://github.com/DavidAnson/markdownlint/blob/v0.40.0/lib/md004.mjs#L9).
31
- * @param {UnorderedListMarker} currentUnorderedListMarker The current unordered list marker.
32
- * @returns {UnorderedListMarker} The next unordered list marker.
31
+ * @param {UnorderedListStyle} currentUnorderedListStyle The current unordered list style.
32
+ * @returns {UnorderedListStyle} The next unordered list style.
33
33
  */
34
- function getNextUnorderedListMarker(currentUnorderedListMarker) {
35
- if (currentUnorderedListMarker === '-') {
34
+ function getNextUnorderedListStyle(currentUnorderedListStyle) {
35
+ if (currentUnorderedListStyle === '-') {
36
36
  return '+';
37
- } else if (currentUnorderedListMarker === '+') {
37
+ } else if (currentUnorderedListStyle === '+') {
38
38
  return '*';
39
39
  } else {
40
40
  return '-';
@@ -90,7 +90,7 @@ export default {
90
90
  const { sourceCode } = context;
91
91
  const [{ style }] = context.options;
92
92
 
93
- /** @type {Array<UnorderedListMarker | null | undefined>} */
93
+ /** @type {Array<UnorderedListStyle | null | undefined>} */
94
94
  const unorderedListStyle = [
95
95
  style === 'consistent' || style === 'sublist' ? null : style,
96
96
  ];
@@ -105,7 +105,7 @@ export default {
105
105
 
106
106
  'list[ordered=false] > listItem'(/** @type {ListItem} */ node) {
107
107
  const [nodeStartOffset] = sourceCode.getRange(node);
108
- const currentUnorderedListStyle = /** @type {UnorderedListMarker} */ (
108
+ const currentUnorderedListStyle = /** @type {UnorderedListStyle} */ (
109
109
  sourceCode.text[nodeStartOffset]
110
110
  );
111
111
  const currentListDepth = style === 'sublist' ? listDepth : 0;
@@ -115,7 +115,7 @@ export default {
115
115
  unorderedListStyle[currentListDepth] =
116
116
  // If the previous depth used the same style, use the next style in sequence.
117
117
  unorderedListStyle[currentListDepth - 1] === currentUnorderedListStyle
118
- ? getNextUnorderedListMarker(currentUnorderedListStyle)
118
+ ? getNextUnorderedListStyle(currentUnorderedListStyle)
119
119
  : currentUnorderedListStyle;
120
120
  }
121
121
 
@@ -6,8 +6,10 @@
6
6
  import allowImageUrl from './allow-image-url.js';
7
7
  import allowLinkUrl from './allow-link-url.js';
8
8
  import codeLangShorthand from './code-lang-shorthand.js';
9
+ import consistentCodeStyle from './consistent-code-style.js';
9
10
  import consistentDeleteStyle from './consistent-delete-style.js';
10
11
  import consistentEmphasisStyle from './consistent-emphasis-style.js';
12
+ import consistentInlineCodeStyle from './consistent-inline-code-style.js';
11
13
  import consistentStrongStyle from './consistent-strong-style.js';
12
14
  import consistentThematicBreakStyle from './consistent-thematic-break-style.js';
13
15
  import consistentUnorderedListStyle from './consistent-unordered-list-style.js';
@@ -31,8 +33,10 @@ export default {
31
33
  'allow-image-url': allowImageUrl,
32
34
  'allow-link-url': allowLinkUrl,
33
35
  'code-lang-shorthand': codeLangShorthand,
36
+ 'consistent-code-style': consistentCodeStyle,
34
37
  'consistent-delete-style': consistentDeleteStyle,
35
38
  'consistent-emphasis-style': consistentEmphasisStyle,
39
+ 'consistent-inline-code-style': consistentInlineCodeStyle,
36
40
  'consistent-strong-style': consistentStrongStyle,
37
41
  'consistent-thematic-break-style': consistentThematicBreakStyle,
38
42
  'consistent-unordered-list-style': consistentUnorderedListStyle,
@@ -1,6 +0,0 @@
1
- /**
2
- * Get the file name of the module.
3
- * @param {string} importMetaUrl The absolute `file:` URL of the module.
4
- * @returns {string} The file name of the module.
5
- */
6
- export default function getFileName(importMetaUrl: string): string;
@@ -1,3 +0,0 @@
1
- import getFileName from './get-file-name.js';
2
- import ruleTester from './rule-tester.js';
3
- export { getFileName, ruleTester };
@@ -1,23 +0,0 @@
1
- /**
2
- * @fileoverview Get the file name of the module.
3
- */
4
-
5
- // --------------------------------------------------------------------------------
6
- // Import
7
- // --------------------------------------------------------------------------------
8
-
9
- import { fileURLToPath } from 'node:url';
10
- import { parse } from 'node:path';
11
-
12
- // --------------------------------------------------------------------------------
13
- // Export
14
- // --------------------------------------------------------------------------------
15
-
16
- /**
17
- * Get the file name of the module.
18
- * @param {string} importMetaUrl The absolute `file:` URL of the module.
19
- * @returns {string} The file name of the module.
20
- */
21
- export default function getFileName(importMetaUrl) {
22
- return parse(fileURLToPath(importMetaUrl)).name.replace(/\.test$/, '');
23
- }
@@ -1,6 +0,0 @@
1
- /* eslint sort-imports: 'error', sort-keys: 'error' */
2
-
3
- import getFileName from './get-file-name.js';
4
- import ruleTester from './rule-tester.js';
5
-
6
- export { getFileName, ruleTester };