eslint-markdown 0.12.0 → 0.13.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/build/configs/all.d.ts +1 -0
- package/build/configs/all.js +1 -0
- package/build/configs/stylistic.d.ts +1 -0
- package/build/configs/stylistic.js +1 -0
- package/build/core/utils/index.d.ts +2 -1
- package/build/core/utils/index.js +2 -1
- package/build/core/utils/test-regex-stateless.d.ts +10 -0
- package/build/core/utils/test-regex-stateless.js +21 -0
- package/build/index.d.ts +48 -0
- package/build/rules/allow-heading.js +1 -12
- package/build/rules/allow-image-url.js +3 -3
- package/build/rules/allow-link-url.js +3 -3
- package/build/rules/consistent-thematic-break-style.d.ts +1 -0
- package/build/rules/consistent-thematic-break-style.js +10 -0
- package/build/rules/index.d.ts +48 -0
- package/build/rules/index.js +2 -0
- package/build/rules/no-trailing-heading-punctuation.d.ts +53 -0
- package/build/rules/no-trailing-heading-punctuation.js +187 -0
- package/package.json +2 -2
package/build/configs/all.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export default function all(plugin: ESLint.Plugin): {
|
|
|
37
37
|
readonly 'md/no-irregular-dash': "error";
|
|
38
38
|
readonly 'md/no-irregular-whitespace': "error";
|
|
39
39
|
readonly 'md/no-tab': "error";
|
|
40
|
+
readonly 'md/no-trailing-heading-punctuation': "error";
|
|
40
41
|
readonly 'md/no-url-trailing-slash': "error";
|
|
41
42
|
readonly 'md/require-heading-id': "error";
|
|
42
43
|
readonly 'md/require-image-title': "error";
|
package/build/configs/all.js
CHANGED
|
@@ -45,6 +45,7 @@ export default function all(plugin) {
|
|
|
45
45
|
'md/no-irregular-dash': 'error',
|
|
46
46
|
'md/no-irregular-whitespace': 'error',
|
|
47
47
|
'md/no-tab': 'error',
|
|
48
|
+
'md/no-trailing-heading-punctuation': 'error',
|
|
48
49
|
'md/no-url-trailing-slash': 'error',
|
|
49
50
|
'md/require-heading-id': 'error',
|
|
50
51
|
'md/require-image-title': 'error',
|
|
@@ -25,5 +25,6 @@ export default function stylistic(plugin: ESLint.Plugin): {
|
|
|
25
25
|
readonly 'md/consistent-unordered-list-style': "error";
|
|
26
26
|
readonly 'md/no-consecutive-blank-line': "error";
|
|
27
27
|
readonly 'md/no-tab': "error";
|
|
28
|
+
readonly 'md/no-trailing-heading-punctuation': "error";
|
|
28
29
|
};
|
|
29
30
|
};
|
|
@@ -2,4 +2,5 @@ import escapeStringRegexp from './escape-string-regexp.js';
|
|
|
2
2
|
import getElementsByTagName from './html.js';
|
|
3
3
|
import isBlankLine from './is-blank-line.js';
|
|
4
4
|
import SkipRanges from './skip-ranges.js';
|
|
5
|
-
|
|
5
|
+
import testRegexStateless from './test-regex-stateless.js';
|
|
6
|
+
export { escapeStringRegexp, getElementsByTagName, isBlankLine, SkipRanges, testRegexStateless, };
|
|
@@ -2,4 +2,5 @@ import escapeStringRegexp from './escape-string-regexp.js';
|
|
|
2
2
|
import getElementsByTagName from './html.js';
|
|
3
3
|
import isBlankLine from './is-blank-line.js';
|
|
4
4
|
import SkipRanges from './skip-ranges.js';
|
|
5
|
-
|
|
5
|
+
import testRegexStateless from './test-regex-stateless.js';
|
|
6
|
+
export { escapeStringRegexp, getElementsByTagName, isBlankLine, SkipRanges, testRegexStateless, };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility to test a regular expression without mutating its state.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Tests a regex without mutating the state stored in its `lastIndex`.
|
|
6
|
+
* @param regex Regex to test.
|
|
7
|
+
* @param text Text to test.
|
|
8
|
+
* @returns Whether the regex matches the text.
|
|
9
|
+
*/
|
|
10
|
+
export default function testRegexStateless(regex: RegExp, text: string): boolean;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility to test a regular expression without mutating its state.
|
|
3
|
+
*/
|
|
4
|
+
// --------------------------------------------------------------------------------
|
|
5
|
+
// Helper
|
|
6
|
+
// --------------------------------------------------------------------------------
|
|
7
|
+
const statefulRegexFlagRegex = /[gy]/u;
|
|
8
|
+
// --------------------------------------------------------------------------------
|
|
9
|
+
// Export
|
|
10
|
+
// --------------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Tests a regex without mutating the state stored in its `lastIndex`.
|
|
13
|
+
* @param regex Regex to test.
|
|
14
|
+
* @param text Text to test.
|
|
15
|
+
* @returns Whether the regex matches the text.
|
|
16
|
+
*/
|
|
17
|
+
export default function testRegexStateless(regex, text) {
|
|
18
|
+
return statefulRegexFlagRegex.test(regex.flags)
|
|
19
|
+
? new RegExp(regex).test(text)
|
|
20
|
+
: regex.test(text);
|
|
21
|
+
}
|
package/build/index.d.ts
CHANGED
|
@@ -594,6 +594,7 @@ declare const plugin: {
|
|
|
594
594
|
readonly properties: {
|
|
595
595
|
readonly style: {
|
|
596
596
|
readonly type: "string";
|
|
597
|
+
readonly pattern: "^(?:consistent|(?:-[ \\t]*){3,}|(?:\\*[ \\t]*){3,}|(?:_[ \\t]*){3,})$";
|
|
597
598
|
};
|
|
598
599
|
};
|
|
599
600
|
readonly additionalProperties: false;
|
|
@@ -1206,6 +1207,53 @@ declare const plugin: {
|
|
|
1206
1207
|
'root:exit'(): void;
|
|
1207
1208
|
};
|
|
1208
1209
|
};
|
|
1210
|
+
'no-trailing-heading-punctuation': {
|
|
1211
|
+
readonly meta: {
|
|
1212
|
+
readonly type: "problem";
|
|
1213
|
+
readonly docs: {
|
|
1214
|
+
readonly description: "Disallow trailing punctuation in headings";
|
|
1215
|
+
readonly url: string;
|
|
1216
|
+
readonly recommended: false;
|
|
1217
|
+
readonly stylistic: true;
|
|
1218
|
+
};
|
|
1219
|
+
readonly fixable: "code";
|
|
1220
|
+
readonly schema: [{
|
|
1221
|
+
readonly type: "object";
|
|
1222
|
+
readonly properties: {
|
|
1223
|
+
readonly punctuation: {
|
|
1224
|
+
readonly type: "array";
|
|
1225
|
+
readonly items: {
|
|
1226
|
+
readonly type: "string";
|
|
1227
|
+
readonly minLength: 1;
|
|
1228
|
+
readonly maxLength: 1;
|
|
1229
|
+
};
|
|
1230
|
+
readonly minItems: 1;
|
|
1231
|
+
readonly uniqueItems: true;
|
|
1232
|
+
};
|
|
1233
|
+
};
|
|
1234
|
+
readonly additionalProperties: false;
|
|
1235
|
+
}];
|
|
1236
|
+
readonly defaultOptions: [{
|
|
1237
|
+
readonly punctuation: [".", ",", ";", ":", "!", "。", ",", ";", ":", "!"];
|
|
1238
|
+
}];
|
|
1239
|
+
readonly messages: {
|
|
1240
|
+
readonly noTrailingHeadingPunctuation: "Trailing punctuation `{{ punctuation }}` is not allowed in headings.";
|
|
1241
|
+
};
|
|
1242
|
+
readonly language: "markdown";
|
|
1243
|
+
readonly dialects: ["commonmark", "gfm"];
|
|
1244
|
+
};
|
|
1245
|
+
readonly create: (context: import("@eslint/core").RuleContext<{
|
|
1246
|
+
LangOptions: import("@eslint/markdown").MarkdownLanguageOptions;
|
|
1247
|
+
Code: import("@eslint/markdown").MarkdownSourceCode;
|
|
1248
|
+
RuleOptions: [{
|
|
1249
|
+
punctuation: string[];
|
|
1250
|
+
}];
|
|
1251
|
+
Node: import("mdast").Node;
|
|
1252
|
+
MessageIds: "noTrailingHeadingPunctuation";
|
|
1253
|
+
}>) => {
|
|
1254
|
+
heading(node: import("mdast").Heading): void;
|
|
1255
|
+
};
|
|
1256
|
+
};
|
|
1209
1257
|
'no-url-trailing-slash': {
|
|
1210
1258
|
readonly meta: {
|
|
1211
1259
|
readonly type: "problem";
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* @fileoverview Rule to enforce the use of allowed or disallowed headings.
|
|
3
3
|
* @author lumir(lumirlumir)
|
|
4
4
|
*/
|
|
5
|
+
import { testRegexStateless } from '../core/utils/index.js';
|
|
5
6
|
import { URL_RULE_DOCS } from '../core/constants.js';
|
|
6
7
|
// --------------------------------------------------------------------------------
|
|
7
8
|
// Helper
|
|
8
9
|
// --------------------------------------------------------------------------------
|
|
9
|
-
const statefulRegexFlagRegex = /[gy]/u;
|
|
10
10
|
const headingOptionsSchema = {
|
|
11
11
|
type: 'object',
|
|
12
12
|
properties: {
|
|
@@ -27,17 +27,6 @@ const headingOptionsSchema = {
|
|
|
27
27
|
},
|
|
28
28
|
additionalProperties: false,
|
|
29
29
|
};
|
|
30
|
-
/**
|
|
31
|
-
* Tests a regex without mutating the state stored in its `lastIndex`.
|
|
32
|
-
* @param regex Regex to test.
|
|
33
|
-
* @param text Text to test.
|
|
34
|
-
* @returns Whether the regex matches the text.
|
|
35
|
-
*/
|
|
36
|
-
function testRegexStateless(regex, text) {
|
|
37
|
-
return statefulRegexFlagRegex.test(regex.flags)
|
|
38
|
-
? new RegExp(regex).test(text)
|
|
39
|
-
: regex.test(text);
|
|
40
|
-
}
|
|
41
30
|
// --------------------------------------------------------------------------------
|
|
42
31
|
// Rule Definition
|
|
43
32
|
// --------------------------------------------------------------------------------
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @author lumir(lumirlumir)
|
|
4
4
|
*/
|
|
5
5
|
import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
|
|
6
|
-
import { getElementsByTagName } from '../core/utils/index.js';
|
|
6
|
+
import { getElementsByTagName, testRegexStateless } from '../core/utils/index.js';
|
|
7
7
|
import { URL_RULE_DOCS } from '../core/constants.js';
|
|
8
8
|
// --------------------------------------------------------------------------------
|
|
9
9
|
// Rule Definition
|
|
@@ -114,7 +114,7 @@ export default {
|
|
|
114
114
|
* Therefore, calling the `some` method on an empty array will always return `false`.
|
|
115
115
|
*/
|
|
116
116
|
for (const { loc, url } of images) {
|
|
117
|
-
if (!allowUrls.some(regex => regex
|
|
117
|
+
if (!allowUrls.some(regex => testRegexStateless(regex, url))) {
|
|
118
118
|
context.report({
|
|
119
119
|
loc,
|
|
120
120
|
messageId: 'allowImageUrl',
|
|
@@ -124,7 +124,7 @@ export default {
|
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
|
-
if (disallowUrls.some(regex => regex
|
|
127
|
+
if (disallowUrls.some(regex => testRegexStateless(regex, url))) {
|
|
128
128
|
context.report({
|
|
129
129
|
loc,
|
|
130
130
|
messageId: 'disallowImageUrl',
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @author lumir(lumirlumir)
|
|
4
4
|
*/
|
|
5
5
|
import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
|
|
6
|
-
import { getElementsByTagName } from '../core/utils/index.js';
|
|
6
|
+
import { getElementsByTagName, testRegexStateless } from '../core/utils/index.js';
|
|
7
7
|
import { URL_RULE_DOCS } from '../core/constants.js';
|
|
8
8
|
// --------------------------------------------------------------------------------
|
|
9
9
|
// Rule Definition
|
|
@@ -114,7 +114,7 @@ export default {
|
|
|
114
114
|
* Therefore, calling the `some` method on an empty array will always return `false`.
|
|
115
115
|
*/
|
|
116
116
|
for (const { loc, url } of links) {
|
|
117
|
-
if (!allowUrls.some(regex => regex
|
|
117
|
+
if (!allowUrls.some(regex => testRegexStateless(regex, url))) {
|
|
118
118
|
context.report({
|
|
119
119
|
loc,
|
|
120
120
|
messageId: 'allowLinkUrl',
|
|
@@ -124,7 +124,7 @@ export default {
|
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
|
-
if (disallowUrls.some(regex => regex
|
|
127
|
+
if (disallowUrls.some(regex => testRegexStateless(regex, url))) {
|
|
128
128
|
context.report({
|
|
129
129
|
loc,
|
|
130
130
|
messageId: 'disallowLinkUrl',
|
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
// --------------------------------------------------------------------------------
|
|
8
8
|
import { URL_RULE_DOCS } from '../core/constants.js';
|
|
9
9
|
// --------------------------------------------------------------------------------
|
|
10
|
+
// Helper
|
|
11
|
+
// --------------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Matches `consistent` or a valid CommonMark thematic break style
|
|
14
|
+
* (3+ identical `-`, `*`, or `_` characters, each optionally followed by spaces/tabs).
|
|
15
|
+
* @see https://spec.commonmark.org/0.31.2/#thematic-breaks
|
|
16
|
+
*/
|
|
17
|
+
const thematicBreakStylePattern = '^(?:consistent|(?:-[ \\t]*){3,}|(?:\\*[ \\t]*){3,}|(?:_[ \\t]*){3,})$';
|
|
18
|
+
// --------------------------------------------------------------------------------
|
|
10
19
|
// Rule Definition
|
|
11
20
|
// --------------------------------------------------------------------------------
|
|
12
21
|
export default {
|
|
@@ -25,6 +34,7 @@ export default {
|
|
|
25
34
|
properties: {
|
|
26
35
|
style: {
|
|
27
36
|
type: 'string',
|
|
37
|
+
pattern: thematicBreakStylePattern,
|
|
28
38
|
},
|
|
29
39
|
},
|
|
30
40
|
additionalProperties: false,
|
package/build/rules/index.d.ts
CHANGED
|
@@ -585,6 +585,7 @@ declare const _default: {
|
|
|
585
585
|
readonly properties: {
|
|
586
586
|
readonly style: {
|
|
587
587
|
readonly type: "string";
|
|
588
|
+
readonly pattern: "^(?:consistent|(?:-[ \\t]*){3,}|(?:\\*[ \\t]*){3,}|(?:_[ \\t]*){3,})$";
|
|
588
589
|
};
|
|
589
590
|
};
|
|
590
591
|
readonly additionalProperties: false;
|
|
@@ -1197,6 +1198,53 @@ declare const _default: {
|
|
|
1197
1198
|
'root:exit'(): void;
|
|
1198
1199
|
};
|
|
1199
1200
|
};
|
|
1201
|
+
'no-trailing-heading-punctuation': {
|
|
1202
|
+
readonly meta: {
|
|
1203
|
+
readonly type: "problem";
|
|
1204
|
+
readonly docs: {
|
|
1205
|
+
readonly description: "Disallow trailing punctuation in headings";
|
|
1206
|
+
readonly url: string;
|
|
1207
|
+
readonly recommended: false;
|
|
1208
|
+
readonly stylistic: true;
|
|
1209
|
+
};
|
|
1210
|
+
readonly fixable: "code";
|
|
1211
|
+
readonly schema: [{
|
|
1212
|
+
readonly type: "object";
|
|
1213
|
+
readonly properties: {
|
|
1214
|
+
readonly punctuation: {
|
|
1215
|
+
readonly type: "array";
|
|
1216
|
+
readonly items: {
|
|
1217
|
+
readonly type: "string";
|
|
1218
|
+
readonly minLength: 1;
|
|
1219
|
+
readonly maxLength: 1;
|
|
1220
|
+
};
|
|
1221
|
+
readonly minItems: 1;
|
|
1222
|
+
readonly uniqueItems: true;
|
|
1223
|
+
};
|
|
1224
|
+
};
|
|
1225
|
+
readonly additionalProperties: false;
|
|
1226
|
+
}];
|
|
1227
|
+
readonly defaultOptions: [{
|
|
1228
|
+
readonly punctuation: [".", ",", ";", ":", "!", "。", ",", ";", ":", "!"];
|
|
1229
|
+
}];
|
|
1230
|
+
readonly messages: {
|
|
1231
|
+
readonly noTrailingHeadingPunctuation: "Trailing punctuation `{{ punctuation }}` is not allowed in headings.";
|
|
1232
|
+
};
|
|
1233
|
+
readonly language: "markdown";
|
|
1234
|
+
readonly dialects: ["commonmark", "gfm"];
|
|
1235
|
+
};
|
|
1236
|
+
readonly create: (context: import("@eslint/core").RuleContext<{
|
|
1237
|
+
LangOptions: import("@eslint/markdown").MarkdownLanguageOptions;
|
|
1238
|
+
Code: import("@eslint/markdown").MarkdownSourceCode;
|
|
1239
|
+
RuleOptions: [{
|
|
1240
|
+
punctuation: string[];
|
|
1241
|
+
}];
|
|
1242
|
+
Node: import("mdast").Node;
|
|
1243
|
+
MessageIds: "noTrailingHeadingPunctuation";
|
|
1244
|
+
}>) => {
|
|
1245
|
+
heading(node: import("mdast").Heading): void;
|
|
1246
|
+
};
|
|
1247
|
+
};
|
|
1200
1248
|
'no-url-trailing-slash': {
|
|
1201
1249
|
readonly meta: {
|
|
1202
1250
|
readonly type: "problem";
|
package/build/rules/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import noGitConflictMarker from './no-git-conflict-marker.js';
|
|
|
23
23
|
import noIrregularDash from './no-irregular-dash.js';
|
|
24
24
|
import noIrregularWhitespace from './no-irregular-whitespace.js';
|
|
25
25
|
import noTab from './no-tab.js';
|
|
26
|
+
import noTrailingHeadingPunctuation from './no-trailing-heading-punctuation.js';
|
|
26
27
|
import noUrlTrailingSlash from './no-url-trailing-slash.js';
|
|
27
28
|
import requireHeadingId from './require-heading-id.js';
|
|
28
29
|
import requireImageTitle from './require-image-title.js';
|
|
@@ -51,6 +52,7 @@ export default {
|
|
|
51
52
|
'no-irregular-dash': noIrregularDash,
|
|
52
53
|
'no-irregular-whitespace': noIrregularWhitespace,
|
|
53
54
|
'no-tab': noTab,
|
|
55
|
+
'no-trailing-heading-punctuation': noTrailingHeadingPunctuation,
|
|
54
56
|
'no-url-trailing-slash': noUrlTrailingSlash,
|
|
55
57
|
'require-heading-id': requireHeadingId,
|
|
56
58
|
'require-image-title': requireImageTitle,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow trailing punctuation in headings.
|
|
3
|
+
* @author Ga eun Lee(tooth-is-silver)
|
|
4
|
+
*/
|
|
5
|
+
type RuleOptions = [{
|
|
6
|
+
punctuation: string[];
|
|
7
|
+
}];
|
|
8
|
+
declare const _default: {
|
|
9
|
+
readonly meta: {
|
|
10
|
+
readonly type: "problem";
|
|
11
|
+
readonly docs: {
|
|
12
|
+
readonly description: "Disallow trailing punctuation in headings";
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly recommended: false;
|
|
15
|
+
readonly stylistic: true;
|
|
16
|
+
};
|
|
17
|
+
readonly fixable: "code";
|
|
18
|
+
readonly schema: [{
|
|
19
|
+
readonly type: "object";
|
|
20
|
+
readonly properties: {
|
|
21
|
+
readonly punctuation: {
|
|
22
|
+
readonly type: "array";
|
|
23
|
+
readonly items: {
|
|
24
|
+
readonly type: "string";
|
|
25
|
+
readonly minLength: 1;
|
|
26
|
+
readonly maxLength: 1;
|
|
27
|
+
};
|
|
28
|
+
readonly minItems: 1;
|
|
29
|
+
readonly uniqueItems: true;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
readonly additionalProperties: false;
|
|
33
|
+
}];
|
|
34
|
+
readonly defaultOptions: [{
|
|
35
|
+
readonly punctuation: [".", ",", ";", ":", "!", "。", ",", ";", ":", "!"];
|
|
36
|
+
}];
|
|
37
|
+
readonly messages: {
|
|
38
|
+
readonly noTrailingHeadingPunctuation: "Trailing punctuation `{{ punctuation }}` is not allowed in headings.";
|
|
39
|
+
};
|
|
40
|
+
readonly language: "markdown";
|
|
41
|
+
readonly dialects: ["commonmark", "gfm"];
|
|
42
|
+
};
|
|
43
|
+
readonly create: (context: import("@eslint/core").RuleContext<{
|
|
44
|
+
LangOptions: import("@eslint/markdown").MarkdownLanguageOptions;
|
|
45
|
+
Code: import("@eslint/markdown").MarkdownSourceCode;
|
|
46
|
+
RuleOptions: RuleOptions;
|
|
47
|
+
Node: import("mdast").Node;
|
|
48
|
+
MessageIds: "noTrailingHeadingPunctuation";
|
|
49
|
+
}>) => {
|
|
50
|
+
heading(node: import("mdast").Heading): void;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export default _default;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Rule to disallow trailing punctuation in headings.
|
|
3
|
+
* @author Ga eun Lee(tooth-is-silver)
|
|
4
|
+
*/
|
|
5
|
+
// --------------------------------------------------------------------------------
|
|
6
|
+
// Import
|
|
7
|
+
// --------------------------------------------------------------------------------
|
|
8
|
+
import { escapeStringRegexp } from '../core/utils/index.js';
|
|
9
|
+
import { URL_RULE_DOCS } from '../core/constants.js';
|
|
10
|
+
// --------------------------------------------------------------------------------
|
|
11
|
+
// Helper
|
|
12
|
+
// --------------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Regular expression for identifying a GitHub emoji code at the end of a line.
|
|
15
|
+
* - NOTE: These patterns are based on the `markdownlint`.
|
|
16
|
+
* @see https://github.com/DavidAnson/markdownlint/blob/v0.41.1/helpers/helpers.cjs#L36-L38
|
|
17
|
+
*/
|
|
18
|
+
const trailingGemojiRegex = /:(?:[abmovx]|[-+]1|100|1234|(?:1st|2nd|3rd)_place_medal|8ball|clock\d{1,4}|e-mail|non-potable_water|o2|t-rex|u5272|u5408|u55b6|u6307|u6708|u6709|u6e80|u7121|u7533|u7981|u7a7a|[a-z]{2,15}2?|[a-z]{1,14}(?:_[a-z\d]{1,16})+):$/;
|
|
19
|
+
/**
|
|
20
|
+
* Regular expression for identifying an HTML entity at the end of a line.
|
|
21
|
+
* - NOTE: These patterns are based on the `markdownlint`.
|
|
22
|
+
* @see https://github.com/DavidAnson/markdownlint/blob/v0.41.1/helpers/helpers.cjs#L32-L34
|
|
23
|
+
*/
|
|
24
|
+
const trailingHtmlEntityRegex = /&(?:#\d+|#[xX][\da-fA-F]+|[a-zA-Z]{2,31}|blk\d{2}|emsp1[34]|frac\d{2}|sup\d|there4);$/;
|
|
25
|
+
// --------------------------------------------------------------------------------
|
|
26
|
+
// Rule Definition
|
|
27
|
+
// --------------------------------------------------------------------------------
|
|
28
|
+
export default {
|
|
29
|
+
meta: {
|
|
30
|
+
type: 'problem',
|
|
31
|
+
docs: {
|
|
32
|
+
description: 'Disallow trailing punctuation in headings',
|
|
33
|
+
url: URL_RULE_DOCS('no-trailing-heading-punctuation'),
|
|
34
|
+
recommended: false,
|
|
35
|
+
stylistic: true,
|
|
36
|
+
},
|
|
37
|
+
fixable: 'code',
|
|
38
|
+
schema: [
|
|
39
|
+
{
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
punctuation: {
|
|
43
|
+
type: 'array',
|
|
44
|
+
items: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
minLength: 1,
|
|
47
|
+
maxLength: 1,
|
|
48
|
+
},
|
|
49
|
+
minItems: 1,
|
|
50
|
+
uniqueItems: true,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
defaultOptions: [
|
|
57
|
+
{
|
|
58
|
+
punctuation: ['.', ',', ';', ':', '!', '。', ',', ';', ':', '!'],
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
messages: {
|
|
62
|
+
noTrailingHeadingPunctuation: 'Trailing punctuation `{{ punctuation }}` is not allowed in headings.',
|
|
63
|
+
},
|
|
64
|
+
language: 'markdown',
|
|
65
|
+
dialects: ['commonmark', 'gfm'],
|
|
66
|
+
},
|
|
67
|
+
create(context) {
|
|
68
|
+
const { sourceCode } = context;
|
|
69
|
+
const [{ punctuation }] = context.options;
|
|
70
|
+
const trailingPunctuationRegex = new RegExp(`[ \\t\\r\\n]*[${escapeStringRegexp(punctuation.join(''))}]+$`);
|
|
71
|
+
return {
|
|
72
|
+
heading(node) {
|
|
73
|
+
/*
|
|
74
|
+
* NOTE: This behavior is consistent with the `markdownlint` rule `MD026`.
|
|
75
|
+
*
|
|
76
|
+
* Instead of using deep recursive traversal to find the final `text` node,
|
|
77
|
+
* we simply access the node's last child directly with shallow traversal.
|
|
78
|
+
*
|
|
79
|
+
* This is because in the "Not OK" case, the `text` node is located
|
|
80
|
+
* in the last position while DFS(Depth First Search) traversal,
|
|
81
|
+
* but is located under `emphasis` or `strong` nodes.
|
|
82
|
+
*
|
|
83
|
+
* This is the situation we don't want to support.
|
|
84
|
+
* The trailing punctuation must be in a plain `text` node at the end
|
|
85
|
+
* of the heading without being wrapped by other nodes.
|
|
86
|
+
*
|
|
87
|
+
* OK:
|
|
88
|
+
*
|
|
89
|
+
* ```md
|
|
90
|
+
* # heading .
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* Not OK:
|
|
94
|
+
*
|
|
95
|
+
* ```md
|
|
96
|
+
* # heading *.*
|
|
97
|
+
* ^ ^
|
|
98
|
+
*
|
|
99
|
+
* # heading **.**
|
|
100
|
+
* ^^ ^^
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
const lastChildNode = node.children.at(-1);
|
|
104
|
+
/*
|
|
105
|
+
* ATX headings and closed ATX headings that contain no content have no child nodes.
|
|
106
|
+
*
|
|
107
|
+
* ATX Headings:
|
|
108
|
+
*
|
|
109
|
+
* ```md
|
|
110
|
+
* #
|
|
111
|
+
*
|
|
112
|
+
* ##
|
|
113
|
+
*
|
|
114
|
+
* ###
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* ATX Closed Headings:
|
|
118
|
+
*
|
|
119
|
+
* ```md
|
|
120
|
+
* # #
|
|
121
|
+
*
|
|
122
|
+
* ## ##
|
|
123
|
+
*
|
|
124
|
+
* ### ###
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
if (!lastChildNode || lastChildNode.type !== 'text') {
|
|
128
|
+
// If there is no child node or trailing `text` node, intentionally skip the heading.
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const lastChildText = sourceCode.getText(lastChildNode);
|
|
132
|
+
const match = trailingPunctuationRegex.exec(lastChildText);
|
|
133
|
+
if (!match) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let trailingPunctuation = match[0];
|
|
137
|
+
/*
|
|
138
|
+
* Slice from the beginning of the trailing `text` node through the first
|
|
139
|
+
* character matched by `trailingPunctuationRegex`, including that character.
|
|
140
|
+
*
|
|
141
|
+
* The first matched character can be the closing `:` of a gemoji or the
|
|
142
|
+
* closing `;` of an HTML entity. Including it allows the end-anchored regular
|
|
143
|
+
* expressions to recognize the construct while excluding any punctuation
|
|
144
|
+
* that follows it.
|
|
145
|
+
*
|
|
146
|
+
* Gemoji:
|
|
147
|
+
*
|
|
148
|
+
* ```text
|
|
149
|
+
* lastChildText: Heading :smile:.
|
|
150
|
+
* textThroughFirstTrailingPunctuation: Heading :smile:
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* HTML entity:
|
|
154
|
+
*
|
|
155
|
+
* ```text
|
|
156
|
+
* lastChildText: Copyright ©.
|
|
157
|
+
* textThroughFirstTrailingPunctuation: Copyright ©
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
const textThroughFirstTrailingPunctuation = lastChildText.slice(0, match.index + 1);
|
|
161
|
+
if (trailingGemojiRegex.test(textThroughFirstTrailingPunctuation) ||
|
|
162
|
+
trailingHtmlEntityRegex.test(textThroughFirstTrailingPunctuation)) {
|
|
163
|
+
trailingPunctuation = trailingPunctuation.slice(1);
|
|
164
|
+
}
|
|
165
|
+
if (!trailingPunctuation) {
|
|
166
|
+
// If the match contains only a gemoji or HTML entity terminator, skip it.
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const [, endOffset] = sourceCode.getRange(lastChildNode);
|
|
170
|
+
const startOffset = endOffset - trailingPunctuation.length;
|
|
171
|
+
context.report({
|
|
172
|
+
loc: {
|
|
173
|
+
start: sourceCode.getLocFromIndex(startOffset),
|
|
174
|
+
end: sourceCode.getLocFromIndex(endOffset),
|
|
175
|
+
},
|
|
176
|
+
data: {
|
|
177
|
+
punctuation: trailingPunctuation,
|
|
178
|
+
},
|
|
179
|
+
messageId: 'noTrailingHeadingPunctuation',
|
|
180
|
+
fix(fixer) {
|
|
181
|
+
return fixer.removeRange([startOffset, endOffset]);
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-markdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Lint your Markdown with ESLint. Additional rules for use with `@eslint/markdown`.🛠️",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@types/mdast": "^4.0.4",
|
|
80
80
|
"@types/unist": "^3.0.3",
|
|
81
|
-
"eslint": "^9.39.
|
|
81
|
+
"eslint": "^9.39.5",
|
|
82
82
|
"vitest": "^4.1.9"
|
|
83
83
|
}
|
|
84
84
|
}
|