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 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 { AST } from "svg-eslint-parser";
2
- import { Linter, Rule } from "eslint";
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-deprecated': RuleModule<"deprecatedElement", Options$4, unknown>;
198
- 'no-doctype': RuleModule<"invalid", [], unknown>;
199
- 'no-elements': RuleModule<"invalid", Options$3, unknown>;
200
- 'no-empty-container': RuleModule<"invalid", Options$2, unknown>;
201
- 'no-empty-desc': RuleModule<"invalid", [], unknown>;
202
- 'no-empty-groups': RuleModule<"invalid", [], unknown>;
203
- 'no-empty-text': RuleModule<"invalid", [], unknown>;
204
- 'no-empty-title': RuleModule<"invalid", [], unknown>;
205
- 'no-event-handlers': RuleModule<"invalid", Options$1, unknown>;
206
- 'no-inline-styles': RuleModule<"invalid", [], unknown>;
207
- 'no-invalid-role': RuleModule<"invalid", Options, unknown>;
208
- 'no-script-tags': RuleModule<"invalid", [], unknown>;
209
- 'require-viewbox': RuleModule<"missing", [], unknown>;
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 { RuleOptions, Rules, configs, plugin as default, plugin, meta, recommended, rules };
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.5";
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$12 = "no-deprecated";
484
+ const RULE_NAME$13 = "no-deprecated";
323
485
  const defaultOptions$4 = {};
324
486
  var no_deprecated_default = createESLintRule({
325
- name: RULE_NAME$12,
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$11 = "no-doctype";
521
+ const RULE_NAME$12 = "no-doctype";
360
522
  var no_doctype_default = createESLintRule({
361
- name: RULE_NAME$11,
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$1) => ignorePatterns.some((pattern) => pattern.test(name$1));
804
+ const isIgnored = (name) => ignorePatterns.some((pattern) => pattern.test(name));
612
805
  return { Attribute(node) {
613
- const name$1 = node.key.value;
614
- if (!name$1 || !name$1.toLowerCase().startsWith("on")) return;
615
- if (isIgnored(name$1)) return;
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: name$1 }
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, src_default as default, meta, plugin, recommended, rules };
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.5",
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.2",
43
+ "@ntnyq/eslint-config": "^6.0.0-beta.9",
44
44
  "@ntnyq/prettier-config": "^3.0.1",
45
- "@types/node": "^25.0.10",
46
- "bumpp": "^10.4.0",
47
- "eslint": "^9.39.2",
48
- "eslint-plugin-eslint-plugin": "^7.3.0",
49
- "eslint-vitest-rule-tester": "^3.0.1",
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.0-beta.4",
55
+ "tsdown": "^0.20.3",
56
56
  "typescript": "^5.9.3",
57
- "vitest": "^4.0.17"
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,cjs,vue,md,svg,yml,yaml,toml,json}": "eslint --fix",
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": {