eslint-plugin-svg 0.0.5 → 0.0.6
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 +4 -1
- package/dist/index.d.ts +19 -212
- package/dist/index.js +288 -93
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -54,13 +54,16 @@ export default defineConfig([
|
|
|
54
54
|
| [no-empty-text](https://eslint-plugin-svg.ntnyq.com/rules/no-empty-text) | Disallow empty text element | ✅ | | |
|
|
55
55
|
| [no-empty-container](https://eslint-plugin-svg.ntnyq.com/rules/no-empty-container) | Disallow empty container element | ✅ | | |
|
|
56
56
|
| [no-empty-groups](https://eslint-plugin-svg.ntnyq.com/rules/no-empty-groups) | Disallow empty g element | ✅ | | |
|
|
57
|
+
| [no-base64-data-url](https://eslint-plugin-svg.ntnyq.com/rules/no-base64-data-url) | Disallow base64 data URLs in attributes | | | |
|
|
57
58
|
| [no-inline-styles](https://eslint-plugin-svg.ntnyq.com/rules/no-inline-styles) | Disallow inline style attribute | ✅ | | |
|
|
58
59
|
| [no-event-handlers](https://eslint-plugin-svg.ntnyq.com/rules/no-event-handlers) | Disallow inline event handlers | ✅ | | |
|
|
59
60
|
| [no-script-tags](https://eslint-plugin-svg.ntnyq.com/rules/no-script-tags) | Disallow script elements | ✅ | | |
|
|
60
61
|
| [require-viewbox](https://eslint-plugin-svg.ntnyq.com/rules/require-viewbox) | Require viewBox on svg elements | ✅ | | |
|
|
61
62
|
| [no-deprecated](https://eslint-plugin-svg.ntnyq.com/rules/no-deprecated) | Disallow deprecated elements | ✅ | | |
|
|
62
|
-
| [no-elements](https://eslint-plugin-svg.ntnyq.com/rules/no-elements) | Disallow elements by name | | | |
|
|
63
63
|
| [no-doctype](https://eslint-plugin-svg.ntnyq.com/rules/no-doctype) | Disallow doctype | ✅ | 🔧 | |
|
|
64
|
+
| [no-duplicate-ids](https://eslint-plugin-svg.ntnyq.com/rules/no-duplicate-ids) | Disallow duplicate id attributes | ✅ | | |
|
|
65
|
+
| [no-elements](https://eslint-plugin-svg.ntnyq.com/rules/no-elements) | Disallow elements by name | | | |
|
|
66
|
+
| [no-comments](https://eslint-plugin-svg.ntnyq.com/rules/no-comments) | Disallow comments in SVG files | | | |
|
|
64
67
|
| [no-invalid-role](https://eslint-plugin-svg.ntnyq.com/rules/no-invalid-role) | Disallow invalid value of role attribute | ✅ | | |
|
|
65
68
|
|
|
66
69
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -1,212 +1,24 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Linter
|
|
3
|
-
import { JSONSchema4 } from "json-schema";
|
|
1
|
+
import * as eslint from "eslint";
|
|
2
|
+
import { Linter } from "eslint";
|
|
4
3
|
|
|
5
|
-
//#region src/types/sourceCode.d.ts
|
|
6
|
-
interface SourceCode {
|
|
7
|
-
ast: AST.Program;
|
|
8
|
-
hasBOM: boolean;
|
|
9
|
-
lines: string[];
|
|
10
|
-
text: string;
|
|
11
|
-
commentsExistBetween(left: SVGNodeOrToken, right: SVGNodeOrToken): boolean;
|
|
12
|
-
getAllComments(): AST.CommentNode[];
|
|
13
|
-
getCommentsAfter(nodeOrToken: SVGNodeOrToken): AST.CommentNode[];
|
|
14
|
-
getCommentsBefore(nodeOrToken: SVGNodeOrToken): AST.CommentNode[];
|
|
15
|
-
getCommentsInside(node: AST.AnyNode): AST.CommentNode[];
|
|
16
|
-
getFirstToken(node: AST.AnyNode): AST.AnyToken;
|
|
17
|
-
getIndexFromLoc(loc: AST.Position): number;
|
|
18
|
-
getLastToken(node: AST.AnyNode): AST.AnyToken;
|
|
19
|
-
getLastTokens(node: AST.AnyNode, options?: CursorWithCountOptions): SVGToken[];
|
|
20
|
-
getLines(): string[];
|
|
21
|
-
getLocFromIndex(index: number): AST.Position;
|
|
22
|
-
getNodeByRangeIndex(index: number): AST.AnyNode | null;
|
|
23
|
-
getTokenAfter(node: SVGNodeOrToken): AST.AnyToken | null;
|
|
24
|
-
getTokenBefore(node: SVGNodeOrToken): AST.AnyToken | null;
|
|
25
|
-
isSpaceBetweenTokens(first: SVGToken, second: SVGToken): boolean;
|
|
26
|
-
visitorKeys: {
|
|
27
|
-
[nodeType: string]: string[];
|
|
28
|
-
};
|
|
29
|
-
parserServices?: {
|
|
30
|
-
isSVG?: true;
|
|
31
|
-
parseError?: any;
|
|
32
|
-
};
|
|
33
|
-
getComments(node: SVGNodeOrToken): {
|
|
34
|
-
leading: AST.CommentNode[];
|
|
35
|
-
trailing: AST.CommentNode[];
|
|
36
|
-
};
|
|
37
|
-
getFirstToken(node: AST.AnyNode, options?: CursorWithSkipOptions): SVGToken | null;
|
|
38
|
-
getFirstTokenBetween(left: SVGNodeOrToken, right: SVGNodeOrToken, options?: CursorWithSkipOptions): SVGToken | null;
|
|
39
|
-
getFirstTokens(node: AST.AnyNode, options?: CursorWithCountOptions): SVGToken[];
|
|
40
|
-
getFirstTokensBetween(left: SVGNodeOrToken, right: SVGNodeOrToken, options?: CursorWithCountOptions): SVGToken[];
|
|
41
|
-
getLastToken(node: AST.AnyNode, options?: CursorWithSkipOptions): SVGToken | null;
|
|
42
|
-
getLastTokenBetween(left: SVGNodeOrToken, right: SVGNodeOrToken, options?: CursorWithSkipOptions): SVGToken | null;
|
|
43
|
-
getLastTokensBetween(left: SVGNodeOrToken, right: SVGNodeOrToken, options?: CursorWithCountOptions): SVGToken[];
|
|
44
|
-
getText(node?: SVGNodeOrToken, beforeCount?: number, afterCount?: number): string;
|
|
45
|
-
getTokenAfter(node: SVGNodeOrToken, options?: CursorWithSkipOptions): SVGToken | null;
|
|
46
|
-
getTokenBefore(node: SVGNodeOrToken, options?: CursorWithSkipOptions): SVGToken | null;
|
|
47
|
-
getTokenByRangeStart(offset: number, options?: {
|
|
48
|
-
includeComments?: boolean;
|
|
49
|
-
}): SVGToken | null;
|
|
50
|
-
getTokens(node: AST.AnyNode, beforeCount?: number, afterCount?: number): SVGToken[];
|
|
51
|
-
getTokens(node: AST.AnyNode, options: CursorWithCountOptions | FilterPredicate): SVGToken[];
|
|
52
|
-
getTokensAfter(node: SVGNodeOrToken, options?: CursorWithCountOptions): SVGToken[];
|
|
53
|
-
getTokensBefore(node: SVGNodeOrToken, options?: CursorWithCountOptions): SVGToken[];
|
|
54
|
-
getTokensBetween(left: SVGNodeOrToken, right: SVGNodeOrToken, padding?: number | CursorWithCountOptions | FilterPredicate): SVGToken[];
|
|
55
|
-
}
|
|
56
|
-
type CursorWithCountOptions = number | FilterPredicate | {
|
|
57
|
-
count?: number;
|
|
58
|
-
filter?: FilterPredicate;
|
|
59
|
-
includeComments?: boolean;
|
|
60
|
-
};
|
|
61
|
-
type CursorWithSkipOptions = number | FilterPredicate | {
|
|
62
|
-
filter?: FilterPredicate;
|
|
63
|
-
includeComments?: boolean;
|
|
64
|
-
skip?: number;
|
|
65
|
-
};
|
|
66
|
-
type FilterPredicate = (tokenOrComment: SVGToken) => boolean;
|
|
67
|
-
type SVGNodeOrToken = AST.AnyNode | AST.AnyToken;
|
|
68
|
-
type SVGToken = AST.AnyToken | AST.CommentNode;
|
|
69
|
-
//#endregion
|
|
70
|
-
//#region src/types/eslint.d.ts
|
|
71
|
-
/**
|
|
72
|
-
* Rule fixer
|
|
73
|
-
*/
|
|
74
|
-
type Fix = {
|
|
75
|
-
range: AST.Range;
|
|
76
|
-
text: string;
|
|
77
|
-
};
|
|
78
|
-
type ReportDescriptor<TMessageIds extends string> = ReportDescriptorWithSuggestion<TMessageIds> & (ReportDescriptorLocOnly | ReportDescriptorNodeOptionalLoc);
|
|
79
|
-
type ReportDescriptorBase<TMessageIds extends string> = {
|
|
80
|
-
readonly messageId: TMessageIds;
|
|
81
|
-
readonly data?: ReportDescriptorMessageData;
|
|
82
|
-
readonly fix?: ReportFixer;
|
|
83
|
-
};
|
|
84
|
-
type ReportDescriptorLocOnly = {
|
|
85
|
-
loc: Readonly<AST.Position> | Readonly<AST.SourceLocation>;
|
|
86
|
-
};
|
|
87
|
-
type ReportDescriptorMessageData = Readonly<Record<string, unknown>>;
|
|
88
|
-
type ReportDescriptorNodeOptionalLoc = {
|
|
89
|
-
readonly node: AST.AnyNode;
|
|
90
|
-
readonly loc?: Readonly<AST.Position> | Readonly<AST.SourceLocation>;
|
|
91
|
-
};
|
|
92
|
-
interface ReportDescriptorWithSuggestion<TMessageIds extends string> extends ReportDescriptorBase<TMessageIds> {
|
|
93
|
-
readonly suggest?: readonly Rule.SuggestionReportDescriptor[];
|
|
94
|
-
}
|
|
95
|
-
type ReportFixer = (fixer: RuleFixer) => Fix | Fix[] | IterableIterator<Fix> | null;
|
|
96
|
-
interface RuleContext<TMessageIds extends string, TOptions extends readonly unknown[] = []> {
|
|
97
|
-
id: string;
|
|
98
|
-
options: TOptions;
|
|
99
|
-
parserPath: string;
|
|
100
|
-
settings: {
|
|
101
|
-
svg?: SVGSettings;
|
|
102
|
-
[name: string]: any;
|
|
103
|
-
};
|
|
104
|
-
getFilename(): string;
|
|
105
|
-
getSourceCode(): SourceCode;
|
|
106
|
-
report(descriptor: ReportDescriptor<TMessageIds>): void;
|
|
107
|
-
parserServices?: {
|
|
108
|
-
isSVG?: true;
|
|
109
|
-
parseError?: any;
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
type RuleFixer = {
|
|
113
|
-
insertTextAfter(nodeOrToken: AST.AnyNode | AST.AnyToken, text: string): Fix;
|
|
114
|
-
insertTextAfterRange(range: AST.Range, text: string): Fix;
|
|
115
|
-
insertTextBefore(nodeOrToken: AST.AnyNode | AST.AnyToken, text: string): Fix;
|
|
116
|
-
insertTextBeforeRange(range: AST.Range, text: string): Fix;
|
|
117
|
-
remove(nodeOrToken: AST.AnyNode | AST.AnyToken): Fix;
|
|
118
|
-
removeRange(range: AST.Range): Fix;
|
|
119
|
-
replaceText(nodeOrToken: AST.AnyNode | AST.AnyToken, text: string): Fix;
|
|
120
|
-
replaceTextRange(range: AST.Range, text: string): Fix;
|
|
121
|
-
};
|
|
122
|
-
interface RuleListener {
|
|
123
|
-
Attribute?: (node: AST.AttributeNode) => void;
|
|
124
|
-
'Attribute:exit'?: (node: AST.AttributeNode) => void;
|
|
125
|
-
Comment?: (node: AST.CommentNode) => void;
|
|
126
|
-
'Comment:exit'?: (node: AST.CommentNode) => void;
|
|
127
|
-
Doctype?: (node: AST.DoctypeNode) => void;
|
|
128
|
-
'Doctype:exit'?: (node: AST.DoctypeNode) => void;
|
|
129
|
-
DoctypeAttribute?: (node: AST.DoctypeAttributeNode) => void;
|
|
130
|
-
'DoctypeAttribute:exit'?: (node: AST.DoctypeAttributeNode) => void;
|
|
131
|
-
Document?: (node: AST.DocumentNode) => void;
|
|
132
|
-
'Document:exit'?: (node: AST.DocumentNode) => void;
|
|
133
|
-
Program?: (node: AST.Program) => void;
|
|
134
|
-
'Program:exit'?: (node: AST.Program) => void;
|
|
135
|
-
Tag?: (node: AST.TagNode) => void;
|
|
136
|
-
'Tag:exit'?: (node: AST.TagNode) => void;
|
|
137
|
-
Text?: (node: AST.TextNode) => void;
|
|
138
|
-
'Text:exit'?: (node: AST.TextNode) => void;
|
|
139
|
-
[key: string]: ((node: never) => void) | undefined;
|
|
140
|
-
}
|
|
141
|
-
interface RuleMetaData<TMessageIds extends string, TDocs = unknown, TOptions extends readonly unknown[] = []> {
|
|
142
|
-
messages: Record<TMessageIds, string>;
|
|
143
|
-
schema: JSONSchema4 | readonly JSONSchema4[];
|
|
144
|
-
type: 'layout' | 'problem' | 'suggestion';
|
|
145
|
-
defaultOptions?: TOptions;
|
|
146
|
-
deprecated?: boolean;
|
|
147
|
-
docs?: RuleMetaDataDocs & TDocs;
|
|
148
|
-
fixable?: 'code' | 'whitespace';
|
|
149
|
-
hasSuggestions?: boolean;
|
|
150
|
-
replacedBy?: readonly string[];
|
|
151
|
-
}
|
|
152
|
-
interface RuleMetaDataDocs {
|
|
153
|
-
description: string;
|
|
154
|
-
category?: string;
|
|
155
|
-
recommended?: boolean;
|
|
156
|
-
url?: string;
|
|
157
|
-
}
|
|
158
|
-
interface RuleModule<TMessageIds extends string, TOptions extends readonly unknown[] = [], TDocs = unknown> {
|
|
159
|
-
defaultOptions: TOptions;
|
|
160
|
-
meta?: RuleMetaData<TMessageIds, TDocs, TOptions>;
|
|
161
|
-
create(context: RuleContext<TMessageIds, TOptions>): RuleListener;
|
|
162
|
-
}
|
|
163
|
-
type SVGSettings = {
|
|
164
|
-
indent?: number;
|
|
165
|
-
};
|
|
166
|
-
//#endregion
|
|
167
|
-
//#region src/rules/no-deprecated.d.ts
|
|
168
|
-
type Options$4 = [{
|
|
169
|
-
allowElements?: string[];
|
|
170
|
-
}];
|
|
171
|
-
//#endregion
|
|
172
|
-
//#region src/rules/no-elements.d.ts
|
|
173
|
-
type Options$3 = [{
|
|
174
|
-
elements?: string[];
|
|
175
|
-
}];
|
|
176
|
-
//#endregion
|
|
177
|
-
//#region src/rules/no-empty-container.d.ts
|
|
178
|
-
type Options$2 = [{
|
|
179
|
-
elements?: string[];
|
|
180
|
-
ignores?: string[];
|
|
181
|
-
ignoreComments?: boolean;
|
|
182
|
-
ignoreWhitespace?: boolean;
|
|
183
|
-
}];
|
|
184
|
-
//#endregion
|
|
185
|
-
//#region src/rules/no-event-handlers.d.ts
|
|
186
|
-
type Options$1 = [{
|
|
187
|
-
ignores?: string[];
|
|
188
|
-
}];
|
|
189
|
-
//#endregion
|
|
190
|
-
//#region src/rules/no-invalid-role.d.ts
|
|
191
|
-
type Options = [{
|
|
192
|
-
roles?: string[];
|
|
193
|
-
}];
|
|
194
|
-
//#endregion
|
|
195
4
|
//#region src/rules/index.d.ts
|
|
196
5
|
declare const rules: {
|
|
197
|
-
'no-
|
|
198
|
-
'no-
|
|
199
|
-
'no-
|
|
200
|
-
'no-
|
|
201
|
-
'no-
|
|
202
|
-
'no-
|
|
203
|
-
'no-empty-
|
|
204
|
-
'no-empty-
|
|
205
|
-
'no-
|
|
206
|
-
'no-
|
|
207
|
-
'no-
|
|
208
|
-
'no-
|
|
209
|
-
'
|
|
6
|
+
'no-base64-data-url': eslint.Rule.RuleModule;
|
|
7
|
+
'no-comments': eslint.Rule.RuleModule;
|
|
8
|
+
'no-deprecated': eslint.Rule.RuleModule;
|
|
9
|
+
'no-doctype': eslint.Rule.RuleModule;
|
|
10
|
+
'no-duplicate-ids': eslint.Rule.RuleModule;
|
|
11
|
+
'no-elements': eslint.Rule.RuleModule;
|
|
12
|
+
'no-empty-container': eslint.Rule.RuleModule;
|
|
13
|
+
'no-empty-desc': eslint.Rule.RuleModule;
|
|
14
|
+
'no-empty-groups': eslint.Rule.RuleModule;
|
|
15
|
+
'no-empty-text': eslint.Rule.RuleModule;
|
|
16
|
+
'no-empty-title': eslint.Rule.RuleModule;
|
|
17
|
+
'no-event-handlers': eslint.Rule.RuleModule;
|
|
18
|
+
'no-inline-styles': eslint.Rule.RuleModule;
|
|
19
|
+
'no-invalid-role': eslint.Rule.RuleModule;
|
|
20
|
+
'no-script-tags': eslint.Rule.RuleModule;
|
|
21
|
+
'require-viewbox': eslint.Rule.RuleModule;
|
|
210
22
|
};
|
|
211
23
|
//#endregion
|
|
212
24
|
//#region src/types/plugin.d.ts
|
|
@@ -221,11 +33,6 @@ interface PluginSVG {
|
|
|
221
33
|
};
|
|
222
34
|
}
|
|
223
35
|
//#endregion
|
|
224
|
-
//#region src/dts.d.ts
|
|
225
|
-
type RuleDefinitions = typeof rules;
|
|
226
|
-
type RuleOptions = { [K in keyof RuleDefinitions]: RuleDefinitions[K]['defaultOptions'] };
|
|
227
|
-
type Rules = { [K in keyof RuleOptions]: Linter.RuleEntry<RuleOptions[K]> };
|
|
228
|
-
//#endregion
|
|
229
36
|
//#region src/meta.d.ts
|
|
230
37
|
declare const meta: {
|
|
231
38
|
name: string;
|
|
@@ -248,4 +55,4 @@ declare const configs: PluginSVG['configs'];
|
|
|
248
55
|
*/
|
|
249
56
|
declare const plugin: PluginSVG;
|
|
250
57
|
//#endregion
|
|
251
|
-
export {
|
|
58
|
+
export { configs, plugin as default, plugin, meta, recommended, rules };
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const recommended = [{
|
|
|
16
16
|
rules: {
|
|
17
17
|
"svg/no-deprecated": "error",
|
|
18
18
|
"svg/no-doctype": "error",
|
|
19
|
+
"svg/no-duplicate-ids": "error",
|
|
19
20
|
"svg/no-empty-container": "error",
|
|
20
21
|
"svg/no-empty-desc": "error",
|
|
21
22
|
"svg/no-empty-groups": "error",
|
|
@@ -33,7 +34,7 @@ const configs = { recommended };
|
|
|
33
34
|
//#endregion
|
|
34
35
|
//#region package.json
|
|
35
36
|
var name = "eslint-plugin-svg";
|
|
36
|
-
var version = "0.0.
|
|
37
|
+
var version = "0.0.6";
|
|
37
38
|
|
|
38
39
|
//#endregion
|
|
39
40
|
//#region src/meta.ts
|
|
@@ -42,6 +43,248 @@ const meta = {
|
|
|
42
43
|
version
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/utils/merge.ts
|
|
48
|
+
/**
|
|
49
|
+
* Check if the variable contains an object strictly rejecting arrays
|
|
50
|
+
* @returns `true` if obj is an object
|
|
51
|
+
*/
|
|
52
|
+
function isObjectNotArray(obj) {
|
|
53
|
+
return typeof obj === "object" && obj != null && !Array.isArray(obj);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Pure function - doesn't mutate either parameter!
|
|
57
|
+
* Merges two objects together deeply, overwriting the properties in first with the properties in second
|
|
58
|
+
* @param first - The first object
|
|
59
|
+
* @param second - The second object
|
|
60
|
+
* @returns a new object
|
|
61
|
+
*/
|
|
62
|
+
function deepMerge(first = {}, second = {}) {
|
|
63
|
+
const keys = new Set(Object.keys(first).concat(Object.keys(second)));
|
|
64
|
+
return Array.from(keys).reduce((acc, key) => {
|
|
65
|
+
const firstHasKey = key in first;
|
|
66
|
+
const secondHasKey = key in second;
|
|
67
|
+
const firstValue = first[key];
|
|
68
|
+
const secondValue = second[key];
|
|
69
|
+
if (firstHasKey && secondHasKey) if (isObjectNotArray(firstValue) && isObjectNotArray(secondValue)) acc[key] = deepMerge(firstValue, secondValue);
|
|
70
|
+
else acc[key] = secondValue;
|
|
71
|
+
else if (firstHasKey) acc[key] = firstValue;
|
|
72
|
+
else acc[key] = secondValue;
|
|
73
|
+
return acc;
|
|
74
|
+
}, {});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/utils/createRule.ts
|
|
79
|
+
function createRule({ create, defaultOptions, meta }) {
|
|
80
|
+
const resolvedDefaultOptions = toArray(meta.defaultOptions);
|
|
81
|
+
return {
|
|
82
|
+
create: ((context) => {
|
|
83
|
+
const optionsCount = Math.max(context.options.length, defaultOptions.length);
|
|
84
|
+
return create(context, Array.from({ length: optionsCount }, (_, i) => {
|
|
85
|
+
/* v8 ignore start */
|
|
86
|
+
if (isObjectNotArray(context.options[i]) && isObjectNotArray(resolvedDefaultOptions[i])) return deepMerge(resolvedDefaultOptions[i], context.options[i]);
|
|
87
|
+
return context.options[i] ?? resolvedDefaultOptions[i];
|
|
88
|
+
/* v8 ignore stop */
|
|
89
|
+
}));
|
|
90
|
+
}),
|
|
91
|
+
meta: {
|
|
92
|
+
...meta,
|
|
93
|
+
defaultOptions: resolvedDefaultOptions
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function RuleCreator(urlCreator) {
|
|
98
|
+
return function createNamedRule({ name, meta, ...rule }) {
|
|
99
|
+
return createRule({
|
|
100
|
+
meta: {
|
|
101
|
+
...meta,
|
|
102
|
+
docs: {
|
|
103
|
+
...meta.docs,
|
|
104
|
+
url: urlCreator(name)
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
...rule
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const createESLintRule = RuleCreator((ruleName) => `https://eslint-plugin-svg.ntnyq.com/rules/${ruleName}.html`);
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/utils/resolveOptions.ts
|
|
115
|
+
/**
|
|
116
|
+
* resolve rule options
|
|
117
|
+
*
|
|
118
|
+
* @param options - context.options
|
|
119
|
+
* @param defaultOptions - default options
|
|
120
|
+
* @returns - resolved options
|
|
121
|
+
*/
|
|
122
|
+
function resolveOptions(options, defaultOptions) {
|
|
123
|
+
/* v8 ignore next guard by eslint */
|
|
124
|
+
return options?.[0] || defaultOptions;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/rules/no-base64-data-url.ts
|
|
129
|
+
const RULE_NAME$15 = "no-base64-data-url";
|
|
130
|
+
const defaultOptions$5 = {
|
|
131
|
+
mode: "base64-only",
|
|
132
|
+
attributes: "*",
|
|
133
|
+
ignoreAttributes: [],
|
|
134
|
+
checkUrlFunction: true,
|
|
135
|
+
allowMimeTypes: []
|
|
136
|
+
};
|
|
137
|
+
function normalize(value) {
|
|
138
|
+
return value.trim().toLowerCase();
|
|
139
|
+
}
|
|
140
|
+
function parseDataUrl(value) {
|
|
141
|
+
const rawValue = value.trim();
|
|
142
|
+
if (!rawValue.toLowerCase().startsWith("data:")) return {
|
|
143
|
+
isDataUrl: false,
|
|
144
|
+
isBase64: false,
|
|
145
|
+
mimeType: ""
|
|
146
|
+
};
|
|
147
|
+
const commaIndex = rawValue.indexOf(",");
|
|
148
|
+
if (commaIndex === -1) return {
|
|
149
|
+
isDataUrl: false,
|
|
150
|
+
isBase64: false,
|
|
151
|
+
mimeType: ""
|
|
152
|
+
};
|
|
153
|
+
const parts = rawValue.slice(5, commaIndex).split(";").map((part) => normalize(part));
|
|
154
|
+
const firstPart = parts[0] ?? "";
|
|
155
|
+
const mimeType = firstPart.includes("/") ? firstPart : "";
|
|
156
|
+
return {
|
|
157
|
+
isDataUrl: true,
|
|
158
|
+
isBase64: parts.includes("base64"),
|
|
159
|
+
mimeType
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function extractUrlValues(value) {
|
|
163
|
+
const candidates = [];
|
|
164
|
+
const lowerValue = value.toLowerCase();
|
|
165
|
+
let index = 0;
|
|
166
|
+
while (index < value.length) {
|
|
167
|
+
const openIndex = lowerValue.indexOf("url(", index);
|
|
168
|
+
if (openIndex === -1) break;
|
|
169
|
+
const contentStart = openIndex + 4;
|
|
170
|
+
const closeIndex = value.indexOf(")", contentStart);
|
|
171
|
+
if (closeIndex === -1) break;
|
|
172
|
+
let content = value.slice(contentStart, closeIndex).trim();
|
|
173
|
+
if (content.startsWith("\"") && content.endsWith("\"") || content.startsWith("'") && content.endsWith("'")) content = content.slice(1, -1).trim();
|
|
174
|
+
candidates.push(content);
|
|
175
|
+
index = closeIndex + 1;
|
|
176
|
+
}
|
|
177
|
+
return candidates;
|
|
178
|
+
}
|
|
179
|
+
function isDisallowedDataUrl(value, mode, allowedMimeTypes) {
|
|
180
|
+
const parsed = parseDataUrl(value);
|
|
181
|
+
if (!parsed.isDataUrl) return false;
|
|
182
|
+
if (parsed.mimeType && allowedMimeTypes.has(parsed.mimeType)) return false;
|
|
183
|
+
if (mode === "any-data-url") return true;
|
|
184
|
+
return parsed.isBase64;
|
|
185
|
+
}
|
|
186
|
+
var no_base64_data_url_default = createESLintRule({
|
|
187
|
+
name: RULE_NAME$15,
|
|
188
|
+
meta: {
|
|
189
|
+
type: "suggestion",
|
|
190
|
+
docs: {
|
|
191
|
+
description: "disallow base64 data URLs in SVG attributes",
|
|
192
|
+
recommended: false
|
|
193
|
+
},
|
|
194
|
+
schema: [{
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
mode: {
|
|
198
|
+
type: "string",
|
|
199
|
+
description: `whether to disallow only base64 data URLs or all data URLs`,
|
|
200
|
+
enum: ["base64-only", "any-data-url"]
|
|
201
|
+
},
|
|
202
|
+
attributes: {
|
|
203
|
+
description: "attributes to check",
|
|
204
|
+
oneOf: [{
|
|
205
|
+
description: "check all attributes",
|
|
206
|
+
type: "string",
|
|
207
|
+
const: "*"
|
|
208
|
+
}, {
|
|
209
|
+
description: "check only listed attributes",
|
|
210
|
+
type: "array",
|
|
211
|
+
items: { type: "string" },
|
|
212
|
+
uniqueItems: true
|
|
213
|
+
}]
|
|
214
|
+
},
|
|
215
|
+
ignoreAttributes: {
|
|
216
|
+
type: "array",
|
|
217
|
+
description: "attribute names to ignore",
|
|
218
|
+
items: { type: "string" },
|
|
219
|
+
uniqueItems: true
|
|
220
|
+
},
|
|
221
|
+
checkUrlFunction: {
|
|
222
|
+
type: "boolean",
|
|
223
|
+
description: "whether to check data URLs wrapped by url(...)"
|
|
224
|
+
},
|
|
225
|
+
allowMimeTypes: {
|
|
226
|
+
type: "array",
|
|
227
|
+
description: "MIME types to allow for data URLs",
|
|
228
|
+
items: { type: "string" },
|
|
229
|
+
uniqueItems: true
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
additionalProperties: false
|
|
233
|
+
}],
|
|
234
|
+
messages: { invalid: `Data URL usage is not allowed in attribute '{{name}}'` }
|
|
235
|
+
},
|
|
236
|
+
defaultOptions: [defaultOptions$5],
|
|
237
|
+
create(context) {
|
|
238
|
+
const resolvedOptions = resolveOptions(context.options, defaultOptions$5);
|
|
239
|
+
const { mode, attributes, ignoreAttributes, checkUrlFunction, allowMimeTypes } = {
|
|
240
|
+
...defaultOptions$5,
|
|
241
|
+
...resolvedOptions
|
|
242
|
+
};
|
|
243
|
+
let allowedAttributes = null;
|
|
244
|
+
if (attributes !== "*") allowedAttributes = new Set(attributes.map((name) => normalize(name)));
|
|
245
|
+
const ignoredAttributes = new Set(ignoreAttributes.map((name) => normalize(name)));
|
|
246
|
+
const allowedMimeTypes = new Set(allowMimeTypes.map((type) => normalize(type)));
|
|
247
|
+
return { Attribute(node) {
|
|
248
|
+
if (!node.value) return;
|
|
249
|
+
const name = normalize(node.key.value);
|
|
250
|
+
if (ignoredAttributes.has(name)) return;
|
|
251
|
+
if (allowedAttributes && !allowedAttributes.has(name)) return;
|
|
252
|
+
const valuesToCheck = [node.value.value];
|
|
253
|
+
if (checkUrlFunction) valuesToCheck.push(...extractUrlValues(node.value.value));
|
|
254
|
+
if (valuesToCheck.some((value) => isDisallowedDataUrl(value, mode, allowedMimeTypes))) context.report({
|
|
255
|
+
node: node.value,
|
|
256
|
+
messageId: "invalid",
|
|
257
|
+
data: { name: node.key.value }
|
|
258
|
+
});
|
|
259
|
+
} };
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/rules/no-comments.ts
|
|
265
|
+
const RULE_NAME$14 = "no-comments";
|
|
266
|
+
var no_comments_default = createESLintRule({
|
|
267
|
+
name: RULE_NAME$14,
|
|
268
|
+
meta: {
|
|
269
|
+
type: "suggestion",
|
|
270
|
+
docs: {
|
|
271
|
+
description: "disallow comments in SVG files",
|
|
272
|
+
recommended: false
|
|
273
|
+
},
|
|
274
|
+
schema: [],
|
|
275
|
+
messages: { invalid: "Comments are not allowed in SVG files" }
|
|
276
|
+
},
|
|
277
|
+
defaultOptions: [],
|
|
278
|
+
create(context) {
|
|
279
|
+
return { Comment(node) {
|
|
280
|
+
context.report({
|
|
281
|
+
node,
|
|
282
|
+
messageId: "invalid"
|
|
283
|
+
});
|
|
284
|
+
} };
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
45
288
|
//#endregion
|
|
46
289
|
//#region src/constants/roles.ts
|
|
47
290
|
/**
|
|
@@ -236,93 +479,12 @@ const DEPRECATED_ELEMENTS = [
|
|
|
236
479
|
"vkern"
|
|
237
480
|
];
|
|
238
481
|
|
|
239
|
-
//#endregion
|
|
240
|
-
//#region src/utils/merge.ts
|
|
241
|
-
/**
|
|
242
|
-
* Check if the variable contains an object strictly rejecting arrays
|
|
243
|
-
* @returns `true` if obj is an object
|
|
244
|
-
*/
|
|
245
|
-
function isObjectNotArray(obj) {
|
|
246
|
-
return typeof obj === "object" && obj != null && !Array.isArray(obj);
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Pure function - doesn't mutate either parameter!
|
|
250
|
-
* Merges two objects together deeply, overwriting the properties in first with the properties in second
|
|
251
|
-
* @param first - The first object
|
|
252
|
-
* @param second - The second object
|
|
253
|
-
* @returns a new object
|
|
254
|
-
*/
|
|
255
|
-
function deepMerge(first = {}, second = {}) {
|
|
256
|
-
const keys = new Set(Object.keys(first).concat(Object.keys(second)));
|
|
257
|
-
return Array.from(keys).reduce((acc, key) => {
|
|
258
|
-
const firstHasKey = key in first;
|
|
259
|
-
const secondHasKey = key in second;
|
|
260
|
-
const firstValue = first[key];
|
|
261
|
-
const secondValue = second[key];
|
|
262
|
-
if (firstHasKey && secondHasKey) if (isObjectNotArray(firstValue) && isObjectNotArray(secondValue)) acc[key] = deepMerge(firstValue, secondValue);
|
|
263
|
-
else acc[key] = secondValue;
|
|
264
|
-
else if (firstHasKey) acc[key] = firstValue;
|
|
265
|
-
else acc[key] = secondValue;
|
|
266
|
-
return acc;
|
|
267
|
-
}, {});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
//#endregion
|
|
271
|
-
//#region src/utils/createRule.ts
|
|
272
|
-
function createRule({ create, defaultOptions: defaultOptions$5, meta: meta$1 }) {
|
|
273
|
-
return {
|
|
274
|
-
create: ((context) => {
|
|
275
|
-
const optionsCount = Math.max(context.options.length, defaultOptions$5.length);
|
|
276
|
-
return create(context, Array.from({ length: optionsCount }, (_, i) => {
|
|
277
|
-
/* v8 ignore start */
|
|
278
|
-
if (isObjectNotArray(context.options[i]) && isObjectNotArray(defaultOptions$5[i])) return deepMerge(defaultOptions$5[i], context.options[i]);
|
|
279
|
-
return context.options[i] ?? defaultOptions$5[i];
|
|
280
|
-
/* v8 ignore stop */
|
|
281
|
-
}));
|
|
282
|
-
}),
|
|
283
|
-
defaultOptions: defaultOptions$5,
|
|
284
|
-
meta: {
|
|
285
|
-
...meta$1,
|
|
286
|
-
defaultOptions: defaultOptions$5
|
|
287
|
-
}
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
function RuleCreator(urlCreator) {
|
|
291
|
-
return function createNamedRule({ name: name$1, meta: meta$1, ...rule }) {
|
|
292
|
-
return createRule({
|
|
293
|
-
meta: {
|
|
294
|
-
...meta$1,
|
|
295
|
-
docs: {
|
|
296
|
-
...meta$1.docs,
|
|
297
|
-
url: urlCreator(name$1)
|
|
298
|
-
}
|
|
299
|
-
},
|
|
300
|
-
...rule
|
|
301
|
-
});
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
const createESLintRule = RuleCreator((ruleName) => `https://eslint-plugin-svg.ntnyq.com/rules/${ruleName}.html`);
|
|
305
|
-
|
|
306
|
-
//#endregion
|
|
307
|
-
//#region src/utils/resolveOptions.ts
|
|
308
|
-
/**
|
|
309
|
-
* resolve rule options
|
|
310
|
-
*
|
|
311
|
-
* @param options - context.options
|
|
312
|
-
* @param defaultOptions - default options
|
|
313
|
-
* @returns - resolved options
|
|
314
|
-
*/
|
|
315
|
-
function resolveOptions(options, defaultOptions$5) {
|
|
316
|
-
/* v8 ignore next guard by eslint */
|
|
317
|
-
return options?.[0] || defaultOptions$5;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
482
|
//#endregion
|
|
321
483
|
//#region src/rules/no-deprecated.ts
|
|
322
|
-
const RULE_NAME$
|
|
484
|
+
const RULE_NAME$13 = "no-deprecated";
|
|
323
485
|
const defaultOptions$4 = {};
|
|
324
486
|
var no_deprecated_default = createESLintRule({
|
|
325
|
-
name: RULE_NAME$
|
|
487
|
+
name: RULE_NAME$13,
|
|
326
488
|
meta: {
|
|
327
489
|
type: "suggestion",
|
|
328
490
|
docs: {
|
|
@@ -356,9 +518,9 @@ var no_deprecated_default = createESLintRule({
|
|
|
356
518
|
|
|
357
519
|
//#endregion
|
|
358
520
|
//#region src/rules/no-doctype.ts
|
|
359
|
-
const RULE_NAME$
|
|
521
|
+
const RULE_NAME$12 = "no-doctype";
|
|
360
522
|
var no_doctype_default = createESLintRule({
|
|
361
|
-
name: RULE_NAME$
|
|
523
|
+
name: RULE_NAME$12,
|
|
362
524
|
meta: {
|
|
363
525
|
type: "suggestion",
|
|
364
526
|
docs: {
|
|
@@ -381,6 +543,37 @@ var no_doctype_default = createESLintRule({
|
|
|
381
543
|
}
|
|
382
544
|
});
|
|
383
545
|
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/rules/no-duplicate-ids.ts
|
|
548
|
+
const RULE_NAME$11 = "no-duplicate-ids";
|
|
549
|
+
var no_duplicate_ids_default = createESLintRule({
|
|
550
|
+
name: RULE_NAME$11,
|
|
551
|
+
meta: {
|
|
552
|
+
type: "problem",
|
|
553
|
+
docs: {
|
|
554
|
+
description: "disallow duplicate id attributes in SVG elements",
|
|
555
|
+
recommended: true
|
|
556
|
+
},
|
|
557
|
+
schema: [],
|
|
558
|
+
messages: { duplicate: `Duplicate id '{{id}}' found` }
|
|
559
|
+
},
|
|
560
|
+
defaultOptions: [],
|
|
561
|
+
create(context) {
|
|
562
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
563
|
+
return { Tag(node) {
|
|
564
|
+
const idAttribute = node.attributes?.find((attr) => attr.key.value === "id");
|
|
565
|
+
if (!idAttribute || !idAttribute.value || !idAttribute.value.value) return;
|
|
566
|
+
const id = idAttribute.value.value;
|
|
567
|
+
if (seenIds.has(id)) context.report({
|
|
568
|
+
node: idAttribute,
|
|
569
|
+
messageId: "duplicate",
|
|
570
|
+
data: { id }
|
|
571
|
+
});
|
|
572
|
+
else seenIds.add(id);
|
|
573
|
+
} };
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
384
577
|
//#endregion
|
|
385
578
|
//#region src/rules/no-elements.ts
|
|
386
579
|
const RULE_NAME$10 = "no-elements";
|
|
@@ -608,15 +801,15 @@ var no_event_handlers_default = createESLintRule({
|
|
|
608
801
|
create(context) {
|
|
609
802
|
const { ignores = [] } = resolveOptions(context.options, defaultOptions$1);
|
|
610
803
|
const ignorePatterns = ignores.map((pattern) => new RegExp(pattern));
|
|
611
|
-
const isIgnored = (name
|
|
804
|
+
const isIgnored = (name) => ignorePatterns.some((pattern) => pattern.test(name));
|
|
612
805
|
return { Attribute(node) {
|
|
613
|
-
const name
|
|
614
|
-
if (!name
|
|
615
|
-
if (isIgnored(name
|
|
806
|
+
const name = node.key.value;
|
|
807
|
+
if (!name || !name.toLowerCase().startsWith("on")) return;
|
|
808
|
+
if (isIgnored(name)) return;
|
|
616
809
|
context.report({
|
|
617
810
|
node: node.value ?? node.key,
|
|
618
811
|
messageId: "invalid",
|
|
619
|
-
data: { name
|
|
812
|
+
data: { name }
|
|
620
813
|
});
|
|
621
814
|
} };
|
|
622
815
|
}
|
|
@@ -753,8 +946,11 @@ var require_viewbox_default = createESLintRule({
|
|
|
753
946
|
//#endregion
|
|
754
947
|
//#region src/rules/index.ts
|
|
755
948
|
const rules = {
|
|
949
|
+
"no-base64-data-url": no_base64_data_url_default,
|
|
950
|
+
"no-comments": no_comments_default,
|
|
756
951
|
"no-deprecated": no_deprecated_default,
|
|
757
952
|
"no-doctype": no_doctype_default,
|
|
953
|
+
"no-duplicate-ids": no_duplicate_ids_default,
|
|
758
954
|
"no-elements": no_elements_default,
|
|
759
955
|
"no-empty-container": no_empty_container_default,
|
|
760
956
|
"no-empty-desc": no_empty_desc_default,
|
|
@@ -781,7 +977,6 @@ const plugin = {
|
|
|
781
977
|
rules,
|
|
782
978
|
configs
|
|
783
979
|
};
|
|
784
|
-
var src_default = plugin;
|
|
785
980
|
|
|
786
981
|
//#endregion
|
|
787
|
-
export { configs,
|
|
982
|
+
export { configs, plugin as default, plugin, meta, recommended, rules };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-svg",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.6",
|
|
5
5
|
"description": "Rules for consistent, readable and valid SVG files.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"eslint",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"dist"
|
|
32
32
|
],
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"eslint": "^9.5.0"
|
|
34
|
+
"eslint": "^9.5.0 || ^10.0.0"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@ntnyq/utils": "^0.11.0",
|
|
@@ -40,27 +40,27 @@
|
|
|
40
40
|
"uncase": "^0.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@ntnyq/eslint-config": "^6.0.0-beta.
|
|
43
|
+
"@ntnyq/eslint-config": "^6.0.0-beta.9",
|
|
44
44
|
"@ntnyq/prettier-config": "^3.0.1",
|
|
45
|
-
"@types/node": "^25.0
|
|
46
|
-
"bumpp": "^10.4.
|
|
47
|
-
"eslint": "^
|
|
48
|
-
"eslint-plugin-eslint-plugin": "^7.3.
|
|
49
|
-
"eslint-vitest-rule-tester": "^3.0
|
|
45
|
+
"@types/node": "^25.3.0",
|
|
46
|
+
"bumpp": "^10.4.1",
|
|
47
|
+
"eslint": "^10.0.2",
|
|
48
|
+
"eslint-plugin-eslint-plugin": "^7.3.1",
|
|
49
|
+
"eslint-vitest-rule-tester": "^3.1.0",
|
|
50
50
|
"husky": "^9.1.7",
|
|
51
51
|
"nano-staged": "^0.9.0",
|
|
52
52
|
"npm-run-all2": "^8.0.4",
|
|
53
53
|
"prettier": "^3.8.1",
|
|
54
54
|
"tinyglobby": "^0.2.15",
|
|
55
|
-
"tsdown": "^0.20.
|
|
55
|
+
"tsdown": "^0.20.3",
|
|
56
56
|
"typescript": "^5.9.3",
|
|
57
|
-
"vitest": "^4.0.
|
|
57
|
+
"vitest": "^4.0.18"
|
|
58
58
|
},
|
|
59
59
|
"engines": {
|
|
60
60
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
|
61
61
|
},
|
|
62
62
|
"nano-staged": {
|
|
63
|
-
"*.{js,ts,mjs,
|
|
63
|
+
"*.{js,ts,mjs,tsx,vue,md,svg,yml,yaml,toml,json}": "eslint --fix",
|
|
64
64
|
"*.{css,scss,html}": "prettier -uw"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|