Skip to content

Commit e2a488c

Browse files
feat: new rule proposal: no-invalid-entity-351 (#371)
* feat:New rule proposal: no-invalid-entity-351 * Update no-invalid-entity.js removed extra whitespace * fixes: yarn lint. run yarn format and added nsnb in cspell * Update rules.md * fix * Update no-invalid-entity.test.js --------- Co-authored-by: YeonJuan <[email protected]>
1 parent 7c0dc9d commit e2a488c

File tree

7 files changed

+2495
-1
lines changed

7 files changed

+2495
-1
lines changed

.cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"treegrid",
5757
"contentinfo",
5858
"smashingmagazine",
59-
"contenteditable"
59+
"contenteditable",
60+
"nbsb"
6061
]
6162
}

docs/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
| [no-duplicate-class](rules/no-duplicate-class) | Disallow to use duplicate class | 🔧 |
1515
| [no-duplicate-id](rules/no-duplicate-id) | Disallow to use duplicate id ||
1616
| [no-inline-styles](rules/no-inline-styles) | Disallow using inline style | |
17+
| [no-invalid-entity](rules/no-invalid-entity) | Disallows the use of invalid HTML entities | |
1718
| [no-nested-interactive](rules/no-nested-interactive) | Disallows nested interactive elements | |
1819
| [no-obsolete-tags](rules/no-obsolete-tags) | Disallow to use obsolete elements in HTML5 ||
1920
| [no-restricted-attr-values](rules/no-restricted-attr-values) | Disallow specified attributes | |

docs/rules/no-invalid-entity.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# no-invalid-entity
2+
3+
Disallow use of invalid HTML entities.
4+
5+
## Rule Details
6+
7+
This rule disallows the use of invalid HTML entities in your markup. HTML entities are special codes used to represent characters with specific meanings in HTML, such as `<` (`<`) or `&` (`&`). Invalid entities—such as typos, undefined entities, or malformed numeric references—can be silently ignored by browsers, leading to rendering issues or confusion.
8+
9+
The rule validates both named entities (e.g., ` `) and numeric entities (e.g., ` ` for decimal or ` ` for hexadecimal) against a list of valid entities defined in `entities.json`. An entity is considered invalid if:
10+
11+
- It is a named entity not found in `entities.json` (e.g., `&nbsb;` or `&unknown;`).
12+
- It is a numeric entity with an invalid format (e.g., `&#zzzz;`).
13+
- It is a numeric entity outside the valid Unicode range (0 to 0x10FFFF, e.g., ``).
14+
15+
## How to use
16+
17+
```js,.eslintrc.js
18+
module.exports = {
19+
rules: {
20+
"@html-eslint/no-invalid-entity": "error",
21+
},
22+
};
23+
```
24+
25+
### Examples
26+
27+
#### Incorrect Code
28+
29+
```html
30+
<p>&nbsb;</p> <!-- typo -->
31+
<p>&unknown;</p> <!-- undefined entity -->
32+
<p>&#zzzz;</p> <!-- invalid numeric reference -->
33+
```
34+
35+
#### Correct Code
36+
37+
```html
38+
<p>&lt; &gt; &amp; &nbsp; &#160; &#xA0;</p>
39+
```
40+
41+
## When Not To Use It
42+
43+
Disable this rule if you are intentionally using invalid entities for specific purposes, such as testing or non-standard rendering. Be aware that invalid entities may not render consistently across different browsers.

packages/eslint-plugin/lib/data/entities.json

Lines changed: 2299 additions & 0 deletions
Large diffs are not rendered by default.

packages/eslint-plugin/lib/rules/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const requireExplicitSize = require("./require-explicit-size");
4949
const useBaseLine = require("./use-baseline");
5050
const noDuplicateClass = require("./no-duplicate-class");
5151
const noEmptyHeadings = require("./no-empty-headings");
52+
const noInvalidEntity = require("./no-invalid-entity");
5253
// import new rule here ↑
5354
// DO NOT REMOVE THIS COMMENT
5455

@@ -104,6 +105,7 @@ const rules = {
104105
"use-baseline": useBaseLine,
105106
"no-duplicate-class": noDuplicateClass,
106107
"no-empty-headings": noEmptyHeadings,
108+
"no-invalid-entity": noInvalidEntity,
107109
// export new rule here ↑
108110
// DO NOT REMOVE THIS COMMENT
109111
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @typedef { import("../types").RuleModule<[]> } RuleModule
3+
* @typedef { import("../types").SuggestionReportDescriptor } SuggestionReportDescriptor
4+
* @typedef { import("@html-eslint/types").Text} Text
5+
*/
6+
7+
// Define the type for entities.json
8+
/**
9+
* @typedef {Object} EntityData
10+
* @property {number[]} codepoints
11+
* @property {string} characters
12+
*/
13+
14+
/** @type {{ [key: string]: EntityData }} */
15+
const entities = require("../data/entities.json");
16+
17+
const { RULE_CATEGORY } = require("../constants");
18+
const { createVisitors } = require("./utils/visitors");
19+
const { getRuleUrl } = require("./utils/rule");
20+
21+
const MESSAGE_IDS = {
22+
INVALID_ENTITY: "invalidEntity",
23+
};
24+
25+
/**
26+
* @type {RuleModule}
27+
*/
28+
module.exports = {
29+
meta: {
30+
type: "code",
31+
docs: {
32+
description: "Disallows the use of invalid HTML entities",
33+
category: RULE_CATEGORY.BEST_PRACTICE,
34+
recommended: false,
35+
url: getRuleUrl("no-invalid-entity"),
36+
},
37+
fixable: null,
38+
hasSuggestions: false,
39+
schema: [],
40+
messages: {
41+
[MESSAGE_IDS.INVALID_ENTITY]: "Invalid HTML entity '{{entity}}' used.",
42+
},
43+
},
44+
45+
create(context) {
46+
/**
47+
* @param {Text} node
48+
*/
49+
function check(node) {
50+
const text = node.value;
51+
52+
// Regular expression to match named and numeric entities
53+
const entityRegex = /&([a-zA-Z]+|#[0-9]+|#x[0-9a-fA-F]+|[#][^;]+);/g;
54+
let match;
55+
56+
while ((match = entityRegex.exec(text)) !== null) {
57+
const entity = match[0];
58+
const entityName = match[1];
59+
60+
// Check named entities
61+
if (!entityName.startsWith("#")) {
62+
const fullEntity = `&${entityName};`;
63+
if (!Object.prototype.hasOwnProperty.call(entities, fullEntity)) {
64+
context.report({
65+
node,
66+
messageId: MESSAGE_IDS.INVALID_ENTITY,
67+
data: { entity },
68+
});
69+
}
70+
}
71+
// Check numeric entities
72+
else {
73+
const isHex = entityName[1] === "x";
74+
const numStr = isHex ? entityName.slice(2) : entityName.slice(1);
75+
const num = isHex ? parseInt(numStr, 16) : parseInt(numStr, 10);
76+
77+
// If the number is not a valid integer, report an error
78+
if (isNaN(num)) {
79+
context.report({
80+
node,
81+
messageId: MESSAGE_IDS.INVALID_ENTITY,
82+
data: { entity },
83+
});
84+
continue;
85+
}
86+
87+
// Check if the numeric entity is valid (exists in entities.json or within valid Unicode range)
88+
const entityKey = Object.keys(entities).find((key) => {
89+
const codepoints = entities[key].codepoints;
90+
return codepoints.length === 1 && codepoints[0] === num;
91+
});
92+
93+
if (!entityKey && (num < 0 || num > 0x10ffff)) {
94+
context.report({
95+
node,
96+
messageId: MESSAGE_IDS.INVALID_ENTITY,
97+
data: { entity },
98+
});
99+
}
100+
}
101+
}
102+
}
103+
104+
return createVisitors(context, {
105+
Text: check,
106+
});
107+
},
108+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const createRuleTester = require("../rule-tester");
2+
const rule = require("../../lib/rules/no-invalid-entity");
3+
4+
const ruleTester = createRuleTester();
5+
const templateRuleTester = createRuleTester("espree");
6+
7+
ruleTester.run("no-invalid-entity", rule, {
8+
valid: [
9+
{ code: "<p>&lt; &gt; &amp; &nbsp;</p>" },
10+
{ code: "<p>&#xD55C;</p>" },
11+
],
12+
invalid: [
13+
{
14+
code: "<p>&nbsb;</p>", // Typo in entity name
15+
errors: [{ message: "Invalid HTML entity '&nbsb;' used." }],
16+
},
17+
{
18+
code: "<p>&unknown;</p>", // Undefined entity
19+
errors: [{ message: "Invalid HTML entity '&unknown;' used." }],
20+
},
21+
{
22+
code: "<p>&#zzzz;</p>", // Invalid numeric entity
23+
errors: [{ message: "Invalid HTML entity '&#zzzz;' used." }],
24+
},
25+
{
26+
code: "<p>&#x110000;</p>",
27+
errors: [{ message: "Invalid HTML entity '&#x110000;' used." }],
28+
},
29+
],
30+
});
31+
32+
templateRuleTester.run("[template] no-invalid-entity", rule, {
33+
valid: [{ code: `html\`<p>&lt; &gt; &amp; &nbsp;</p>\`` }],
34+
invalid: [
35+
{
36+
code: "html`<p>&nbsb;</p>`",
37+
errors: [{ message: "Invalid HTML entity '&nbsb;' used." }],
38+
},
39+
],
40+
});

0 commit comments

Comments
 (0)