@herb-tools/linter 0.7.2 → 0.7.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.
@@ -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
- if (!isRestrictedAttribute(attributeName))
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
- if (!isRestrictedAttribute(name))
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
- this.addOffense(`Attribute \`${combinedName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`, attributeNode.name.location, "warning");
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;AAK5H,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,MAAM,wBAAyB,SAAQ,qBAAqB;IAChD,+BAA+B,CAAC,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAoC;QAC1H,IAAI,CAAC,qBAAqB,CAAC,aAAa,CAAC;YAAE,OAAM;QACjD,IAAI,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAM;QAExC,IAAI,CAAC,UAAU,CACb,eAAe,aAAa,2FAA2F,EACvH,aAAa,CAAC,IAAK,CAAC,QAAQ,EAC5B,SAAS,CACV,CAAA;IACH,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,qBAAqB,CAAC,IAAI,CAAC;YAAE,OAAM;QACxC,IAAI,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAM;QAExC,IAAI,CAAC,UAAU,CACb,eAAe,YAAY,2FAA2F,EACtH,aAAa,CAAC,IAAK,CAAC,QAAQ,EAC5B,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"}
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.2",
3
+ "version": "0.7.3",
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.2",
43
- "@herb-tools/highlighter": "0.7.2",
44
- "@herb-tools/node-wasm": "0.7.2",
45
- "@herb-tools/printer": "0.7.2",
42
+ "@herb-tools/core": "0.7.3",
43
+ "@herb-tools/highlighter": "0.7.3",
44
+ "@herb-tools/node-wasm": "0.7.3",
45
+ "@herb-tools/printer": "0.7.3",
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
- if (!isRestrictedAttribute(attributeName)) return
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
- if (!isRestrictedAttribute(name)) return
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 \`${combinedName}\` must not be empty. Either provide a meaningful value or remove the attribute entirely.`,
59
- attributeNode.name!.location,
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
  }