@herb-tools/linter 0.4.1 → 0.4.3
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 +16 -4
- package/dist/herb-lint.js +557 -122
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +454 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +448 -69
- package/dist/index.js.map +1 -1
- package/dist/package.json +4 -4
- package/dist/src/cli/file-processor.js +2 -4
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/default-rules.js +8 -0
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/linter.js +37 -6
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-no-empty-tags.js +4 -3
- package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
- package/dist/src/rules/erb-no-output-control-flow.js +4 -3
- package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js +93 -0
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -0
- package/dist/src/rules/erb-require-whitespace-inside-tags.js +22 -5
- package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
- package/dist/src/rules/erb-requires-trailing-newline.js +22 -0
- package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -0
- package/dist/src/rules/html-anchor-require-href.js +4 -3
- package/dist/src/rules/html-anchor-require-href.js.map +1 -1
- package/dist/src/rules/html-aria-attribute-must-be-valid.js +4 -3
- package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-aria-level-must-be-valid.js +27 -0
- package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -0
- package/dist/src/rules/html-aria-role-heading-requires-level.js +4 -3
- package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
- package/dist/src/rules/html-aria-role-must-be-valid.js +4 -3
- package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
- package/dist/src/rules/html-attribute-double-quotes.js +4 -3
- package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
- package/dist/src/rules/html-attribute-values-require-quotes.js +4 -3
- package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
- package/dist/src/rules/html-boolean-attributes-no-value.js +4 -3
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-img-require-alt.js +4 -3
- package/dist/src/rules/html-img-require-alt.js.map +1 -1
- package/dist/src/rules/html-no-block-inside-inline.js +4 -3
- package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-attributes.js +4 -3
- package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +4 -3
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-empty-headings.js +4 -3
- package/dist/src/rules/html-no-empty-headings.js.map +1 -1
- package/dist/src/rules/html-no-nested-links.js +4 -3
- package/dist/src/rules/html-no-nested-links.js.map +1 -1
- package/dist/src/rules/html-tag-name-lowercase.js +21 -11
- package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
- package/dist/src/rules/index.js +4 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/rule-utils.js +168 -2
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +58 -0
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -0
- package/dist/src/types.js +15 -1
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/linter.d.ts +20 -5
- package/dist/types/rules/erb-no-empty-tags.d.ts +4 -3
- package/dist/types/rules/erb-no-output-control-flow.d.ts +4 -3
- package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +7 -0
- package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +4 -3
- package/dist/types/rules/erb-requires-trailing-newline.d.ts +6 -0
- package/dist/types/rules/html-anchor-require-href.d.ts +4 -3
- package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +4 -3
- package/dist/types/rules/html-aria-level-must-be-valid.d.ts +7 -0
- package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +4 -3
- package/dist/types/rules/html-aria-role-must-be-valid.d.ts +4 -3
- package/dist/types/rules/html-attribute-double-quotes.d.ts +4 -3
- package/dist/types/rules/html-attribute-values-require-quotes.d.ts +4 -3
- package/dist/types/rules/html-boolean-attributes-no-value.d.ts +4 -3
- package/dist/types/rules/html-img-require-alt.d.ts +4 -3
- package/dist/types/rules/html-no-block-inside-inline.d.ts +4 -3
- package/dist/types/rules/html-no-duplicate-attributes.d.ts +4 -3
- package/dist/types/rules/html-no-duplicate-ids.d.ts +4 -3
- package/dist/types/rules/html-no-empty-headings.d.ts +4 -3
- package/dist/types/rules/html-no-nested-links.d.ts +4 -3
- package/dist/types/rules/html-tag-name-lowercase.d.ts +4 -3
- package/dist/types/rules/index.d.ts +4 -0
- package/dist/types/rules/rule-utils.d.ts +82 -4
- package/dist/types/rules/svg-tag-name-capitalization.d.ts +7 -0
- package/dist/types/src/linter.d.ts +20 -5
- package/dist/types/src/rules/erb-no-empty-tags.d.ts +4 -3
- package/dist/types/src/rules/erb-no-output-control-flow.d.ts +4 -3
- package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +7 -0
- package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +4 -3
- package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +6 -0
- package/dist/types/src/rules/html-anchor-require-href.d.ts +4 -3
- package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +4 -3
- package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +7 -0
- package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +4 -3
- package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +4 -3
- package/dist/types/src/rules/html-attribute-double-quotes.d.ts +4 -3
- package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +4 -3
- package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +4 -3
- package/dist/types/src/rules/html-img-require-alt.d.ts +4 -3
- package/dist/types/src/rules/html-no-block-inside-inline.d.ts +4 -3
- package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +4 -3
- package/dist/types/src/rules/html-no-duplicate-ids.d.ts +4 -3
- package/dist/types/src/rules/html-no-empty-headings.d.ts +4 -3
- package/dist/types/src/rules/html-no-nested-links.d.ts +4 -3
- package/dist/types/src/rules/html-tag-name-lowercase.d.ts +4 -3
- package/dist/types/src/rules/index.d.ts +4 -0
- package/dist/types/src/rules/rule-utils.d.ts +82 -4
- package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +7 -0
- package/dist/types/src/types.d.ts +49 -6
- package/dist/types/types.d.ts +49 -6
- package/docs/rules/README.md +5 -1
- package/docs/rules/erb-prefer-image-tag-helper.md +65 -0
- package/docs/rules/erb-requires-trailing-newline.md +37 -0
- package/docs/rules/html-anchor-require-href.md +1 -1
- package/docs/rules/html-aria-level-must-be-valid.md +37 -0
- package/docs/rules/svg-tag-name-capitalization.md +57 -0
- package/package.json +4 -4
- package/src/cli/file-processor.ts +2 -4
- package/src/default-rules.ts +8 -0
- package/src/linter.ts +42 -8
- package/src/rules/erb-no-empty-tags.ts +5 -4
- package/src/rules/erb-no-output-control-flow.ts +6 -4
- package/src/rules/erb-prefer-image-tag-helper.ts +124 -0
- package/src/rules/erb-require-whitespace-inside-tags.ts +38 -6
- package/src/rules/erb-requires-trailing-newline.ts +27 -0
- package/src/rules/html-anchor-require-href.ts +5 -4
- package/src/rules/html-aria-attribute-must-be-valid.ts +5 -5
- package/src/rules/html-aria-level-must-be-valid.ts +42 -0
- package/src/rules/html-aria-role-heading-requires-level.ts +5 -4
- package/src/rules/html-aria-role-must-be-valid.ts +5 -4
- package/src/rules/html-attribute-double-quotes.ts +5 -4
- package/src/rules/html-attribute-values-require-quotes.ts +5 -4
- package/src/rules/html-boolean-attributes-no-value.ts +5 -4
- package/src/rules/html-img-require-alt.ts +5 -4
- package/src/rules/html-no-block-inside-inline.ts +5 -4
- package/src/rules/html-no-duplicate-attributes.ts +5 -4
- package/src/rules/html-no-duplicate-ids.ts +5 -5
- package/src/rules/html-no-empty-headings.ts +5 -4
- package/src/rules/html-no-nested-links.ts +5 -4
- package/src/rules/html-tag-name-lowercase.ts +29 -13
- package/src/rules/index.ts +4 -0
- package/src/rules/rule-utils.ts +203 -4
- package/src/rules/svg-tag-name-capitalization.ts +74 -0
- package/src/types.ts +60 -6
package/src/rules/rule-utils.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Visitor
|
|
2
|
+
Visitor,
|
|
3
|
+
Position,
|
|
4
|
+
Location
|
|
3
5
|
} from "@herb-tools/core"
|
|
4
6
|
|
|
5
7
|
import type {
|
|
@@ -10,9 +12,11 @@ import type {
|
|
|
10
12
|
HTMLOpenTagNode,
|
|
11
13
|
HTMLSelfCloseTagNode,
|
|
12
14
|
LiteralNode,
|
|
13
|
-
|
|
15
|
+
LexResult,
|
|
16
|
+
Token
|
|
14
17
|
} from "@herb-tools/core"
|
|
15
|
-
import type { LintOffense, LintSeverity, } from "../types.js"
|
|
18
|
+
import type { LintOffense, LintSeverity, LintContext } from "../types.js"
|
|
19
|
+
import { DEFAULT_LINT_CONTEXT } from "../types.js"
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
22
|
* Base visitor class that provides common functionality for rule visitors
|
|
@@ -20,11 +24,13 @@ import type { LintOffense, LintSeverity, } from "../types.js"
|
|
|
20
24
|
export abstract class BaseRuleVisitor extends Visitor {
|
|
21
25
|
public readonly offenses: LintOffense[] = []
|
|
22
26
|
protected ruleName: string
|
|
27
|
+
protected context: LintContext
|
|
23
28
|
|
|
24
|
-
constructor(ruleName: string) {
|
|
29
|
+
constructor(ruleName: string, context?: Partial<LintContext>) {
|
|
25
30
|
super()
|
|
26
31
|
|
|
27
32
|
this.ruleName = ruleName
|
|
33
|
+
this.context = { ...DEFAULT_LINT_CONTEXT, ...context }
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/**
|
|
@@ -188,6 +194,53 @@ export const HTML_BOOLEAN_ATTRIBUTES = new Set([
|
|
|
188
194
|
|
|
189
195
|
export const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"])
|
|
190
196
|
|
|
197
|
+
/**
|
|
198
|
+
* SVG elements that use camelCase naming
|
|
199
|
+
*/
|
|
200
|
+
export const SVG_CAMEL_CASE_ELEMENTS = new Set([
|
|
201
|
+
"animateMotion",
|
|
202
|
+
"animateTransform",
|
|
203
|
+
"clipPath",
|
|
204
|
+
"feBlend",
|
|
205
|
+
"feColorMatrix",
|
|
206
|
+
"feComponentTransfer",
|
|
207
|
+
"feComposite",
|
|
208
|
+
"feConvolveMatrix",
|
|
209
|
+
"feDiffuseLighting",
|
|
210
|
+
"feDisplacementMap",
|
|
211
|
+
"feDistantLight",
|
|
212
|
+
"feDropShadow",
|
|
213
|
+
"feFlood",
|
|
214
|
+
"feFuncA",
|
|
215
|
+
"feFuncB",
|
|
216
|
+
"feFuncG",
|
|
217
|
+
"feFuncR",
|
|
218
|
+
"feGaussianBlur",
|
|
219
|
+
"feImage",
|
|
220
|
+
"feMerge",
|
|
221
|
+
"feMergeNode",
|
|
222
|
+
"feMorphology",
|
|
223
|
+
"feOffset",
|
|
224
|
+
"fePointLight",
|
|
225
|
+
"feSpecularLighting",
|
|
226
|
+
"feSpotLight",
|
|
227
|
+
"feTile",
|
|
228
|
+
"feTurbulence",
|
|
229
|
+
"foreignObject",
|
|
230
|
+
"glyphRef",
|
|
231
|
+
"linearGradient",
|
|
232
|
+
"radialGradient",
|
|
233
|
+
"textPath"
|
|
234
|
+
])
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Mapping from lowercase SVG element names to their correct camelCase versions
|
|
238
|
+
* Generated dynamically from SVG_CAMEL_CASE_ELEMENTS
|
|
239
|
+
*/
|
|
240
|
+
export const SVG_LOWERCASE_TO_CAMELCASE = new Map(
|
|
241
|
+
Array.from(SVG_CAMEL_CASE_ELEMENTS).map(element => [element.toLowerCase(), element])
|
|
242
|
+
)
|
|
243
|
+
|
|
191
244
|
export const VALID_ARIA_ROLES = new Set([
|
|
192
245
|
"banner", "complementary", "contentinfo", "form", "main", "navigation", "region", "search",
|
|
193
246
|
"article", "cell", "columnheader", "definition", "directory", "document", "feed", "figure",
|
|
@@ -252,6 +305,22 @@ export const ARIA_ATTRIBUTES = new Set([
|
|
|
252
305
|
'aria-valuetext',
|
|
253
306
|
])
|
|
254
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Helper function to create a location at the end of the source with a 1-character range
|
|
310
|
+
*/
|
|
311
|
+
export function createEndOfFileLocation(source: string): Location {
|
|
312
|
+
const lines = source.split('\n')
|
|
313
|
+
const lastLineNumber = lines.length
|
|
314
|
+
const lastLine = lines[lines.length - 1]
|
|
315
|
+
const lastColumnNumber = lastLine.length
|
|
316
|
+
|
|
317
|
+
const startColumn = lastColumnNumber > 0 ? lastColumnNumber - 1 : 0
|
|
318
|
+
const start = new Position(lastLineNumber, startColumn)
|
|
319
|
+
const end = new Position(lastLineNumber, lastColumnNumber)
|
|
320
|
+
|
|
321
|
+
return new Location(start, end)
|
|
322
|
+
}
|
|
323
|
+
|
|
255
324
|
/**
|
|
256
325
|
* Checks if an element is inline
|
|
257
326
|
*/
|
|
@@ -279,6 +348,10 @@ export function isBooleanAttribute(attributeName: string): boolean {
|
|
|
279
348
|
* and attribute iteration logic. Provides simplified interface with extracted attribute info.
|
|
280
349
|
*/
|
|
281
350
|
export abstract class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
351
|
+
constructor(ruleName: string, context?: Partial<LintContext>) {
|
|
352
|
+
super(ruleName, context)
|
|
353
|
+
}
|
|
354
|
+
|
|
282
355
|
visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
|
|
283
356
|
this.checkAttributesOnNode(node)
|
|
284
357
|
super.visitHTMLOpenTagNode(node)
|
|
@@ -336,3 +409,129 @@ export function forEachAttribute(
|
|
|
336
409
|
}
|
|
337
410
|
}
|
|
338
411
|
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Base lexer visitor class that provides common functionality for lexer-based rule visitors
|
|
415
|
+
*/
|
|
416
|
+
export abstract class BaseLexerRuleVisitor {
|
|
417
|
+
public readonly offenses: LintOffense[] = []
|
|
418
|
+
protected ruleName: string
|
|
419
|
+
protected context: LintContext
|
|
420
|
+
|
|
421
|
+
constructor(ruleName: string, context?: Partial<LintContext>) {
|
|
422
|
+
this.ruleName = ruleName
|
|
423
|
+
this.context = { ...DEFAULT_LINT_CONTEXT, ...context }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Helper method to create a lint offense for lexer rules
|
|
428
|
+
*/
|
|
429
|
+
protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
|
|
430
|
+
return {
|
|
431
|
+
rule: this.ruleName,
|
|
432
|
+
code: this.ruleName,
|
|
433
|
+
source: "Herb Linter",
|
|
434
|
+
message,
|
|
435
|
+
location,
|
|
436
|
+
severity,
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Helper method to add an offense to the offenses array
|
|
442
|
+
*/
|
|
443
|
+
protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
|
|
444
|
+
this.offenses.push(this.createOffense(message, location, severity))
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Main entry point for lexer rule visitors
|
|
449
|
+
* @param lexResult - The lexer result containing tokens and source
|
|
450
|
+
*/
|
|
451
|
+
visit(lexResult: LexResult): void {
|
|
452
|
+
this.visitTokens(lexResult.value.tokens)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Visit all tokens
|
|
457
|
+
* Override this method to implement token-level checks
|
|
458
|
+
*/
|
|
459
|
+
protected visitTokens(tokens: Token[]): void {
|
|
460
|
+
for (const token of tokens) {
|
|
461
|
+
this.visitToken(token)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Visit individual tokens
|
|
467
|
+
* Override this method to implement per-token checks
|
|
468
|
+
*/
|
|
469
|
+
protected visitToken(_token: Token): void {
|
|
470
|
+
// Default implementation does nothing
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Base source visitor class that provides common functionality for source-based rule visitors
|
|
477
|
+
*/
|
|
478
|
+
export abstract class BaseSourceRuleVisitor {
|
|
479
|
+
public readonly offenses: LintOffense[] = []
|
|
480
|
+
protected ruleName: string
|
|
481
|
+
protected context: LintContext
|
|
482
|
+
|
|
483
|
+
constructor(ruleName: string, context?: Partial<LintContext>) {
|
|
484
|
+
this.ruleName = ruleName
|
|
485
|
+
this.context = { ...DEFAULT_LINT_CONTEXT, ...context }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Helper method to create a lint offense for source rules
|
|
490
|
+
*/
|
|
491
|
+
protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
|
|
492
|
+
return {
|
|
493
|
+
rule: this.ruleName as any, // Type assertion for compatibility
|
|
494
|
+
code: this.ruleName,
|
|
495
|
+
source: "Herb Linter",
|
|
496
|
+
message,
|
|
497
|
+
location,
|
|
498
|
+
severity,
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Helper method to add an offense to the offenses array
|
|
504
|
+
*/
|
|
505
|
+
protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
|
|
506
|
+
this.offenses.push(this.createOffense(message, location, severity))
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Main entry point for source rule visitors
|
|
511
|
+
* @param source - The raw source code
|
|
512
|
+
*/
|
|
513
|
+
visit(source: string): void {
|
|
514
|
+
this.visitSource(source)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Visit the source code directly
|
|
519
|
+
* Override this method to implement source-level checks
|
|
520
|
+
*/
|
|
521
|
+
protected abstract visitSource(source: string): void
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Helper method to create a location for a specific position in the source
|
|
525
|
+
*/
|
|
526
|
+
protected createLocationAt(source: string, position: number): Location {
|
|
527
|
+
const beforePosition = source.substring(0, position)
|
|
528
|
+
const lines = beforePosition.split('\n')
|
|
529
|
+
const line = lines.length
|
|
530
|
+
const column = lines[lines.length - 1].length + 1
|
|
531
|
+
|
|
532
|
+
const start = new Position(line, column)
|
|
533
|
+
const end = new Position(line, column)
|
|
534
|
+
|
|
535
|
+
return new Location(start, end)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { BaseRuleVisitor, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE } from "./rule-utils.js"
|
|
2
|
+
|
|
3
|
+
import { ParserRule } from "../types.js"
|
|
4
|
+
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
+
import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
|
|
6
|
+
|
|
7
|
+
class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
|
|
8
|
+
private insideSVG = false
|
|
9
|
+
|
|
10
|
+
visitHTMLElementNode(node: HTMLElementNode): void {
|
|
11
|
+
const tagName = node.tag_name?.value
|
|
12
|
+
|
|
13
|
+
if (tagName && ["svg"].includes(tagName.toLowerCase())) {
|
|
14
|
+
const wasInsideSVG = this.insideSVG
|
|
15
|
+
this.insideSVG = true
|
|
16
|
+
this.visitChildNodes(node)
|
|
17
|
+
this.insideSVG = wasInsideSVG
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (this.insideSVG) {
|
|
22
|
+
if (node.open_tag) {
|
|
23
|
+
this.checkTagName(node.open_tag as HTMLOpenTagNode)
|
|
24
|
+
}
|
|
25
|
+
if (node.close_tag) {
|
|
26
|
+
this.checkTagName(node.close_tag as HTMLCloseTagNode)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.visitChildNodes(node)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
|
|
34
|
+
if (this.insideSVG) {
|
|
35
|
+
this.checkTagName(node)
|
|
36
|
+
}
|
|
37
|
+
this.visitChildNodes(node)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | HTMLSelfCloseTagNode): void {
|
|
41
|
+
const tagName = node.tag_name?.value
|
|
42
|
+
|
|
43
|
+
if (!tagName) return
|
|
44
|
+
|
|
45
|
+
if (SVG_CAMEL_CASE_ELEMENTS.has(tagName)) return
|
|
46
|
+
|
|
47
|
+
const lowercaseTagName = tagName.toLowerCase()
|
|
48
|
+
const correctCamelCase = SVG_LOWERCASE_TO_CAMELCASE.get(lowercaseTagName)
|
|
49
|
+
|
|
50
|
+
if (correctCamelCase && tagName !== correctCamelCase) {
|
|
51
|
+
let type: string = node.type
|
|
52
|
+
|
|
53
|
+
if (node.type == "AST_HTML_OPEN_TAG_NODE") type = "Opening"
|
|
54
|
+
if (node.type == "AST_HTML_CLOSE_TAG_NODE") type = "Closing"
|
|
55
|
+
if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE") type = "Self-closing"
|
|
56
|
+
|
|
57
|
+
this.addOffense(
|
|
58
|
+
`${type} SVG tag name \`${tagName}\` should use proper capitalization. Use \`${correctCamelCase}\` instead.`,
|
|
59
|
+
node.tag_name!.location,
|
|
60
|
+
"error"
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class SVGTagNameCapitalizationRule extends ParserRule {
|
|
67
|
+
name = "svg-tag-name-capitalization"
|
|
68
|
+
|
|
69
|
+
check(node: Node, context?: Partial<LintContext>): LintOffense[] {
|
|
70
|
+
const visitor = new SVGTagNameCapitalizationVisitor(this.name, context)
|
|
71
|
+
visitor.visit(node)
|
|
72
|
+
return visitor.offenses
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Node, Diagnostic } from "@herb-tools/core"
|
|
1
|
+
import { Node, Diagnostic, LexResult } from "@herb-tools/core"
|
|
2
2
|
import type { defaultRules } from "./default-rules.js"
|
|
3
3
|
|
|
4
4
|
export type LintSeverity = "error" | "warning"
|
|
@@ -20,13 +20,67 @@ export interface LintResult {
|
|
|
20
20
|
warnings: number
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export abstract class ParserRule {
|
|
24
|
+
static type = "parser" as const
|
|
25
|
+
abstract name: string
|
|
26
|
+
abstract check(node: Node, context?: Partial<LintContext>): LintOffense[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export abstract class LexerRule {
|
|
30
|
+
static type = "lexer" as const
|
|
31
|
+
abstract name: string
|
|
32
|
+
abstract check(lexResult: LexResult, context?: Partial<LintContext>): LintOffense[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LexerRuleConstructor {
|
|
36
|
+
type: "lexer"
|
|
37
|
+
new (): LexerRule
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Complete lint context with all properties defined.
|
|
42
|
+
* Use Partial<LintContext> when passing context to rules.
|
|
43
|
+
*/
|
|
44
|
+
export interface LintContext {
|
|
45
|
+
fileName: string | undefined
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
/**
|
|
29
|
-
*
|
|
49
|
+
* Default context object with all keys defined but set to undefined
|
|
50
|
+
*/
|
|
51
|
+
export const DEFAULT_LINT_CONTEXT: LintContext = {
|
|
52
|
+
fileName: undefined
|
|
53
|
+
} as const
|
|
54
|
+
|
|
55
|
+
export abstract class SourceRule {
|
|
56
|
+
static type = "source" as const
|
|
57
|
+
abstract name: string
|
|
58
|
+
abstract check(source: string, context?: Partial<LintContext>): LintOffense[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SourceRuleConstructor {
|
|
62
|
+
type: "source"
|
|
63
|
+
new (): SourceRule
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Type representing a parser/AST rule class constructor.
|
|
30
68
|
* The Linter accepts rule classes rather than instances for better performance and memory usage.
|
|
69
|
+
* Parser rules are the default and don't require static properties.
|
|
70
|
+
*/
|
|
71
|
+
export type ParserRuleClass = (new () => ParserRule) & {
|
|
72
|
+
type?: "parser"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type LexerRuleClass = LexerRuleConstructor
|
|
76
|
+
export type SourceRuleClass = SourceRuleConstructor
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Union type for any rule instance (Parser/AST, Lexer, or Source)
|
|
80
|
+
*/
|
|
81
|
+
export type Rule = ParserRule | LexerRule | SourceRule
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Union type for any rule class (Parser/AST, Lexer, or Source)
|
|
31
85
|
*/
|
|
32
|
-
export type RuleClass =
|
|
86
|
+
export type RuleClass = ParserRuleClass | LexerRuleClass | SourceRuleClass
|