eslint-markdown 0.6.0 → 0.7.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
@@ -1,8 +1,7 @@
1
1
  # eslint-markdown
2
2
 
3
- [![lint](https://img.shields.io/github/actions/workflow/status/lumirlumir/npm-eslint-markdown/lint.yml?label=lint&color=6358d4&labelColor=333333&logo=github)](https://github.com/lumirlumir/npm-eslint-markdown/actions/workflows/lint.yml)
3
+ [![ci](https://img.shields.io/github/actions/workflow/status/lumirlumir/npm-eslint-markdown/ci.yml?label=ci&color=6358d4&labelColor=333333&logo=github)](https://github.com/lumirlumir/npm-eslint-markdown/actions/workflows/ci.yml)
4
4
  [![test](https://img.shields.io/github/actions/workflow/status/lumirlumir/npm-eslint-markdown/test.yml?label=test&color=6358d4&labelColor=333333&logo=github)](https://github.com/lumirlumir/npm-eslint-markdown/actions/workflows/test.yml)
5
- [![test-cross-platform](https://img.shields.io/github/actions/workflow/status/lumirlumir/npm-eslint-markdown/test-cross-platform.yml?label=test-cross-platform&color=6358d4&labelColor=333333&logo=github)](https://github.com/lumirlumir/npm-eslint-markdown/actions/workflows/test-cross-platform.yml)
6
5
  [![codecov](https://img.shields.io/codecov/c/gh/lumirlumir/npm-eslint-markdown?token=LJMUst9eR3&label=Codecov&color=6358d4&labelColor=333333&logo=codecov)](https://codecov.io/gh/lumirlumir/npm-eslint-markdown)
7
6
  ![Node Current](https://img.shields.io/node/v/eslint-markdown?color=6358d4&labelColor=333333&logo=node.js)
8
7
 
@@ -26,6 +26,7 @@ export default function all(plugin: ESLint.Plugin): {
26
26
  readonly 'md/consistent-unordered-list-style': "error";
27
27
  readonly 'md/no-control-character': "error";
28
28
  readonly 'md/no-curly-quote': "error";
29
+ readonly 'md/no-double-punctuation': "error";
29
30
  readonly 'md/no-double-space': "error";
30
31
  readonly 'md/no-emoji': "error";
31
32
  readonly 'md/no-git-conflict-marker': "error";
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Check if a line is blank.
3
+ * - NOTE: The `blockquoteDepth` parameter is zero-based: `0` means one blockquote marker, `1` means two, and so on.
3
4
  * @param {string} str Line string.
5
+ * @param {number} [blockquoteDepth] The depth of blockquotes. Default is `-1`.
4
6
  * @returns {boolean} `true` if the line is blank. `false` otherwise.
5
7
  */
6
- export default function isBlankLine(str: string): boolean;
8
+ export default function isBlankLine(str: string, blockquoteDepth?: number): boolean;
@@ -58,7 +58,9 @@ declare const _default: {
58
58
  Node: import("mdast").Node;
59
59
  MessageIds: MessageIds;
60
60
  }>): {
61
+ blockquote(): void;
61
62
  code(node: import("mdast").Code): void;
63
+ 'blockquote:exit'(): void;
62
64
  };
63
65
  };
64
66
  export default _default;
@@ -11,6 +11,7 @@ declare const _default: {
11
11
  'consistent-unordered-list-style': import("../core/types.js").RuleModule<import("./consistent-unordered-list-style.js").RuleOptions, "style">;
12
12
  'no-control-character': import("../core/types.js").RuleModule<import("./no-control-character.js").RuleOptions, "noControlCharacter">;
13
13
  'no-curly-quote': import("../core/types.js").RuleModule<import("./no-curly-quote.js").RuleOptions, "noCurlyQuote">;
14
+ 'no-double-punctuation': import("../core/types.js").RuleModule<import("./no-double-punctuation.js").RuleOptions, "noDoublePunctuation">;
14
15
  'no-double-space': import("../core/types.js").RuleModule<import("./no-double-space.js").RuleOptions, import("./no-double-space.js").MessageIds>;
15
16
  'no-emoji': import("../core/types.js").RuleModule<[], "noEmoji">;
16
17
  'no-git-conflict-marker': import("../core/types.js").RuleModule<import("./no-git-conflict-marker.js").RuleOptions, "noGitConflictMarker">;
@@ -0,0 +1,49 @@
1
+ declare const _default: {
2
+ meta: {
3
+ type: "problem";
4
+ docs: {
5
+ description: string;
6
+ url: string;
7
+ recommended: boolean;
8
+ stylistic: false;
9
+ };
10
+ schema: {
11
+ type: "object";
12
+ properties: {
13
+ allow: {
14
+ type: "array";
15
+ items: {
16
+ type: "string";
17
+ minLength: number;
18
+ maxLength: number;
19
+ pattern: string;
20
+ };
21
+ uniqueItems: true;
22
+ };
23
+ };
24
+ additionalProperties: false;
25
+ }[];
26
+ defaultOptions: [{
27
+ allow: never[];
28
+ }];
29
+ messages: {
30
+ noDoublePunctuation: string;
31
+ };
32
+ language: string;
33
+ dialects: string[];
34
+ };
35
+ create(context: import("@eslint/core").RuleContext<{
36
+ LangOptions: import("@eslint/markdown").MarkdownLanguageOptions;
37
+ Code: import("@eslint/markdown").MarkdownSourceCode;
38
+ RuleOptions: RuleOptions;
39
+ Node: import("mdast").Node;
40
+ MessageIds: "noDoublePunctuation";
41
+ }>): {
42
+ text(node: import("mdast").Text): void;
43
+ };
44
+ };
45
+ export default _default;
46
+ export type RuleOptions = [{
47
+ allow: string[];
48
+ }];
49
+ export type MessageIds = "noDoublePunctuation";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-markdown",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Lint your Markdown with ESLint.🛠️",
6
6
  "exports": {
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "scripts": {
61
61
  "prepublishOnly": "npm run build",
62
- "build": "node ../../scripts/build-config.mjs && npx tsc && node ../../scripts/cp.mjs ../../LICENSE.md LICENSE.md ../../README.md README.md",
62
+ "build": "node ../../scripts/build-config.js && tsc && node ../../scripts/cp.js ../../LICENSE.md LICENSE.md ../../README.md README.md",
63
63
  "test": "npm run test:types && npm run test:unit",
64
64
  "test:types": "tsc -p ./tsconfig.test.json",
65
65
  "test:unit": "node --test"
@@ -75,10 +75,11 @@
75
75
  "dependencies": {
76
76
  "@eslint/markdown": "^7.5.1",
77
77
  "micromark-util-normalize-identifier": "^2.0.1",
78
- "parse5": "^8.0.0"
78
+ "parse5": "^8.0.1"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@types/mdast": "^4.0.4",
82
+ "@types/unist": "^3.0.3",
82
83
  "eslint": "^9.39.4"
83
84
  }
84
85
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview `all` configuration.
3
- * WARNING: This file is autogenerated using the `scripts/build-config.mjs`
3
+ * WARNING: This file is autogenerated using the `scripts/build-config.js`
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -50,6 +50,7 @@ export default function all(plugin) {
50
50
  'md/consistent-unordered-list-style': 'error',
51
51
  'md/no-control-character': 'error',
52
52
  'md/no-curly-quote': 'error',
53
+ 'md/no-double-punctuation': 'error',
53
54
  'md/no-double-space': 'error',
54
55
  'md/no-emoji': 'error',
55
56
  'md/no-git-conflict-marker': 'error',
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview `base` configuration.
3
- * WARNING: This file is autogenerated using the `scripts/build-config.mjs`
3
+ * WARNING: This file is autogenerated using the `scripts/build-config.js`
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview `recommended` configuration.
3
- * WARNING: This file is autogenerated using the `scripts/build-config.mjs`
3
+ * WARNING: This file is autogenerated using the `scripts/build-config.js`
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview `stylistic` configuration.
3
- * WARNING: This file is autogenerated using the `scripts/build-config.mjs`
3
+ * WARNING: This file is autogenerated using the `scripts/build-config.js`
4
4
  */
5
5
 
6
6
  // --------------------------------------------------------------------------------
@@ -8,6 +8,7 @@
8
8
  // --------------------------------------------------------------------------------
9
9
 
10
10
  const whitespaceChars = new Set([' ', '\t']);
11
+ const blockquoteChar = '>';
11
12
 
12
13
  // --------------------------------------------------------------------------------
13
14
  // Export
@@ -15,15 +16,25 @@ const whitespaceChars = new Set([' ', '\t']);
15
16
 
16
17
  /**
17
18
  * Check if a line is blank.
19
+ * - NOTE: The `blockquoteDepth` parameter is zero-based: `0` means one blockquote marker, `1` means two, and so on.
18
20
  * @param {string} str Line string.
21
+ * @param {number} [blockquoteDepth] The depth of blockquotes. Default is `-1`.
19
22
  * @returns {boolean} `true` if the line is blank. `false` otherwise.
20
23
  */
21
- export default function isBlankLine(str) {
24
+ export default function isBlankLine(str, blockquoteDepth = -1) {
22
25
  // `.length` is cached for performance.
23
26
  const strLength = str.length;
27
+ let remainingBlockquotes = blockquoteDepth + 1;
24
28
 
25
29
  for (let i = 0; i < strLength; i++) {
26
- if (!whitespaceChars.has(str[i])) {
30
+ const char = str[i];
31
+
32
+ if (!whitespaceChars.has(char)) {
33
+ if (remainingBlockquotes > 0 && char === blockquoteChar) {
34
+ remainingBlockquotes--;
35
+ continue;
36
+ }
37
+
27
38
  return false;
28
39
  }
29
40
  }
@@ -132,8 +132,14 @@ export default {
132
132
 
133
133
  /** @type {CodeStyle | null} */
134
134
  let codeStyle = style === 'consistent' ? null : style;
135
+ let blockquoteDepth = -1; // NOTE: Depth `0` is the first blockquote level, which is the top level.
135
136
 
136
137
  return {
138
+ blockquote() {
139
+ // When entering a `blockquote` node, increase the depth.
140
+ blockquoteDepth++;
141
+ },
142
+
137
143
  code(node) {
138
144
  // ------------------------------------------------------------------------
139
145
  // 1. Check code style consistency.
@@ -182,7 +188,7 @@ export default {
182
188
  }
183
189
 
184
190
  // If the line is blank, continue checking the next line. If it's not blank, report the issue.
185
- if (isBlankLine(line)) {
191
+ if (isBlankLine(line, blockquoteDepth)) {
186
192
  continue;
187
193
  }
188
194
 
@@ -225,7 +231,7 @@ export default {
225
231
  }
226
232
 
227
233
  // If the line is blank, continue checking the next line. If it's not blank, report the issue.
228
- if (isBlankLine(line)) {
234
+ if (isBlankLine(line, blockquoteDepth)) {
229
235
  continue;
230
236
  }
231
237
 
@@ -244,6 +250,11 @@ export default {
244
250
  }
245
251
  }
246
252
  },
253
+
254
+ 'blockquote:exit'() {
255
+ // When exiting a `blockquote` node, decrease the depth.
256
+ blockquoteDepth--;
257
+ },
247
258
  };
248
259
  },
249
260
  };
@@ -94,7 +94,7 @@ export default {
94
94
  const unorderedListStyle = [
95
95
  style === 'consistent' || style === 'sublist' ? null : style,
96
96
  ];
97
- let listDepth = -1;
97
+ let listDepth = -1; // NOTE: Depth `0` is the first list level, which is the top level.
98
98
 
99
99
  return {
100
100
  list() {
@@ -18,6 +18,7 @@ import consistentUnorderedListStyle from './consistent-unordered-list-style.js';
18
18
  // import noBoldParagraph from './no-bold-paragraph.js';
19
19
  import noControlCharacter from './no-control-character.js';
20
20
  import noCurlyQuote from './no-curly-quote.js';
21
+ import noDoublePunctuation from './no-double-punctuation.js';
21
22
  import noDoubleSpace from './no-double-space.js';
22
23
  import noEmoji from './no-emoji.js';
23
24
  import noGitConflictMarker from './no-git-conflict-marker.js';
@@ -45,6 +46,7 @@ export default {
45
46
  // 'no-bold-paragraph': noBoldParagraph,
46
47
  'no-control-character': noControlCharacter,
47
48
  'no-curly-quote': noCurlyQuote,
49
+ 'no-double-punctuation': noDoublePunctuation,
48
50
  'no-double-space': noDoubleSpace,
49
51
  'no-emoji': noEmoji,
50
52
  'no-git-conflict-marker': noGitConflictMarker,
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @fileoverview Rule to disallow double consecutive punctuation in text.
3
+ * @author 루밀LuMir(lumirlumir)
4
+ */
5
+
6
+ // --------------------------------------------------------------------------------
7
+ // Import
8
+ // --------------------------------------------------------------------------------
9
+
10
+ import { URL_RULE_DOCS } from '../core/constants.js';
11
+
12
+ // --------------------------------------------------------------------------------
13
+ // Typedef
14
+ // --------------------------------------------------------------------------------
15
+
16
+ /**
17
+ * @import { RuleModule } from '../core/types.js';
18
+ * @typedef {[{ allow: string[] }]} RuleOptions
19
+ * @typedef {'noDoublePunctuation'} MessageIds
20
+ */
21
+
22
+ // --------------------------------------------------------------------------------
23
+ // Helper
24
+ // --------------------------------------------------------------------------------
25
+
26
+ /**
27
+ * This pattern is based on the punctuation list used by `remark-lint`.
28
+ * @see https://github.com/remarkjs/remark-lint/tree/main/packages/remark-lint-no-heading-punctuation#parameters
29
+ */
30
+ const doublePunctuationRegex = /(?:^|(?<=[^!,.:;?]))[!,.:;?]{2}(?:$|(?=[^!,.:;?]))/g;
31
+
32
+ // --------------------------------------------------------------------------------
33
+ // Rule Definition
34
+ // --------------------------------------------------------------------------------
35
+
36
+ /** @type {RuleModule<RuleOptions, MessageIds>} */
37
+ export default {
38
+ meta: {
39
+ type: 'problem',
40
+
41
+ docs: {
42
+ description: 'Disallow double consecutive punctuation in text',
43
+ url: URL_RULE_DOCS('no-double-punctuation'),
44
+ recommended: false,
45
+ stylistic: false,
46
+ },
47
+
48
+ schema: [
49
+ {
50
+ type: 'object',
51
+ properties: {
52
+ allow: {
53
+ type: 'array',
54
+ items: {
55
+ type: 'string',
56
+ minLength: 2,
57
+ maxLength: 2,
58
+ pattern: '^[!,.:;?]{2}$',
59
+ },
60
+ uniqueItems: true,
61
+ },
62
+ },
63
+ additionalProperties: false,
64
+ },
65
+ ],
66
+
67
+ defaultOptions: [
68
+ {
69
+ allow: [],
70
+ },
71
+ ],
72
+
73
+ messages: {
74
+ noDoublePunctuation: 'Double punctuation mark `{{ punctuation }}` is not allowed.',
75
+ },
76
+
77
+ language: 'markdown',
78
+
79
+ dialects: ['commonmark', 'gfm'],
80
+ },
81
+
82
+ create(context) {
83
+ const { sourceCode } = context;
84
+ const [{ allow }] = context.options;
85
+
86
+ return {
87
+ text(node) {
88
+ const [nodeStartOffset] = sourceCode.getRange(node);
89
+ const matches = sourceCode.getText(node).matchAll(doublePunctuationRegex);
90
+
91
+ for (const match of matches) {
92
+ const punctuation = match[0];
93
+
94
+ const startOffset = nodeStartOffset + match.index;
95
+ const endOffset = startOffset + punctuation.length;
96
+
97
+ if (allow.includes(punctuation)) continue;
98
+
99
+ context.report({
100
+ loc: {
101
+ start: sourceCode.getLocFromIndex(startOffset),
102
+ end: sourceCode.getLocFromIndex(endOffset),
103
+ },
104
+
105
+ data: {
106
+ punctuation,
107
+ },
108
+
109
+ messageId: 'noDoublePunctuation',
110
+ });
111
+ }
112
+ },
113
+ };
114
+ },
115
+ };