eslint-markdown 0.9.0 → 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.
Files changed (41) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +1 -1
  3. package/build/configs/all.d.ts +1 -0
  4. package/build/core/ast/escape-string-regexp.d.ts +22 -0
  5. package/build/core/ast/index.d.ts +2 -1
  6. package/build/rules/index.d.ts +2 -1
  7. package/build/rules/no-control-character.d.ts +4 -2
  8. package/build/rules/{heading-id.d.ts → require-heading-id.d.ts} +3 -3
  9. package/package.json +5 -3
  10. package/src/configs/all.js +1 -0
  11. package/src/core/ast/escape-string-regexp.js +56 -0
  12. package/src/core/ast/index.js +2 -1
  13. package/src/rules/allow-heading.js +1 -1
  14. package/src/rules/allow-image-url.js +1 -1
  15. package/src/rules/allow-link-url.js +1 -1
  16. package/src/rules/code-lang-shorthand.js +1 -1
  17. package/src/rules/consistent-code-style.js +1 -1
  18. package/src/rules/consistent-delete-style.js +1 -1
  19. package/src/rules/consistent-emphasis-style.js +1 -1
  20. package/src/rules/consistent-inline-code-style.js +1 -1
  21. package/src/rules/consistent-strong-style.js +1 -1
  22. package/src/rules/consistent-thematic-break-style.js +1 -1
  23. package/src/rules/en-capitalization.js +1 -1
  24. package/src/rules/index.js +2 -2
  25. package/src/rules/no-bold-paragraph.js +1 -1
  26. package/src/rules/no-control-character.js +22 -3
  27. package/src/rules/no-curly-quote.js +1 -1
  28. package/src/rules/no-double-punctuation.js +1 -1
  29. package/src/rules/no-double-space.js +1 -1
  30. package/src/rules/no-emoji.js +1 -1
  31. package/src/rules/no-git-conflict-marker.js +1 -1
  32. package/src/rules/no-irregular-dash.js +1 -1
  33. package/src/rules/no-irregular-whitespace.js +1 -1
  34. package/src/rules/no-tab.js +1 -1
  35. package/src/rules/no-url-trailing-slash.js +1 -1
  36. package/src/rules/require-heading-id.js +239 -0
  37. package/src/rules/require-image-title.js +1 -1
  38. package/src/rules/require-link-title.js +1 -1
  39. package/build/core/rule-tester.d.ts +0 -13
  40. package/src/core/rule-tester.js +0 -127
  41. package/src/rules/heading-id.js +0 -153
package/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright (c) 2024-present 루밀LuMir(lumirlumir)
3
+ Copyright (c) 2024-present lumir(lumirlumir)
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  > If you like this package, please give it a star on [GitHub](https://github.com/lumirlumir/npm-eslint-markdown)!⭐<br/>
14
14
  > Your support helps us improve and maintain the project.
15
15
 
16
- Lint your Markdown with ESLint.🛠️
16
+ Lint your Markdown with ESLint. Additional rules for use with `@eslint/markdown`.🛠️
17
17
 
18
18
  ## Documentation
19
19
 
@@ -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,8 +1,9 @@
1
1
  {
2
2
  "name": "eslint-markdown",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
- "description": "Lint your Markdown with ESLint.🛠️",
5
+ "sideEffects": false,
6
+ "description": "Lint your Markdown with ESLint. Additional rules for use with `@eslint/markdown`.🛠️",
6
7
  "exports": {
7
8
  ".": {
8
9
  "types": "./build/index.d.ts",
@@ -26,6 +27,7 @@
26
27
  "LICENSE.md",
27
28
  "README.md",
28
29
  "!src/**/*.test.{js,ts}",
30
+ "!{src,build}/tests/**",
29
31
  "!**/fixtures/**"
30
32
  ],
31
33
  "keywords": [
@@ -39,7 +41,7 @@
39
41
  "commonmark",
40
42
  "gfm"
41
43
  ],
42
- "author": "루밀LuMir <rpfos@naver.com> (https://github.com/lumirlumir)",
44
+ "author": "lumir <rpfos@naver.com> (https://github.com/lumirlumir)",
43
45
  "funding": "https://github.com/sponsors/lumirlumir",
44
46
  "license": "MIT",
45
47
  "homepage": "https://eslint-markdown.lumir.page",
@@ -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 };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce the use of allowed text for headings.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce the use of allowed or disallowed URLs for images.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce the use of allowed or disallowed URLs for links.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce the use of shorthand for code block language identifiers.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce consistent code style.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  /*
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce consistent delete style.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce consistent emphasis style.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce consistent inline code style.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  * @see https://github.com/DavidAnson/markdownlint/blob/v0.40.0/lib/md038.mjs
5
5
  */
6
6
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce consistent strong style.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce consistent thematic break style.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce the use of capital letters at the beginning of sentences.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // @ts-nocheck -- TODO
@@ -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
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow using fully bolded paragraphs as headings.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // @ts-nocheck -- TODO
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow control character.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -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
  },
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow curly quotes(`“`, `”`, `‘` or `’`) in text.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow double consecutive punctuation in text.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow double or multiple consecutive spaces in text, except for leading and trailing spaces.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow emojis in text.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow git conflict markers.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow irregular dash.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow irregular whitespace.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow tab characters.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to disallow URL trailing slash.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -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,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce the use of title attribute for images.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Rule to enforce the use of title attribute for links.
3
- * @author 루밀LuMir(lumirlumir)
3
+ * @author lumir(lumirlumir)
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,13 +0,0 @@
1
- /**
2
- * Markdown rule tester.
3
- * @param {string} ruleName Rule name.
4
- * @param {RuleModule<RuleOptions, MessageIds>} rule Rule module.
5
- * @param {Tests} tests Tests.
6
- */
7
- export default function ruleTester(ruleName: string, rule: RuleModule<RuleOptions, MessageIds>, tests: Tests): void;
8
- export type RuleOptions = MarkdownRuleDefinitionTypeOptions["RuleOptions"];
9
- export type MessageIds = MarkdownRuleDefinitionTypeOptions["MessageIds"];
10
- export type Tests = Parameters<RuleTester["run"]>[2];
11
- import type { RuleModule } from './types.js';
12
- import type { MarkdownRuleDefinitionTypeOptions } from '@eslint/markdown';
13
- import { RuleTester } from 'eslint';
@@ -1,127 +0,0 @@
1
- /**
2
- * @fileoverview Markdown rule tester.
3
- */
4
-
5
- // --------------------------------------------------------------------------------
6
- // Import
7
- // --------------------------------------------------------------------------------
8
-
9
- import { describe, it } from 'node:test';
10
- import { match, ok } from 'node:assert';
11
- import { RuleTester } from 'eslint';
12
- import markdown from '@eslint/markdown';
13
-
14
- // --------------------------------------------------------------------------------
15
- // Typedef
16
- // --------------------------------------------------------------------------------
17
-
18
- /**
19
- * @import { MarkdownRuleDefinitionTypeOptions } from '@eslint/markdown';
20
- * @import { RuleModule } from './types.js';
21
- * @typedef {MarkdownRuleDefinitionTypeOptions['RuleOptions']} RuleOptions
22
- * @typedef {MarkdownRuleDefinitionTypeOptions['MessageIds']} MessageIds
23
- * @typedef {Parameters<RuleTester['run']>[2]} Tests
24
- */
25
-
26
- // --------------------------------------------------------------------------------
27
- // Helper
28
- // --------------------------------------------------------------------------------
29
-
30
- /**
31
- * Rule tester for CommonMark.
32
- */
33
- const ruleTesterCommonmark = new RuleTester({
34
- plugins: {
35
- markdown,
36
- },
37
- language: 'markdown/commonmark',
38
- });
39
-
40
- /**
41
- * Rule tester for GFM.
42
- */
43
- const ruleTesterGfm = new RuleTester({
44
- plugins: {
45
- markdown,
46
- },
47
- language: 'markdown/gfm',
48
- });
49
-
50
- // --------------------------------------------------------------------------------
51
- // Export
52
- // --------------------------------------------------------------------------------
53
-
54
- /**
55
- * Markdown rule tester.
56
- * @param {string} ruleName Rule name.
57
- * @param {RuleModule<RuleOptions, MessageIds>} rule Rule module.
58
- * @param {Tests} tests Tests.
59
- */
60
- export default function ruleTester(ruleName, rule, tests) {
61
- const { meta } = rule;
62
-
63
- describe(ruleName, () => {
64
- describe('meta', () => {
65
- it('`meta` should exist', () => {
66
- ok(meta);
67
- });
68
-
69
- it('`meta.type` should exist', () => {
70
- ok(meta?.type);
71
- });
72
-
73
- it('`meta.docs` should exist', () => {
74
- ok(meta?.docs);
75
- });
76
-
77
- it('`meta.docs.description` should exist and follow the convention', () => {
78
- ok(meta?.docs?.description);
79
- match(meta?.docs?.description, /^(?:Enforce|Require|Disallow) .+[^. ]$/);
80
- });
81
-
82
- it('`meta.docs.url` should exist and end with the rule name', () => {
83
- ok(meta?.docs?.url);
84
- match(meta?.docs?.url, new RegExp(`${ruleName}$`));
85
- });
86
-
87
- it('`meta.messages` should exist', () => {
88
- ok(meta?.messages);
89
- });
90
-
91
- it('`meta.messages.messageId` should exist and value should follow the convention', () => {
92
- // @ts-expect-error -- Required for testing.
93
- Object.values(meta.messages).forEach(message => {
94
- ok(message);
95
- match(message, /^[^a-z].*\.$/);
96
- });
97
- });
98
-
99
- it("`meta.language` should exist and be `'markdown'`", () => {
100
- ok(meta?.language);
101
- match(meta?.language, /^markdown$/);
102
- });
103
-
104
- it("`meta.dialects` should exist and be `'commonmark'` or `'gfm'`", () => {
105
- ok(meta?.dialects);
106
- ok(meta?.dialects.length > 0);
107
- meta?.dialects.forEach(dialect => {
108
- match(dialect, /^(?:commonmark|gfm)$/);
109
- });
110
- });
111
- });
112
-
113
- describe('rule', () => {
114
- if (meta?.dialects?.includes('commonmark')) {
115
- it('commonmark', () => {
116
- ruleTesterCommonmark.run(ruleName, rule, tests);
117
- });
118
- }
119
-
120
- if (meta?.dialects?.includes('gfm')) {
121
- it('gfm', () => {
122
- ruleTesterGfm.run(ruleName, rule, tests);
123
- });
124
- }
125
- });
126
- });
127
- }
@@ -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
- };