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.
- package/LICENSE.md +1 -1
- package/README.md +1 -1
- package/build/configs/all.d.ts +1 -0
- package/build/core/ast/escape-string-regexp.d.ts +22 -0
- package/build/core/ast/index.d.ts +2 -1
- package/build/rules/index.d.ts +2 -1
- package/build/rules/no-control-character.d.ts +4 -2
- package/build/rules/{heading-id.d.ts → require-heading-id.d.ts} +3 -3
- package/package.json +5 -3
- package/src/configs/all.js +1 -0
- package/src/core/ast/escape-string-regexp.js +56 -0
- package/src/core/ast/index.js +2 -1
- package/src/rules/allow-heading.js +1 -1
- package/src/rules/allow-image-url.js +1 -1
- package/src/rules/allow-link-url.js +1 -1
- package/src/rules/code-lang-shorthand.js +1 -1
- package/src/rules/consistent-code-style.js +1 -1
- package/src/rules/consistent-delete-style.js +1 -1
- package/src/rules/consistent-emphasis-style.js +1 -1
- package/src/rules/consistent-inline-code-style.js +1 -1
- package/src/rules/consistent-strong-style.js +1 -1
- package/src/rules/consistent-thematic-break-style.js +1 -1
- package/src/rules/en-capitalization.js +1 -1
- package/src/rules/index.js +2 -2
- package/src/rules/no-bold-paragraph.js +1 -1
- package/src/rules/no-control-character.js +22 -3
- package/src/rules/no-curly-quote.js +1 -1
- package/src/rules/no-double-punctuation.js +1 -1
- package/src/rules/no-double-space.js +1 -1
- package/src/rules/no-emoji.js +1 -1
- package/src/rules/no-git-conflict-marker.js +1 -1
- package/src/rules/no-irregular-dash.js +1 -1
- package/src/rules/no-irregular-whitespace.js +1 -1
- package/src/rules/no-tab.js +1 -1
- package/src/rules/no-url-trailing-slash.js +1 -1
- package/src/rules/require-heading-id.js +239 -0
- package/src/rules/require-image-title.js +1 -1
- package/src/rules/require-link-title.js +1 -1
- package/build/core/rule-tester.d.ts +0 -13
- package/src/core/rule-tester.js +0 -127
- 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
|
|
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
|
|
package/build/configs/all.d.ts
CHANGED
|
@@ -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 };
|
package/build/rules/index.d.ts
CHANGED
|
@@ -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, "
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
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": "
|
|
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",
|
package/src/configs/all.js
CHANGED
|
@@ -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
|
+
}
|
package/src/core/ast/index.js
CHANGED
|
@@ -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 };
|
package/src/rules/index.js
CHANGED
|
@@ -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 control character.
|
|
3
|
-
* @author
|
|
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:
|
|
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 double or multiple consecutive spaces in text, except for leading and trailing spaces.
|
|
3
|
-
* @author
|
|
3
|
+
* @author lumir(lumirlumir)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// --------------------------------------------------------------------------------
|
package/src/rules/no-emoji.js
CHANGED
package/src/rules/no-tab.js
CHANGED
|
@@ -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,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';
|
package/src/core/rule-tester.js
DELETED
|
@@ -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
|
-
}
|
package/src/rules/heading-id.js
DELETED
|
@@ -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
|
-
};
|