@html-eslint/eslint-plugin 0.24.1 → 0.25.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.
@@ -6,6 +6,7 @@ module.exports = {
6
6
  "@html-eslint/require-title": "error",
7
7
  "@html-eslint/no-multiple-h1": "error",
8
8
  "@html-eslint/no-extra-spacing-attrs": "error",
9
+ "@html-eslint/attrs-newline": "error",
9
10
  "@html-eslint/element-newline": "error",
10
11
  "@html-eslint/no-duplicate-id": "error",
11
12
  "@html-eslint/indent": "error",
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @typedef { import("../types").RuleFixer } RuleFixer
3
+ * @typedef { import("../types").RuleModule } RuleModule
4
+ * @typedef { import("../types").TagNode } TagNode
5
+ * @typedef {Object} MessageId
6
+ * @property {"closeStyleWrong"} CLOSE_STYLE_WRONG
7
+ * @property {"newlineMissing"} NEWLINE_MISSING
8
+ * @property {"newlineUnexpected"} NEWLINE_UNEXPECTED
9
+ */
10
+
11
+ const { RULE_CATEGORY } = require("../constants");
12
+
13
+ /**
14
+ * @type {MessageId}
15
+ */
16
+
17
+ const MESSAGE_ID = {
18
+ CLOSE_STYLE_WRONG: "closeStyleWrong",
19
+ NEWLINE_MISSING: "newlineMissing",
20
+ NEWLINE_UNEXPECTED: "newlineUnexpected",
21
+ };
22
+
23
+ /**
24
+ * @type {RuleModule}
25
+ */
26
+ module.exports = {
27
+ meta: {
28
+ type: "code",
29
+
30
+ docs: {
31
+ description: "Enforce newline between attributes",
32
+ category: RULE_CATEGORY.STYLE,
33
+ recommended: true,
34
+ },
35
+
36
+ fixable: true,
37
+ schema: [
38
+ {
39
+ type: "object",
40
+ properties: {
41
+ closeStyle: {
42
+ enum: ["newline", "sameline"],
43
+ },
44
+ ifAttrsMoreThan: {
45
+ type: "integer",
46
+ },
47
+ },
48
+ },
49
+ ],
50
+ messages: {
51
+ [MESSAGE_ID.CLOSE_STYLE_WRONG]:
52
+ "Closing bracket was on {{actual}}; expected {{expected}}",
53
+ [MESSAGE_ID.NEWLINE_MISSING]: "Newline expected before {{attrName}}",
54
+ [MESSAGE_ID.NEWLINE_UNEXPECTED]:
55
+ "Newlines not expected between attributes, since this tag has fewer than {{attrMin}} attributes",
56
+ },
57
+ },
58
+
59
+ create(context) {
60
+ const options = context.options[0] || {};
61
+ const attrMin = isNaN(options.ifAttrsMoreThan)
62
+ ? 2
63
+ : options.ifAttrsMoreThan;
64
+ const closeStyle = options.closeStyle || "newline";
65
+
66
+ return {
67
+ /**
68
+ * @param {TagNode} node
69
+ */
70
+ Tag(node) {
71
+ const shouldBeMultiline = node.attributes.length > attrMin;
72
+
73
+ /**
74
+ * This doesn't do any indentation, so the result will look silly. Indentation should be covered by the `indent` rule
75
+ * @param {RuleFixer} fixer
76
+ */
77
+ function fix(fixer) {
78
+ const spacer = shouldBeMultiline ? "\n" : " ";
79
+ let expected = node.openStart.value;
80
+ for (const attr of node.attributes) {
81
+ expected += `${spacer}${attr.key.value}`;
82
+ if (attr.startWrapper && attr.value && attr.endWrapper) {
83
+ expected += `=${attr.startWrapper.value}${attr.value.value}${attr.endWrapper.value}`;
84
+ }
85
+ }
86
+ if (shouldBeMultiline && closeStyle === "newline") {
87
+ expected += "\n";
88
+ } else if (node.selfClosing) {
89
+ expected += " ";
90
+ }
91
+ expected += node.openEnd.value;
92
+
93
+ return fixer.replaceTextRange(
94
+ [node.openStart.range[0], node.openEnd.range[1]],
95
+ expected
96
+ );
97
+ }
98
+
99
+ if (shouldBeMultiline) {
100
+ let index = 0;
101
+ for (const attr of node.attributes) {
102
+ const attrPrevious = node.attributes[index - 1];
103
+ const relativeToNode = attrPrevious || node.openStart;
104
+ if (attr.loc.start.line === relativeToNode.loc.end.line) {
105
+ return context.report({
106
+ node,
107
+ data: {
108
+ attrName: attr.key.value,
109
+ },
110
+ fix,
111
+ messageId: MESSAGE_ID.NEWLINE_MISSING,
112
+ });
113
+ }
114
+ index += 1;
115
+ }
116
+
117
+ const attrLast = node.attributes[node.attributes.length - 1];
118
+ const closeStyleActual =
119
+ node.openEnd.loc.start.line === attrLast.loc.end.line
120
+ ? "sameline"
121
+ : "newline";
122
+ if (closeStyle !== closeStyleActual) {
123
+ return context.report({
124
+ node,
125
+ data: {
126
+ actual: closeStyleActual,
127
+ expected: closeStyle,
128
+ },
129
+ fix,
130
+ messageId: MESSAGE_ID.CLOSE_STYLE_WRONG,
131
+ });
132
+ }
133
+ } else {
134
+ let expectedLastLineNum = node.openStart.loc.start.line;
135
+ for (const attr of node.attributes) {
136
+ if (shouldBeMultiline) {
137
+ expectedLastLineNum += 1;
138
+ }
139
+ if (attr.value) {
140
+ const valueLineSpan =
141
+ attr.value.loc.end.line - attr.value.loc.start.line;
142
+ expectedLastLineNum += valueLineSpan;
143
+ }
144
+ }
145
+ if (shouldBeMultiline && closeStyle === "newline") {
146
+ expectedLastLineNum += 1;
147
+ }
148
+
149
+ if (node.openEnd.loc.end.line !== expectedLastLineNum) {
150
+ return context.report({
151
+ node,
152
+ data: {
153
+ attrMin,
154
+ },
155
+ fix,
156
+ messageId: MESSAGE_ID.NEWLINE_UNEXPECTED,
157
+ });
158
+ }
159
+ }
160
+ },
161
+ };
162
+ },
163
+ };
@@ -6,6 +6,7 @@ const noDuplicateId = require("./no-duplicate-id");
6
6
  const noInlineStyles = require("./no-inline-styles");
7
7
  const noMultipleH1 = require("./no-multiple-h1");
8
8
  const noExtraSpacingAttrs = require("./no-extra-spacing-attrs");
9
+ const attrsNewline = require("./attrs-newline");
9
10
  const elementNewLine = require("./element-newline");
10
11
  const noSkipHeadingLevels = require("./no-skip-heading-levels");
11
12
  const indent = require("./indent");
@@ -45,6 +46,7 @@ module.exports = {
45
46
  "no-inline-styles": noInlineStyles,
46
47
  "no-multiple-h1": noMultipleH1,
47
48
  "no-extra-spacing-attrs": noExtraSpacingAttrs,
49
+ "attrs-newline": attrsNewline,
48
50
  "element-newline": elementNewLine,
49
51
  "no-skip-heading-levels": noSkipHeadingLevels,
50
52
  "require-li-container": requireLiContainer,
@@ -34,8 +34,11 @@ module.exports = {
34
34
  selfClosing: {
35
35
  enum: ["always", "never"],
36
36
  },
37
- allowSelfClosingCustom: {
38
- type: "boolean",
37
+ selfClosingCustomPatterns: {
38
+ type: "array",
39
+ items: {
40
+ type: "string",
41
+ },
39
42
  },
40
43
  },
41
44
  additionalProperties: false,
@@ -49,14 +52,21 @@ module.exports = {
49
52
  },
50
53
 
51
54
  create(context) {
52
- const shouldSelfClose =
55
+ /** @type {string[]} */
56
+ const foreignContext = [];
57
+ const shouldSelfCloseVoid =
53
58
  context.options && context.options.length
54
59
  ? context.options[0].selfClosing === "always"
55
60
  : false;
56
- const allowSelfClosingCustom =
57
- context.options && context.options.length
58
- ? context.options[0].allowSelfClosingCustom === true
59
- : false;
61
+ /** @type {string[]} */
62
+ const selfClosingCustomPatternsOption =
63
+ (context.options &&
64
+ context.options.length &&
65
+ context.options[0].selfClosingCustomPatterns) ||
66
+ [];
67
+ const selfClosingCustomPatterns = selfClosingCustomPatternsOption.map(
68
+ (i) => new RegExp(i)
69
+ );
60
70
 
61
71
  /**
62
72
  * @param {TagNode} node
@@ -91,7 +101,10 @@ module.exports = {
91
101
  if (!fixable) {
92
102
  return null;
93
103
  }
94
- return fixer.replaceText(node.openEnd, " />");
104
+ const fixes = [];
105
+ fixes.push(fixer.replaceText(node.openEnd, " />"));
106
+ if (node.close) fixes.push(fixer.remove(node.close));
107
+ return fixes;
95
108
  },
96
109
  });
97
110
  }
@@ -115,17 +128,33 @@ module.exports = {
115
128
  return {
116
129
  Tag(node) {
117
130
  const isVoidElement = VOID_ELEMENTS_SET.has(node.name);
118
- if (
119
- node.selfClosing &&
120
- allowSelfClosingCustom &&
121
- node.name.indexOf("-") !== -1
122
- ) {
123
- checkVoidElement(node, true, false);
124
- } else if (node.selfClosing || isVoidElement) {
125
- checkVoidElement(node, shouldSelfClose, isVoidElement);
131
+ const isSelfClosingCustomElement = !!selfClosingCustomPatterns.some(
132
+ (i) => node.name.match(i)
133
+ );
134
+ const isForeign = foreignContext.length > 0;
135
+ const shouldSelfCloseCustom =
136
+ isSelfClosingCustomElement && !node.children.length;
137
+ const shouldSelfCloseForeign = node.selfClosing;
138
+ const shouldSelfClose =
139
+ (isVoidElement && shouldSelfCloseVoid) ||
140
+ (isSelfClosingCustomElement && shouldSelfCloseCustom) ||
141
+ (isForeign && shouldSelfCloseForeign);
142
+ const canSelfClose =
143
+ isVoidElement || isSelfClosingCustomElement || isForeign;
144
+ if (node.selfClosing || canSelfClose) {
145
+ checkVoidElement(node, shouldSelfClose, canSelfClose);
126
146
  } else if (node.openEnd.value !== "/>") {
127
147
  checkClosingTag(node);
128
148
  }
149
+ if (["svg", "math"].includes(node.name)) foreignContext.push(node.name);
150
+ },
151
+ /**
152
+ * @param {TagNode} node
153
+ */
154
+ "Tag:exit"(node) {
155
+ if (node.name === foreignContext[foreignContext.length - 1]) {
156
+ foreignContext.pop();
157
+ }
129
158
  },
130
159
  };
131
160
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@html-eslint/eslint-plugin",
3
- "version": "0.24.1",
3
+ "version": "0.25.0",
4
4
  "description": "ESLint plugin for html",
5
5
  "author": "yeonjuan",
6
6
  "homepage": "https://github.com/yeonjuan/html-eslint#readme",
@@ -45,11 +45,11 @@
45
45
  "accessibility"
46
46
  ],
47
47
  "devDependencies": {
48
- "@html-eslint/parser": "^0.24.1",
48
+ "@html-eslint/parser": "^0.25.0",
49
49
  "@types/eslint": "^8.56.2",
50
50
  "@types/estree": "^0.0.47",
51
51
  "es-html-parser": "^0.0.8",
52
52
  "typescript": "^4.4.4"
53
53
  },
54
- "gitHead": "c9095f0738fea1688099937ebc0a93b1a26b7da9"
54
+ "gitHead": "315631a66e9c626655c243ccb6a46319736383cf"
55
55
  }
@@ -0,0 +1,18 @@
1
+ export const rules: {
2
+ "@html-eslint/require-lang": string;
3
+ "@html-eslint/require-img-alt": string;
4
+ "@html-eslint/require-doctype": string;
5
+ "@html-eslint/require-title": string;
6
+ "@html-eslint/no-multiple-h1": string;
7
+ "@html-eslint/no-extra-spacing-attrs": string;
8
+ "@html-eslint/attrs-newline": string;
9
+ "@html-eslint/element-newline": string;
10
+ "@html-eslint/no-duplicate-id": string;
11
+ "@html-eslint/indent": string;
12
+ "@html-eslint/require-li-container": string;
13
+ "@html-eslint/quotes": string;
14
+ "@html-eslint/no-obsolete-tags": string;
15
+ "@html-eslint/require-closing-tags": string;
16
+ "@html-eslint/no-duplicate-attrs": string;
17
+ };
18
+ //# sourceMappingURL=recommended.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recommended.d.ts","sourceRoot":"","sources":["../../lib/configs/recommended.js"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ declare const _exports: RuleModule;
2
+ export = _exports;
3
+ export type RuleFixer = import("../types").RuleFixer;
4
+ export type RuleModule = import("../types").RuleModule;
5
+ export type TagNode = import("../types").TagNode;
6
+ export type MessageId = {
7
+ CLOSE_STYLE_WRONG: "closeStyleWrong";
8
+ NEWLINE_MISSING: "newlineMissing";
9
+ NEWLINE_UNEXPECTED: "newlineUnexpected";
10
+ };
11
+ //# sourceMappingURL=attrs-newline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attrs-newline.d.ts","sourceRoot":"","sources":["../../lib/rules/attrs-newline.js"],"names":[],"mappings":"wBAuBU,UAAU;;wBAtBN,OAAO,UAAU,EAAE,SAAS;yBAC5B,OAAO,UAAU,EAAE,UAAU;sBAC7B,OAAO,UAAU,EAAE,OAAO;;uBAE1B,iBAAiB;qBACjB,gBAAgB;wBAChB,mBAAmB"}
@@ -0,0 +1,42 @@
1
+ declare const _exports: {
2
+ "require-lang": import("../types").RuleModule;
3
+ "require-img-alt": import("../types").RuleModule;
4
+ "require-doctype": import("../types").RuleModule;
5
+ "require-title": import("../types").RuleModule;
6
+ "no-duplicate-id": import("../types").RuleModule;
7
+ "no-inline-styles": import("../types").RuleModule;
8
+ "no-multiple-h1": import("../types").RuleModule;
9
+ "no-extra-spacing-attrs": import("../types").RuleModule;
10
+ "attrs-newline": import("../types").RuleModule;
11
+ "element-newline": import("../types").RuleModule;
12
+ "no-skip-heading-levels": import("../types").RuleModule;
13
+ "require-li-container": import("../types").RuleModule;
14
+ indent: import("../types").RuleModule;
15
+ quotes: import("../types").RuleModule;
16
+ "id-naming-convention": import("../types").RuleModule;
17
+ "no-obsolete-tags": import("../types").RuleModule;
18
+ "require-attrs": import("../types").RuleModule;
19
+ "require-closing-tags": import("../types").RuleModule;
20
+ "require-meta-description": import("../types").RuleModule;
21
+ "require-frame-title": import("../types").RuleModule;
22
+ "no-non-scalable-viewport": import("../types").RuleModule;
23
+ "no-positive-tabindex": import("../types").RuleModule;
24
+ "require-meta-viewport": import("../types").RuleModule;
25
+ "require-meta-charset": import("../types").RuleModule;
26
+ "no-target-blank": import("../types").RuleModule;
27
+ "no-duplicate-attrs": import("../types").RuleModule;
28
+ "no-abstract-roles": import("../types").RuleModule;
29
+ "require-button-type": import("../types").RuleModule;
30
+ "no-aria-hidden-body": import("../types").RuleModule;
31
+ "no-multiple-empty-lines": import("../types").RuleModule;
32
+ "no-accesskey-attrs": import("../types").RuleModule;
33
+ "no-restricted-attrs": import("../types").RuleModule;
34
+ "no-trailing-spaces": import("../types").RuleModule;
35
+ "no-restricted-attr-values": import("../types").RuleModule;
36
+ "no-script-style-type": import("../types").RuleModule;
37
+ lowercase: import("../types").RuleModule;
38
+ "require-open-graph-protocol": import("../types").RuleModule;
39
+ "sort-attrs": import("../types").RuleModule;
40
+ };
41
+ export = _exports;
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-closing-tags.d.ts","sourceRoot":"","sources":["../../lib/rules/require-closing-tags.js"],"names":[],"mappings":"wBAgBU,UAAU;;yBAfN,OAAO,UAAU,EAAE,UAAU;sBAC7B,OAAO,UAAU,EAAE,OAAO"}