@html-eslint/eslint-plugin 0.12.0 → 0.13.2

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.
@@ -12,7 +12,6 @@
12
12
  * @typedef {Object} MessageId
13
13
  * @property {"wrongIndent"} WRONG_INDENT
14
14
  */
15
-
16
15
  const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
17
16
  const { NodeUtils } = require("./utils");
18
17
 
@@ -68,18 +67,33 @@ module.exports = {
68
67
  },
69
68
  create(context) {
70
69
  const sourceCode = context.getSourceCode();
70
+
71
71
  const indentLevel = new IndentLevel();
72
72
  const { indentType, indentSize } = getIndentTypeAndSize(context.options);
73
73
  const indentUnit =
74
74
  indentType === INDENT_TYPES.SPACE ? " ".repeat(indentSize) : "\t";
75
75
 
76
+ /**
77
+ * @param {string} str
78
+ */
79
+ function countIndentSize(str) {
80
+ return str.length - str.replace(/^[\s\t]+/, "").length;
81
+ }
82
+
76
83
  /**
77
84
  * @param {BaseNode} node
78
85
  */
79
86
  function getLineCodeBefore(node) {
80
- return sourceCode.text
81
- .slice(node.range[0] - node.loc.start.column, node.range[0])
82
- .replace("\n", "");
87
+ const lines = sourceCode.getLines();
88
+ const line = lines[node.loc.start.line - 1];
89
+ let end = node.loc.start.column;
90
+ // @ts-ignore
91
+ if (typeof node.textLine === "string") {
92
+ // @ts-ignore
93
+ end += countIndentSize(node.textLine);
94
+ }
95
+
96
+ return line.slice(0, end);
83
97
  }
84
98
 
85
99
  /**
@@ -120,10 +134,13 @@ module.exports = {
120
134
  actual,
121
135
  },
122
136
  fix(fixer) {
123
- return fixer.replaceTextRange(
124
- [node.range[0] - (node.loc.start.column - 1), node.range[0]],
125
- expectedIndent
126
- );
137
+ const start = node.range[0] - node.loc.start.column;
138
+ let end = node.range[0];
139
+ // @ts-ignore
140
+ if (node.textLine) {
141
+ end += codeBefore.length;
142
+ }
143
+ return fixer.replaceTextRange([start, end], expectedIndent);
127
144
  },
128
145
  });
129
146
  }
@@ -131,14 +148,19 @@ module.exports = {
131
148
  }
132
149
 
133
150
  /**
151
+ * @param {AnyNode} startTag
134
152
  * @param {AttrNode[]} attrs
135
153
  */
136
- function checkAttrsIndent(attrs) {
137
- attrs.forEach((attr) => checkIndent(attr));
154
+ function checkAttrsIndent(startTag, attrs) {
155
+ attrs.forEach((attr) => {
156
+ if (attr.loc.start.line !== startTag.loc.start.line) {
157
+ checkIndent(attr);
158
+ }
159
+ });
138
160
  }
139
161
 
140
162
  /**
141
- * @param {AnyNode} startTag
163
+ * @param {BaseNode} startTag
142
164
  */
143
165
  function checkEndOfStartTag(startTag) {
144
166
  const start = startTag.range[1] - 1;
@@ -146,6 +168,7 @@ module.exports = {
146
168
  const line = startTag.loc.end.line;
147
169
  const endCol = startTag.loc.end.column;
148
170
  const startCol = startTag.loc.end.column - 1;
171
+
149
172
  checkIndent({
150
173
  range: [start, end],
151
174
  start,
@@ -178,8 +201,8 @@ module.exports = {
178
201
 
179
202
  indentLevel.up();
180
203
 
181
- if (Array.isArray(node.attrs)) {
182
- checkAttrsIndent(node.attrs);
204
+ if (node.startTag && Array.isArray(node.attrs)) {
205
+ checkAttrsIndent(node.startTag, node.attrs);
183
206
  }
184
207
 
185
208
  (node.childNodes || []).forEach((current) => {
@@ -201,7 +224,7 @@ module.exports = {
201
224
  }
202
225
  node.lineNodes.forEach((lineNode) => {
203
226
  if (lineNode.textLine.trim().length) {
204
- checkIndent(lineNode, node);
227
+ checkIndent(lineNode);
205
228
  }
206
229
  });
207
230
  if (!node.startTag) {
@@ -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
+ }
@@ -96,6 +96,10 @@ module.exports = {
96
96
  * @param {AttrNode} attr
97
97
  */
98
98
  function checkQuotes(attr) {
99
+ if (attr.value.includes(expectedQuote)) {
100
+ return;
101
+ }
102
+
99
103
  const [opening, closing] = getQuotes(attr);
100
104
  if (QUOTES_CODES.includes(opening)) {
101
105
  if (opening === closing && opening !== expectedQuote) {
@@ -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
  }
@@ -16,7 +16,7 @@ module.exports = {
16
16
  type: "code",
17
17
 
18
18
  docs: {
19
- description: 'Enforce to use `<meta name="chartset">` in `<head>`',
19
+ description: 'Enforce to use `<meta charset="...">` in `<head>`',
20
20
  category: RULE_CATEGORY.BEST_PRACTICE,
21
21
  recommended: false,
22
22
  },
@@ -24,7 +24,7 @@ module.exports = {
24
24
  fixable: null,
25
25
  schema: [],
26
26
  messages: {
27
- [MESSAGE_IDS.MISSING]: 'Missing `<meta name="description">`.',
27
+ [MESSAGE_IDS.MISSING]: 'Missing `<meta charset="...">`.',
28
28
  [MESSAGE_IDS.EMPTY]: "Unexpected empty charset.",
29
29
  },
30
30
  },
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.2",
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.2",
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": "3f1599284d5725db38e0984ecdcd283a76a530fa"
49
49
  }