@html-eslint/eslint-plugin 0.12.0 → 0.13.0

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.
@@ -131,10 +131,15 @@ module.exports = {
131
131
  }
132
132
 
133
133
  /**
134
+ * @param {AnyNode} startTag
134
135
  * @param {AttrNode[]} attrs
135
136
  */
136
- function checkAttrsIndent(attrs) {
137
- attrs.forEach((attr) => checkIndent(attr));
137
+ function checkAttrsIndent(startTag, attrs) {
138
+ attrs.forEach((attr) => {
139
+ if (attr.loc.start.line !== startTag.loc.start.line) {
140
+ checkIndent(attr);
141
+ }
142
+ });
138
143
  }
139
144
 
140
145
  /**
@@ -146,6 +151,7 @@ module.exports = {
146
151
  const line = startTag.loc.end.line;
147
152
  const endCol = startTag.loc.end.column;
148
153
  const startCol = startTag.loc.end.column - 1;
154
+
149
155
  checkIndent({
150
156
  range: [start, end],
151
157
  start,
@@ -178,8 +184,8 @@ module.exports = {
178
184
 
179
185
  indentLevel.up();
180
186
 
181
- if (Array.isArray(node.attrs)) {
182
- checkAttrsIndent(node.attrs);
187
+ if (node.startTag && Array.isArray(node.attrs)) {
188
+ checkAttrsIndent(node.startTag, node.attrs);
183
189
  }
184
190
 
185
191
  (node.childNodes || []).forEach((current) => {
@@ -27,6 +27,7 @@ const requireButtonType = require("./require-button-type");
27
27
  const noAriaHiddenBody = require("./no-aria-hidden-body");
28
28
  const noMultipleEmptyLines = require("./no-multiple-empty-lines");
29
29
  const noAccesskeyAttrs = require("./no-accesskey-attrs");
30
+ const noRestrictedAttrs = require("./no-restricted-attrs");
30
31
 
31
32
  module.exports = {
32
33
  "require-lang": requireLang,
@@ -58,4 +59,5 @@ module.exports = {
58
59
  "no-aria-hidden-body": noAriaHiddenBody,
59
60
  "no-multiple-empty-lines": noMultipleEmptyLines,
60
61
  "no-accesskey-attrs": noAccesskeyAttrs,
62
+ "no-restricted-attrs": noRestrictedAttrs,
61
63
  };
@@ -66,13 +66,18 @@ module.exports = {
66
66
  /**
67
67
  * @param {TagNode} startTag
68
68
  * @param {AttrNode} lastAttr
69
+ * @param {boolean} isSelfClosed
69
70
  */
70
- function checkExtraSpaceAfter(startTag, lastAttr) {
71
+ function checkExtraSpaceAfter(startTag, lastAttr, isSelfClosed) {
71
72
  if (startTag.loc.end.line !== lastAttr.loc.end.line) {
72
73
  // skip the attribute on the different line with the start tag
73
74
  return;
74
75
  }
75
- const spacesBetween = startTag.loc.end.column - lastAttr.loc.end.column;
76
+ let spacesBetween = startTag.loc.end.column - lastAttr.loc.end.column;
77
+ if (isSelfClosed) {
78
+ spacesBetween--;
79
+ }
80
+
76
81
  if (spacesBetween > 1) {
77
82
  context.report({
78
83
  loc: {
@@ -83,7 +88,7 @@ module.exports = {
83
88
  fix(fixer) {
84
89
  return fixer.removeRange([
85
90
  lastAttr.range[1],
86
- startTag.range[1] - 1,
91
+ lastAttr.range[1] + spacesBetween - 1,
87
92
  ]);
88
93
  },
89
94
  });
@@ -125,9 +130,11 @@ module.exports = {
125
130
  checkExtraSpaceBefore(node, node.attrs[0]);
126
131
  }
127
132
  if (node.startTag && node.attrs && node.attrs.length > 0) {
133
+ const isSelfClosed = !node.endTag;
128
134
  checkExtraSpaceAfter(
129
135
  node.startTag,
130
- node.attrs[node.attrs.length - 1]
136
+ node.attrs[node.attrs.length - 1],
137
+ isSelfClosed
131
138
  );
132
139
  }
133
140
  checkExtraSpacesBetweenAttrs(node.attrs || []);
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @typedef {import("../types").Rule} Rule
3
+ * @typedef {import("../types").AnyNode} AnyNode
4
+ * @typedef {{tagPatterns: string[], attrPatterns: string[], message?: string}[]} Options
5
+ */
6
+
7
+ const { RULE_CATEGORY } = require("../constants");
8
+
9
+ const MESSAGE_IDS = {
10
+ RESTRICTED: "restricted",
11
+ };
12
+
13
+ /**
14
+ * @type {Rule}
15
+ */
16
+ module.exports = {
17
+ meta: {
18
+ type: "code",
19
+
20
+ docs: {
21
+ description: "Disallow specified attributes",
22
+ category: RULE_CATEGORY.BEST_PRACTICE,
23
+ recommended: false,
24
+ },
25
+
26
+ fixable: null,
27
+ schema: {
28
+ type: "array",
29
+
30
+ items: {
31
+ type: "object",
32
+ required: ["tagPatterns", "attrPatterns"],
33
+ properties: {
34
+ tagPatterns: {
35
+ type: "array",
36
+ items: {
37
+ type: "string",
38
+ },
39
+ },
40
+ attrPatterns: {
41
+ type: "array",
42
+ items: {
43
+ type: "string",
44
+ },
45
+ },
46
+ message: {
47
+ type: "string",
48
+ },
49
+ },
50
+ },
51
+ },
52
+ messages: {
53
+ [MESSAGE_IDS.RESTRICTED]: "'{{attr}}' is restricted from being used.",
54
+ },
55
+ },
56
+
57
+ create(context) {
58
+ /**
59
+ * @type {Options}
60
+ */
61
+ const options = context.options;
62
+ const checkers = options.map((option) => new PatternChecker(option));
63
+
64
+ return {
65
+ "*"(node) {
66
+ const tagName = node.tagName;
67
+ const startTag = node.startTag;
68
+ if (!tagName || !startTag) return;
69
+ if (!node.attrs.length) return;
70
+
71
+ node.attrs.forEach((attr) => {
72
+ if (!attr.name) return;
73
+
74
+ const matched = checkers.find((checker) =>
75
+ checker.test(node.tagName, attr.name)
76
+ );
77
+
78
+ if (!matched) return;
79
+
80
+ /**
81
+ * @type {{node: AnyNode, message: string, messageId?: string}}
82
+ */
83
+
84
+ const result = {
85
+ node: startTag,
86
+ message: "",
87
+ };
88
+
89
+ const customMessage = matched.getMessage();
90
+
91
+ if (customMessage) {
92
+ result.message = customMessage;
93
+ } else {
94
+ result.messageId = MESSAGE_IDS.RESTRICTED;
95
+ }
96
+
97
+ context.report({
98
+ ...result,
99
+ data: { attr: attr.name },
100
+ });
101
+ });
102
+ },
103
+ };
104
+ },
105
+ };
106
+
107
+ class PatternChecker {
108
+ /**
109
+ * @param {Options[number]} option
110
+ */
111
+ constructor(option) {
112
+ this.option = option;
113
+ this.tagRegExps = option.tagPatterns.map(
114
+ (pattern) => new RegExp(pattern, "u")
115
+ );
116
+ this.attrRegExps = option.attrPatterns.map(
117
+ (pattern) => new RegExp(pattern, "u")
118
+ );
119
+ this.message = option.message;
120
+ }
121
+
122
+ /**
123
+ * @param {string} tagName
124
+ * @param {string} attrName
125
+ * @returns {boolean}
126
+ */
127
+ test(tagName, attrName) {
128
+ const result =
129
+ this.tagRegExps.some((exp) => exp.test(tagName)) &&
130
+ this.attrRegExps.some((exp) => exp.test(attrName));
131
+ return result;
132
+ }
133
+
134
+ /**
135
+ * @returns {string}
136
+ */
137
+ getMessage() {
138
+ return this.message || "";
139
+ }
140
+ }
@@ -57,6 +57,16 @@ module.exports = {
57
57
 
58
58
  function checkClosingTag(node) {
59
59
  if (node.startTag && !node.endTag) {
60
+ if (
61
+ node.namespaceURI === "http://www.w3.org/2000/svg" ||
62
+ node.namespaceURI === "http://www.w3.org/1998/Math/MathML"
63
+ ) {
64
+ const code = getCodeIn(node.startTag.range);
65
+ const hasSelfClose = code.endsWith("/>");
66
+ if (hasSelfClose) {
67
+ return;
68
+ }
69
+ }
60
70
  context.report({
61
71
  node: node.startTag,
62
72
  data: {
@@ -67,7 +77,7 @@ module.exports = {
67
77
  }
68
78
  }
69
79
 
70
- function checkSelfClosing(node) {
80
+ function checkVoidElement(node) {
71
81
  const startTag = node.startTag;
72
82
  const code = getCodeIn(startTag.range);
73
83
  const hasSelfClose = code.endsWith("/>");
@@ -108,7 +118,7 @@ module.exports = {
108
118
  "*"(node) {
109
119
  if (node.startTag) {
110
120
  if (VOID_ELEMENTS_SET.has(node.tagName)) {
111
- checkSelfClosing(node);
121
+ checkVoidElement(node);
112
122
  } else {
113
123
  checkClosingTag(node);
114
124
  }
package/lib/types.d.ts CHANGED
@@ -96,6 +96,7 @@ export interface ElementNode extends BaseNode {
96
96
  childNodes: ElementNode[];
97
97
  startTag?: TagNode;
98
98
  endTag?: TagNode;
99
+ namespaceURI?: string;
99
100
  }
100
101
 
101
102
  export interface AttrNode extends BaseNode {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@html-eslint/eslint-plugin",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "ESLint plugin for html",
5
5
  "author": "yeonjuan",
6
6
  "homepage": "https://github.com/yeonjuan/html-eslint#readme",
@@ -40,10 +40,10 @@
40
40
  "accessibility"
41
41
  ],
42
42
  "devDependencies": {
43
- "@html-eslint/parser": "^0.12.0",
43
+ "@html-eslint/parser": "^0.13.0",
44
44
  "@types/eslint": "^7.2.10",
45
45
  "@types/estree": "^0.0.47",
46
46
  "typescript": "^4.4.4"
47
47
  },
48
- "gitHead": "855dba9201bb8e00c6bc79676282bf38b0a76ea7"
48
+ "gitHead": "d7acca5d9d0517cc44085901e71e57e81704bcb7"
49
49
  }