@herb-tools/linter 0.7.2 → 0.7.4
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 +2 -2
- package/dist/herb-lint.js +26 -17
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +16 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +16 -7
- package/dist/index.js.map +1 -1
- package/dist/package.json +5 -5
- package/dist/src/rules/html-no-empty-attributes.js +17 -7
- package/dist/src/rules/html-no-empty-attributes.js.map +1 -1
- package/package.json +5 -5
- package/src/rules/html-no-empty-attributes.ts +28 -12
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ParserRule } from "../types.js";
|
|
2
2
|
import { AttributeVisitorMixin } from "./rule-utils.js";
|
|
3
|
+
import { IdentityPrinter } from "@herb-tools/printer";
|
|
3
4
|
// Attributes that must not have empty values
|
|
4
5
|
const RESTRICTED_ATTRIBUTES = new Set([
|
|
5
6
|
'id',
|
|
@@ -28,21 +29,30 @@ function isRestrictedAttribute(attributeName) {
|
|
|
28
29
|
}
|
|
29
30
|
return false;
|
|
30
31
|
}
|
|
32
|
+
function isDataAttribute(attributeName) {
|
|
33
|
+
return attributeName.startsWith('data-');
|
|
34
|
+
}
|
|
31
35
|
class NoEmptyAttributesVisitor extends AttributeVisitorMixin {
|
|
32
36
|
checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }) {
|
|
33
|
-
|
|
34
|
-
return;
|
|
35
|
-
if (attributeValue.trim() !== "")
|
|
36
|
-
return;
|
|
37
|
-
this.addOffense(`Attribute \`${attributeName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`, attributeNode.name.location, "warning");
|
|
37
|
+
this.checkEmptyAttribute(attributeName, attributeValue, attributeNode);
|
|
38
38
|
}
|
|
39
39
|
checkDynamicAttributeStaticValue({ combinedName, attributeValue, attributeNode }) {
|
|
40
40
|
const name = (combinedName || "").toLowerCase();
|
|
41
|
-
|
|
41
|
+
this.checkEmptyAttribute(name, attributeValue, attributeNode);
|
|
42
|
+
}
|
|
43
|
+
checkEmptyAttribute(attributeName, attributeValue, attributeNode) {
|
|
44
|
+
if (!isRestrictedAttribute(attributeName))
|
|
42
45
|
return;
|
|
43
46
|
if (attributeValue.trim() !== "")
|
|
44
47
|
return;
|
|
45
|
-
|
|
48
|
+
const hasExplicitValue = attributeNode.value !== null;
|
|
49
|
+
if (isDataAttribute(attributeName)) {
|
|
50
|
+
if (hasExplicitValue) {
|
|
51
|
+
this.addOffense(`Data attribute \`${attributeName}\` should not have an empty value. Either provide a meaningful value or use \`${attributeName}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.location, "warning");
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.addOffense(`Attribute \`${attributeName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`, attributeNode.location, "warning");
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
export class HTMLNoEmptyAttributesRule extends ParserRule {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-no-empty-attributes.js","sourceRoot":"","sources":["../../../src/rules/html-no-empty-attributes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,qBAAqB,EAAuE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"html-no-empty-attributes.js","sourceRoot":"","sources":["../../../src/rules/html-no-empty-attributes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,qBAAqB,EAAuE,MAAM,iBAAiB,CAAA;AAC5H,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAKrD,6CAA6C;AAC7C,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAC;IACpC,IAAI;IACJ,OAAO;IACP,MAAM;IACN,KAAK;IACL,KAAK;IACL,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;CACP,CAAC,CAAA;AAEF,0DAA0D;AAC1D,SAAS,qBAAqB,CAAC,aAAqB;IAClD,uBAAuB;IACvB,IAAI,qBAAqB,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;QAC7C,OAAO,IAAI,CAAA;IACb,CAAC;IAED,8BAA8B;IAC9B,IAAI,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,8BAA8B;IAC9B,IAAI,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,eAAe,CAAC,aAAqB;IAC5C,OAAO,aAAa,CAAC,UAAU,CAAC,OAAO,CAAC,CAAA;AAC1C,CAAC;AAED,MAAM,wBAAyB,SAAQ,qBAAqB;IAChD,+BAA+B,CAAC,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAoC;QAC1H,IAAI,CAAC,mBAAmB,CAAC,aAAa,EAAE,cAAc,EAAE,aAAa,CAAC,CAAA;IACxE,CAAC;IAES,gCAAgC,CAAC,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAqC;QAC3H,MAAM,IAAI,GAAG,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;QAC/C,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,cAAc,EAAE,aAAa,CAAC,CAAA;IAC/D,CAAC;IAEO,mBAAmB,CAAC,aAAqB,EAAE,cAAsB,EAAE,aAAgC;QACzG,IAAI,CAAC,qBAAqB,CAAC,aAAa,CAAC;YAAE,OAAM;QACjD,IAAI,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAM;QAExC,MAAM,gBAAgB,GAAG,aAAa,CAAC,KAAK,KAAK,IAAI,CAAA;QAErD,IAAI,eAAe,CAAC,aAAa,CAAC,EAAE,CAAC;YACnC,IAAI,gBAAgB,EAAE,CAAC;gBACrB,IAAI,CAAC,UAAU,CACb,oBAAoB,aAAa,iFAAiF,aAAa,mBAAmB,eAAe,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,EAC3L,aAAa,CAAC,QAAQ,EACtB,SAAS,CACV,CAAA;YACH,CAAC;YAED,OAAM;QACR,CAAC;QAED,IAAI,CAAC,UAAU,CACb,eAAe,aAAa,2FAA2F,EACvH,aAAa,CAAC,QAAQ,EACtB,SAAS,CACV,CAAA;IACH,CAAC;CACF;AAED,MAAM,OAAO,yBAA0B,SAAQ,UAAU;IACvD,IAAI,GAAG,0BAA0B,CAAA;IAEjC,KAAK,CAAC,MAAmB,EAAE,OAA8B;QACvD,MAAM,OAAO,GAAG,IAAI,wBAAwB,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;QAEhE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAE3B,OAAO,OAAO,CAAC,QAAQ,CAAA;IACzB,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herb-tools/linter",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://herb-tools.dev",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@herb-tools/core": "0.7.
|
|
43
|
-
"@herb-tools/highlighter": "0.7.
|
|
44
|
-
"@herb-tools/node-wasm": "0.7.
|
|
45
|
-
"@herb-tools/printer": "0.7.
|
|
42
|
+
"@herb-tools/core": "0.7.4",
|
|
43
|
+
"@herb-tools/highlighter": "0.7.4",
|
|
44
|
+
"@herb-tools/node-wasm": "0.7.4",
|
|
45
|
+
"@herb-tools/printer": "0.7.4",
|
|
46
46
|
"glob": "^11.0.3"
|
|
47
47
|
},
|
|
48
48
|
"files": [
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ParserRule } from "../types.js"
|
|
2
2
|
import { AttributeVisitorMixin, StaticAttributeStaticValueParams, DynamicAttributeStaticValueParams } from "./rule-utils.js"
|
|
3
|
+
import { IdentityPrinter } from "@herb-tools/printer"
|
|
3
4
|
|
|
4
5
|
import type { LintOffense, LintContext } from "../types.js"
|
|
5
|
-
import type { ParseResult } from "@herb-tools/core"
|
|
6
|
+
import type { ParseResult, HTMLAttributeNode } from "@herb-tools/core"
|
|
6
7
|
|
|
7
8
|
// Attributes that must not have empty values
|
|
8
9
|
const RESTRICTED_ATTRIBUTES = new Set([
|
|
@@ -37,26 +38,41 @@ function isRestrictedAttribute(attributeName: string): boolean {
|
|
|
37
38
|
return false
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function isDataAttribute(attributeName: string): boolean {
|
|
42
|
+
return attributeName.startsWith('data-')
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
class NoEmptyAttributesVisitor extends AttributeVisitorMixin {
|
|
41
46
|
protected checkStaticAttributeStaticValue({ attributeName, attributeValue, attributeNode }: StaticAttributeStaticValueParams): void {
|
|
42
|
-
|
|
43
|
-
if (attributeValue.trim() !== "") return
|
|
44
|
-
|
|
45
|
-
this.addOffense(
|
|
46
|
-
`Attribute \`${attributeName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`,
|
|
47
|
-
attributeNode.name!.location,
|
|
48
|
-
"warning"
|
|
49
|
-
)
|
|
47
|
+
this.checkEmptyAttribute(attributeName, attributeValue, attributeNode)
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
protected checkDynamicAttributeStaticValue({ combinedName, attributeValue, attributeNode }: DynamicAttributeStaticValueParams): void {
|
|
53
51
|
const name = (combinedName || "").toLowerCase()
|
|
54
|
-
|
|
52
|
+
this.checkEmptyAttribute(name, attributeValue, attributeNode)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private checkEmptyAttribute(attributeName: string, attributeValue: string, attributeNode: HTMLAttributeNode): void {
|
|
56
|
+
if (!isRestrictedAttribute(attributeName)) return
|
|
55
57
|
if (attributeValue.trim() !== "") return
|
|
56
58
|
|
|
59
|
+
const hasExplicitValue = attributeNode.value !== null
|
|
60
|
+
|
|
61
|
+
if (isDataAttribute(attributeName)) {
|
|
62
|
+
if (hasExplicitValue) {
|
|
63
|
+
this.addOffense(
|
|
64
|
+
`Data attribute \`${attributeName}\` should not have an empty value. Either provide a meaningful value or use \`${attributeName}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`,
|
|
65
|
+
attributeNode.location,
|
|
66
|
+
"warning"
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
this.addOffense(
|
|
58
|
-
`Attribute \`${
|
|
59
|
-
attributeNode.
|
|
74
|
+
`Attribute \`${attributeName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`,
|
|
75
|
+
attributeNode.location,
|
|
60
76
|
"warning"
|
|
61
77
|
)
|
|
62
78
|
}
|