@html-eslint/eslint-plugin 0.13.1 → 0.14.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.
@@ -1,10 +1,3 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- * @typedef {import("../types").AttrNode} AttrNode
4
- * @typedef {import("../types").TagNode} TagNode
5
- * @typedef {import("../types").ElementNode} ElementNode
6
- */
7
-
8
1
  const { RULE_CATEGORY } = require("../constants");
9
2
 
10
3
  const MESSAGE_IDS = {
@@ -13,9 +6,6 @@ const MESSAGE_IDS = {
13
6
  EXTRA_BEFORE: "unexpectedBefore",
14
7
  };
15
8
 
16
- /**
17
- * @type {Rule}
18
- */
19
9
  module.exports = {
20
10
  meta: {
21
11
  type: "code",
@@ -35,9 +25,6 @@ module.exports = {
35
25
  },
36
26
  },
37
27
  create(context) {
38
- /**
39
- * @param {AttrNode[]} attrs
40
- */
41
28
  function checkExtraSpacesBetweenAttrs(attrs) {
42
29
  attrs.forEach((current, index, attrs) => {
43
30
  if (index >= attrs.length - 1) {
@@ -63,17 +50,13 @@ module.exports = {
63
50
  }
64
51
  });
65
52
  }
66
- /**
67
- * @param {TagNode} startTag
68
- * @param {AttrNode} lastAttr
69
- * @param {boolean} isSelfClosed
70
- */
71
- function checkExtraSpaceAfter(startTag, lastAttr, isSelfClosed) {
72
- if (startTag.loc.end.line !== lastAttr.loc.end.line) {
53
+
54
+ function checkExtraSpaceAfter(openEnd, lastAttr, isSelfClosed) {
55
+ if (openEnd.loc.end.line !== lastAttr.loc.end.line) {
73
56
  // skip the attribute on the different line with the start tag
74
57
  return;
75
58
  }
76
- let spacesBetween = startTag.loc.end.column - lastAttr.loc.end.column;
59
+ let spacesBetween = openEnd.loc.end.column - lastAttr.loc.end.column;
77
60
  if (isSelfClosed) {
78
61
  spacesBetween--;
79
62
  }
@@ -82,7 +65,7 @@ module.exports = {
82
65
  context.report({
83
66
  loc: {
84
67
  start: lastAttr.loc.end,
85
- end: startTag.loc.end,
68
+ end: openEnd.loc.end,
86
69
  },
87
70
  messageId: MESSAGE_IDS.EXTRA_AFTER,
88
71
  fix(fixer) {
@@ -95,19 +78,14 @@ module.exports = {
95
78
  }
96
79
  }
97
80
 
98
- /**
99
- * @param {ElementNode} node
100
- * @param {AttrNode} firstAttr
101
- */
102
81
  function checkExtraSpaceBefore(node, firstAttr) {
103
82
  if (node.loc.start.line !== firstAttr.loc.start.line) {
104
83
  // skip the attribute on the different line with the start tag
105
84
  return;
106
85
  }
107
- const nodeLength = node.tagName.length;
108
- const spacesBetween =
109
- firstAttr.loc.start.column - (node.loc.start.column + nodeLength);
110
- if (spacesBetween > 2) {
86
+
87
+ const spacesBetween = firstAttr.loc.start.column - node.loc.end.column;
88
+ if (spacesBetween >= 2) {
111
89
  context.report({
112
90
  loc: {
113
91
  start: node.loc.start,
@@ -116,7 +94,7 @@ module.exports = {
116
94
  messageId: MESSAGE_IDS.EXTRA_BEFORE,
117
95
  fix(fixer) {
118
96
  return fixer.removeRange([
119
- node.range[0] + nodeLength + 2,
97
+ firstAttr.range[0] - spacesBetween + 1,
120
98
  firstAttr.range[0],
121
99
  ]);
122
100
  },
@@ -125,19 +103,22 @@ module.exports = {
125
103
  }
126
104
 
127
105
  return {
128
- "*"(node) {
129
- if (node.attrs && node.attrs.length > 0) {
130
- checkExtraSpaceBefore(node, node.attrs[0]);
106
+ [["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
107
+ if (!node.attributes || node.attributes.length <= 0) {
108
+ return;
131
109
  }
132
- if (node.startTag && node.attrs && node.attrs.length > 0) {
133
- const isSelfClosed = !node.endTag;
110
+
111
+ checkExtraSpaceBefore(node.openStart, node.attributes[0]);
112
+
113
+ if (node.openEnd && node.attributes && node.attributes.length > 0) {
114
+ const selfClosing = node.openEnd.value === "/>";
134
115
  checkExtraSpaceAfter(
135
- node.startTag,
136
- node.attrs[node.attrs.length - 1],
137
- isSelfClosed
116
+ node.openEnd,
117
+ node.attributes[node.attributes.length - 1],
118
+ selfClosing
138
119
  );
139
120
  }
140
- checkExtraSpacesBetweenAttrs(node.attrs || []);
121
+ checkExtraSpacesBetweenAttrs(node.attributes);
141
122
  },
142
123
  };
143
124
  },
@@ -1,7 +1,3 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
1
  const { RULE_CATEGORY } = require("../constants");
6
2
  const { NodeUtils } = require("./utils");
7
3
 
@@ -9,9 +5,6 @@ const MESSAGE_IDS = {
9
5
  INLINE_STYLE: "unexpectedInlineStyle",
10
6
  };
11
7
 
12
- /**
13
- * @type {Rule}
14
- */
15
8
  module.exports = {
16
9
  meta: {
17
10
  type: "code",
@@ -31,10 +24,11 @@ module.exports = {
31
24
 
32
25
  create(context) {
33
26
  return {
34
- "*"(node) {
35
- if (NodeUtils.hasAttr(node, "style")) {
27
+ Tag(node) {
28
+ const styleAttr = NodeUtils.findAttr(node, "style");
29
+ if (styleAttr) {
36
30
  context.report({
37
- node: node.startTag,
31
+ node: styleAttr,
38
32
  messageId: MESSAGE_IDS.INLINE_STYLE,
39
33
  });
40
34
  }
@@ -32,8 +32,10 @@ module.exports = {
32
32
  const h1s = [];
33
33
 
34
34
  return {
35
- H1(node) {
36
- h1s.push(node);
35
+ Tag(node) {
36
+ if (node.name === "h1") {
37
+ h1s.push(node);
38
+ }
37
39
  },
38
40
  "Program:exit"() {
39
41
  if (h1s.length > 1) {
@@ -32,14 +32,19 @@ module.exports = {
32
32
 
33
33
  create(context) {
34
34
  return {
35
- Meta(node) {
35
+ Tag(node) {
36
+ if (node.name !== "meta") {
37
+ return;
38
+ }
36
39
  const nameAttr = NodeUtils.findAttr(node, "name");
37
40
  const contentAttr = NodeUtils.findAttr(node, "content");
38
41
  if (
39
42
  nameAttr &&
43
+ nameAttr.value &&
40
44
  contentAttr &&
41
- nameAttr.value.toLowerCase() === "viewport" &&
42
- contentAttr.value.match(/user-scalable\s*=\s*no/i)
45
+ contentAttr.value &&
46
+ nameAttr.value.value.toLowerCase() === "viewport" &&
47
+ contentAttr.value.value.match(/user-scalable\s*=\s*no/i)
43
48
  ) {
44
49
  context.report({
45
50
  node: contentAttr,
@@ -1,8 +1,4 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
- const { RULE_CATEGORY, NODE_TYPES } = require("../constants");
1
+ const { RULE_CATEGORY } = require("../constants");
6
2
  const { OBSOLETE_TAGS } = require("../constants");
7
3
 
8
4
  const OBSOLETE_TAGS_SET = new Set(OBSOLETE_TAGS);
@@ -11,9 +7,6 @@ const MESSAGE_IDS = {
11
7
  UNEXPECTED: "unexpected",
12
8
  };
13
9
 
14
- /**
15
- * @type {Rule}
16
- */
17
10
  module.exports = {
18
11
  meta: {
19
12
  type: "code",
@@ -33,16 +26,12 @@ module.exports = {
33
26
 
34
27
  create(context) {
35
28
  return {
36
- "*"(node) {
37
- if (
38
- node.type !== NODE_TYPES.PROGRAM &&
39
- node.tagName &&
40
- OBSOLETE_TAGS_SET.has(node.tagName)
41
- ) {
29
+ Tag(node) {
30
+ if (OBSOLETE_TAGS_SET.has(node.name)) {
42
31
  context.report({
43
32
  node,
44
33
  data: {
45
- tag: node.tagName,
34
+ tag: node.name,
46
35
  },
47
36
  messageId: MESSAGE_IDS.UNEXPECTED,
48
37
  });
@@ -31,9 +31,13 @@ module.exports = {
31
31
 
32
32
  create(context) {
33
33
  return {
34
- "*"(node) {
34
+ [["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
35
35
  const tabIndexAttr = NodeUtils.findAttr(node, "tabindex");
36
- if (tabIndexAttr && parseInt(tabIndexAttr.value, 10) > 0) {
36
+ if (
37
+ tabIndexAttr &&
38
+ tabIndexAttr.value &&
39
+ parseInt(tabIndexAttr.value.value, 10) > 0
40
+ ) {
37
41
  context.report({
38
42
  node: tabIndexAttr,
39
43
  messageId: MESSAGE_IDS.UNEXPECTED,
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * @typedef {import("../types").Rule} Rule
3
- * @typedef {import("../types").AnyNode} AnyNode
4
3
  * @typedef {{tagPatterns: string[], attrPatterns: string[], message?: string}[]} Options
5
4
  */
6
5
 
@@ -62,27 +61,22 @@ module.exports = {
62
61
  const checkers = options.map((option) => new PatternChecker(option));
63
62
 
64
63
  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
-
64
+ [["Tag", "StyleTag", "ScriptTag"].join(",")](node) {
65
+ const tagName = node.name;
66
+ node.attributes.forEach((attr) => {
67
+ if (!attr.key || !attr.key.value) {
68
+ return;
69
+ }
74
70
  const matched = checkers.find((checker) =>
75
- checker.test(node.tagName, attr.name)
71
+ checker.test(tagName, attr.key.value)
76
72
  );
77
73
 
78
- if (!matched) return;
79
-
80
- /**
81
- * @type {{node: AnyNode, message: string, messageId?: string}}
82
- */
74
+ if (!matched) {
75
+ return;
76
+ }
83
77
 
84
78
  const result = {
85
- node: startTag,
79
+ node: attr,
86
80
  message: "",
87
81
  };
88
82
 
@@ -96,7 +90,7 @@ module.exports = {
96
90
 
97
91
  context.report({
98
92
  ...result,
99
- data: { attr: attr.name },
93
+ data: { attr: attr.key.value },
100
94
  });
101
95
  });
102
96
  },
@@ -33,10 +33,13 @@ module.exports = {
33
33
  const headings = [];
34
34
 
35
35
  return {
36
- "H1, H2, H3, H5, H6"(node) {
36
+ Tag(node) {
37
+ if (!["h1", "h2", "h3", "h5", "h6"].includes(node.name)) {
38
+ return;
39
+ }
37
40
  headings.push({
38
41
  node,
39
- level: parseInt(node.type.replace("H", ""), 10),
42
+ level: parseInt(node.name.replace("h", ""), 10),
40
43
  });
41
44
  },
42
45
  "Program:exit"() {
@@ -39,16 +39,19 @@ module.exports = {
39
39
  return /^(?:\w+:|\/\/)/.test(link);
40
40
  }
41
41
  return {
42
- A(node) {
42
+ Tag(node) {
43
+ if (node.name !== "a") {
44
+ return;
45
+ }
43
46
  /* eslint-disable */
44
47
  const target = NodeUtils.findAttr(node, "target");
45
- if (target && target.value === "_blank") {
48
+ if (target && target.value && target.value.value === "_blank") {
46
49
  const href = NodeUtils.findAttr(node, "href");
47
- if (href && isExternalLink(href.value)) {
50
+ if (href && href.value && isExternalLink(href.value.value)) {
48
51
  const rel = NodeUtils.findAttr(node, "rel");
49
- if (!rel || !rel.value.includes("noreferrer")) {
52
+ if (!rel || !rel.value || !rel.value.value.includes("noreferrer")) {
50
53
  context.report({
51
- node,
54
+ node: target,
52
55
  messageId: MESSAGE_IDS.MISSING,
53
56
  });
54
57
  }
@@ -1,9 +1,3 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- * @typedef {import("../types").Range} Range
4
- * @typedef {import("../types").AttrNode} AttrNode
5
- */
6
-
7
1
  const { RULE_CATEGORY } = require("../constants");
8
2
 
9
3
  const MESSAGE_IDS = {
@@ -18,9 +12,6 @@ const QUOTES_STYLES = {
18
12
 
19
13
  const QUOTES_CODES = [`"`, `'`];
20
14
 
21
- /**
22
- * @type {Rule}
23
- */
24
15
  module.exports = {
25
16
  meta: {
26
17
  type: "code",
@@ -62,66 +53,39 @@ module.exports = {
62
53
  return sourceCode.text.slice(range[0], range[1]);
63
54
  }
64
55
 
65
- /**
66
- * @param {AttrNode} attr
67
- * @returns {Range}
68
- */
69
- function getValueRange(attr) {
70
- const attrCode = getCodeIn(attr.range);
71
- const [matched = ""] = attrCode.match(/\S*?\s*=\s*/) || [];
72
- return [attr.range[0] + matched.length, attr.range[1]];
73
- }
74
-
75
- /**
76
- * @param {AttrNode} attr
77
- * @returns {[string, string]}
78
- */
79
56
  function getQuotes(attr) {
80
- const [valueStart, valueEnd] = getValueRange(attr);
81
- const opening = getCodeIn([valueStart, valueStart + 1]);
82
- const closing = getCodeIn([valueEnd - 1, valueEnd]);
83
- return [opening, closing];
57
+ return [attr.startWrapper.value, attr.endWrapper.value];
84
58
  }
85
59
 
86
- /**
87
- * @param {AttrNode} attr
88
- * @returns {boolean}
89
- */
90
- function hasEqualSign(attr) {
91
- const keyEnd = attr.range[0] + attr.name.length;
92
- return getCodeIn([keyEnd, attr.range[1]]).trimStart().startsWith("=");
93
- }
94
-
95
- /**
96
- * @param {AttrNode} attr
97
- */
98
60
  function checkQuotes(attr) {
99
- const [opening, closing] = getQuotes(attr);
100
- if (QUOTES_CODES.includes(opening)) {
101
- if (opening === closing && opening !== expectedQuote) {
102
- context.report({
103
- node: attr,
104
- messageId: MESSAGE_IDS.UNEXPECTED,
105
- data: {
106
- expected: `${SELECTED_STYLE}(${expectedQuote})`,
107
- actual:
108
- SELECTED_STYLE === QUOTES_STYLES.SINGLE
109
- ? `${QUOTES_STYLES.DOUBLE}(")`
110
- : `${QUOTES_STYLES.SINGLE}(')`,
111
- },
112
- fix(fixer) {
113
- const range = getValueRange(attr);
114
- const originCode = getCodeIn(range);
115
- const onlyValue = originCode.slice(1, originCode.length - 1);
61
+ if (!attr.value || attr.value.value.includes(expectedQuote)) {
62
+ return;
63
+ }
116
64
 
117
- return fixer.replaceTextRange(
118
- range,
119
- `${expectedQuote}${onlyValue}${expectedQuote}`
120
- );
121
- },
122
- });
65
+ if (attr.startWrapper && attr.endWrapper) {
66
+ const [opening, closing] = getQuotes(attr);
67
+ if (QUOTES_CODES.includes(opening)) {
68
+ if (opening === closing && opening !== expectedQuote) {
69
+ context.report({
70
+ node: attr,
71
+ messageId: MESSAGE_IDS.UNEXPECTED,
72
+ data: {
73
+ expected: `${SELECTED_STYLE}(${expectedQuote})`,
74
+ actual:
75
+ SELECTED_STYLE === QUOTES_STYLES.SINGLE
76
+ ? `${QUOTES_STYLES.DOUBLE}(")`
77
+ : `${QUOTES_STYLES.SINGLE}(')`,
78
+ },
79
+ fix(fixer) {
80
+ return fixer.replaceTextRange(
81
+ [attr.startWrapper.range[0], attr.endWrapper.range[1]],
82
+ `${expectedQuote}${attr.value.value}${expectedQuote}`
83
+ );
84
+ },
85
+ });
86
+ }
123
87
  }
124
- } else if (hasEqualSign(attr)) {
88
+ } else {
125
89
  context.report({
126
90
  node: attr,
127
91
  messageId: MESSAGE_IDS.MISSING,
@@ -129,10 +93,9 @@ module.exports = {
129
93
  expected: `${SELECTED_STYLE}(${expectedQuote})`,
130
94
  },
131
95
  fix(fixer) {
132
- const range = getValueRange(attr);
133
- const originCode = getCodeIn(range);
96
+ const originCode = getCodeIn(attr.value.range);
134
97
  return fixer.replaceTextRange(
135
- range,
98
+ attr.value.range,
136
99
  `${expectedQuote}${originCode}${expectedQuote}`
137
100
  );
138
101
  },
@@ -141,8 +104,8 @@ module.exports = {
141
104
  }
142
105
 
143
106
  return {
144
- "*"(node) {
145
- (node.attrs || []).forEach(checkQuotes);
107
+ [["Tag", "ScriptTag", "StyleTag"].join(",")](node) {
108
+ node.attributes.forEach(checkQuotes);
146
109
  },
147
110
  };
148
111
  },
@@ -36,19 +36,22 @@ module.exports = {
36
36
 
37
37
  create(context) {
38
38
  return {
39
- Button(node) {
39
+ Tag(node) {
40
+ if (node.name !== "button") {
41
+ return;
42
+ }
40
43
  const typeAttr = NodeUtils.findAttr(node, "type");
41
- if (!typeAttr) {
44
+ if (!typeAttr || !typeAttr.value) {
42
45
  context.report({
43
- node: node.startTag || node,
46
+ node: node.openStart,
44
47
  messageId: MESSAGE_IDS.MISSING,
45
48
  });
46
- } else if (!VALID_BUTTON_TYPES_SET.has(typeAttr.value)) {
49
+ } else if (!VALID_BUTTON_TYPES_SET.has(typeAttr.value.value)) {
47
50
  context.report({
48
51
  node: typeAttr,
49
52
  messageId: MESSAGE_IDS.INVALID,
50
53
  data: {
51
- type: typeAttr.value,
54
+ type: typeAttr.value.value,
52
55
  },
53
56
  });
54
57
  }
@@ -41,36 +41,24 @@ module.exports = {
41
41
  [MESSAGE_IDS.MISSING]: "Missing closing tag for {{tag}}.",
42
42
  [MESSAGE_IDS.MISSING_SELF]: "Missing self closing tag for {{tag}}",
43
43
  [MESSAGE_IDS.UNEXPECTED]: "Unexpected self closing tag for {{tag}}.",
44
+ [MESSAGE_IDS.HUCKS]: "HUCKS.",
44
45
  },
45
46
  },
46
47
 
47
48
  create(context) {
49
+ let svgStacks = [];
50
+
48
51
  const shouldSelfClose =
49
52
  context.options && context.options.length
50
53
  ? context.options[0].selfClosing === "always"
51
54
  : false;
52
55
 
53
- const sourceCode = context.getSourceCode();
54
- function getCodeIn(range) {
55
- return sourceCode.text.slice(range[0], range[1]);
56
- }
57
-
58
56
  function checkClosingTag(node) {
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
- }
57
+ if (!node.close) {
70
58
  context.report({
71
- node: node.startTag,
59
+ node: node,
72
60
  data: {
73
- tag: node.tagName,
61
+ tag: node.name,
74
62
  },
75
63
  messageId: MESSAGE_IDS.MISSING,
76
64
  });
@@ -78,50 +66,47 @@ module.exports = {
78
66
  }
79
67
 
80
68
  function checkVoidElement(node) {
81
- const startTag = node.startTag;
82
- const code = getCodeIn(startTag.range);
83
- const hasSelfClose = code.endsWith("/>");
84
-
69
+ const hasSelfClose = node.openEnd.value === "/>";
85
70
  if (shouldSelfClose && !hasSelfClose) {
86
71
  context.report({
87
- node: startTag,
72
+ node: node.openEnd,
88
73
  data: {
89
- tag: node.tagName,
74
+ tag: node.name,
90
75
  },
91
76
  messageId: MESSAGE_IDS.MISSING_SELF,
92
77
  fix(fixer) {
93
- return fixer.replaceTextRange(
94
- [startTag.range[1] - 1, startTag.range[1]],
95
- " />"
96
- );
78
+ return fixer.replaceText(node.openEnd, " />");
97
79
  },
98
80
  });
99
81
  }
100
82
  if (!shouldSelfClose && hasSelfClose) {
101
83
  context.report({
102
- node: startTag,
84
+ node: node.openEnd,
103
85
  data: {
104
- tag: node.tagName,
86
+ tag: node.name,
105
87
  },
106
88
  messageId: MESSAGE_IDS.UNEXPECTED,
107
89
  fix(fixer) {
108
- return fixer.replaceTextRange(
109
- [startTag.range[1] - 2, startTag.range[1]],
110
- ">"
111
- );
90
+ return fixer.replaceText(node.openEnd, ">");
112
91
  },
113
92
  });
114
93
  }
115
94
  }
116
95
 
117
96
  return {
118
- "*"(node) {
119
- if (node.startTag) {
120
- if (VOID_ELEMENTS_SET.has(node.tagName)) {
121
- checkVoidElement(node);
122
- } else {
123
- checkClosingTag(node);
124
- }
97
+ Tag(node) {
98
+ if (node.name === "svg") {
99
+ svgStacks.push(node);
100
+ }
101
+ if (node.selfClosing || VOID_ELEMENTS_SET.has(node.name)) {
102
+ checkVoidElement(node);
103
+ } else if (node.openEnd.value !== "/>") {
104
+ checkClosingTag(node);
105
+ }
106
+ },
107
+ "Tag:exit"(node) {
108
+ if (node.name === "svg") {
109
+ svgStacks.push(node);
125
110
  }
126
111
  },
127
112
  };
@@ -31,7 +31,7 @@ module.exports = {
31
31
  create(context) {
32
32
  let hasDocType = false;
33
33
  return {
34
- documentType() {
34
+ Doctype() {
35
35
  hasDocType = true;
36
36
  },
37
37
  "Program:exit"(node) {
@@ -1,7 +1,3 @@
1
- /**
2
- * @typedef {import("../types").Rule} Rule
3
- */
4
-
5
1
  const { RULE_CATEGORY } = require("../constants");
6
2
  const { NodeUtils } = require("./utils");
7
3
 
@@ -10,9 +6,6 @@ const MESSAGE_IDS = {
10
6
  UNEXPECTED: "unexpected",
11
7
  };
12
8
 
13
- /**
14
- * @type {Rule}
15
- */
16
9
  module.exports = {
17
10
  meta: {
18
11
  type: "code",
@@ -33,15 +26,18 @@ module.exports = {
33
26
 
34
27
  create(context) {
35
28
  return {
36
- "Frame, Iframe"(node) {
29
+ Tag(node) {
30
+ if (node.name !== "frame" && node.name !== "iframe") {
31
+ return;
32
+ }
37
33
  const title = NodeUtils.findAttr(node, "title");
38
34
  if (!title) {
39
35
  context.report({
40
- node: node.startTag,
41
- data: { frame: `<${node.tagName}>` },
36
+ node: node.openStart,
37
+ data: { frame: `<${node.name}>` },
42
38
  messageId: MESSAGE_IDS.MISSING,
43
39
  });
44
- } else if (title.value.trim().length === 0) {
40
+ } else if (!title.value || title.value.value.trim().length === 0) {
45
41
  context.report({
46
42
  node: title,
47
43
  messageId: MESSAGE_IDS.UNEXPECTED,