Skip to content

Commit 4b237b4

Browse files
authored
Merge pull request #269 from htmlhint/dev/coliff/autofix-spec-char-escape
feat: Improve autofix feature (add `spec-char-escape` )
2 parents beea3a0 + 06c6143 commit 4b237b4

File tree

7 files changed

+127
-4
lines changed

7 files changed

+127
-4
lines changed

htmlhint-server/src/server.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,98 @@ function createAttrNoUnnecessaryWhitespaceFix(
12671267
};
12681268
}
12691269

1270+
/**
1271+
* Create auto-fix action for spec-char-escape rule
1272+
*/
1273+
function createSpecCharEscapeFix(
1274+
document: TextDocument,
1275+
diagnostic: Diagnostic,
1276+
): CodeAction | null {
1277+
if (
1278+
!diagnostic.data ||
1279+
diagnostic.data.ruleId !== "spec-char-escape" ||
1280+
typeof diagnostic.data.line !== "number" ||
1281+
typeof diagnostic.data.col !== "number"
1282+
) {
1283+
return null;
1284+
}
1285+
1286+
const text = document.getText();
1287+
const lines = text.split("\n");
1288+
const line = lines[diagnostic.data.line - 1];
1289+
1290+
if (!line) {
1291+
return null;
1292+
}
1293+
1294+
// Find unescaped special characters that need to be escaped
1295+
// We need to be careful not to escape characters that are already in HTML tags or attributes
1296+
const specialCharPattern = /([<>])/g;
1297+
let match;
1298+
const edits: TextEdit[] = [];
1299+
1300+
while ((match = specialCharPattern.exec(line)) !== null) {
1301+
const startCol = match.index;
1302+
const endCol = startCol + match[1].length;
1303+
const char = match[1];
1304+
1305+
// Check if this match is at or near the diagnostic position
1306+
const diagnosticCol = diagnostic.data.col - 1;
1307+
if (Math.abs(startCol - diagnosticCol) <= 5) {
1308+
// Determine if this character is inside a tag (should not be escaped)
1309+
const beforeMatch = line.substring(0, startCol);
1310+
const lastOpenBracket = beforeMatch.lastIndexOf("<");
1311+
const lastCloseBracket = beforeMatch.lastIndexOf(">");
1312+
1313+
// If we're inside a tag (after < but before >), don't escape
1314+
if (lastOpenBracket > lastCloseBracket) {
1315+
continue;
1316+
}
1317+
1318+
const lineStartPos = document.positionAt(
1319+
text
1320+
.split("\n")
1321+
.slice(0, diagnostic.data.line - 1)
1322+
.join("\n").length + (diagnostic.data.line > 1 ? 1 : 0),
1323+
);
1324+
const startPos = { line: lineStartPos.line, character: startCol };
1325+
const endPos = { line: lineStartPos.line, character: endCol };
1326+
1327+
// Map characters to their HTML entities
1328+
const entityMap: { [key: string]: string } = {
1329+
"<": "&lt;",
1330+
">": "&gt;",
1331+
};
1332+
1333+
const replacement = entityMap[char];
1334+
if (replacement) {
1335+
edits.push({
1336+
range: { start: startPos, end: endPos },
1337+
newText: replacement,
1338+
});
1339+
break; // Only fix the first occurrence near the diagnostic
1340+
}
1341+
}
1342+
}
1343+
1344+
if (edits.length === 0) {
1345+
return null;
1346+
}
1347+
1348+
const workspaceEdit: WorkspaceEdit = {
1349+
changes: {
1350+
[document.uri]: edits,
1351+
},
1352+
};
1353+
1354+
return {
1355+
title: "Escape special character",
1356+
kind: CodeActionKind.QuickFix,
1357+
edit: workspaceEdit,
1358+
isPreferred: true,
1359+
};
1360+
}
1361+
12701362
/**
12711363
* Create auto-fix actions for supported rules
12721364
*/
@@ -1345,6 +1437,10 @@ async function createAutoFixes(
13451437
trace(`[DEBUG] Calling createAttrNoUnnecessaryWhitespaceFix`);
13461438
fix = createAttrNoUnnecessaryWhitespaceFix(document, diagnostic);
13471439
break;
1440+
case "spec-char-escape":
1441+
trace(`[DEBUG] Calling createSpecCharEscapeFix`);
1442+
fix = createSpecCharEscapeFix(document, diagnostic);
1443+
break;
13481444
default:
13491445
trace(`[DEBUG] No autofix function found for rule: ${ruleId}`);
13501446
break;

htmlhint/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
All notable changes to the "vscode-htmlhint" extension will be documented in this file.
44

5+
### v1.10.2 (2025-06-19)
6+
7+
- Add autofix for `spec-char-escape` rule
8+
- Rename extension output channel to "HTMLHint Extension" for better debugging
9+
510
### v1.10.1 (2025-06-19)
611

712
- Update HTMLHint to v1.6.3

htmlhint/README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Integrates the [HTMLHint](https://github.com/htmlhint/HTMLHint) static analysis
66

77
## Configuration
88

9-
The HTMLHint extension will attempt to use the locally installed HTMLHint module (the project-specific module if present, or a globally installed HTMLHint module). If a locally installed HTMLHint isn't available, the extension will use the embedded version (current version 1.5.1).
9+
The HTMLHint extension will attempt to use the locally installed HTMLHint module (the project-specific module if present, or a globally installed HTMLHint module). If a locally installed HTMLHint isn't available, the extension will use the embedded version (current version 1.6.3).
1010

1111
To install a version to the local project folder, run `npm install --save-dev htmlhint`. To install a global version on the current machine, run `npm install --global htmlhint`.
1212

@@ -22,6 +22,25 @@ Many problems can now be fixed automatically by clicking on the lightbulb icon i
2222

2323
![hover](https://github.com/htmlhint/vscode-htmlhint/raw/main/htmlhint/images/hover.png)
2424

25+
### Auto-fix Support
26+
27+
The extension provides automatic fixes for many common HTML issues. Currently supported auto-fixes include:
28+
29+
- **`alt-require`** - Adds alt attribute to images
30+
- **`attr-lowercase`** - Converts uppercase attribute names to lowercase
31+
- **`attr-no-unnecessary-whitespace`** - Removes unnecessary whitespace around attributes
32+
- **`attr-value-double-quotes`** - Converts single quotes to double quotes in attributes
33+
- **`button-type-require`** - Adds type attribute to buttons
34+
- **`doctype-first`** - Adds DOCTYPE declaration at the beginning
35+
- **`doctype-html5`** - Updates DOCTYPE to HTML5
36+
- **`html-lang-require`** - Adds `lang` attribute to `<html>` tag
37+
- **`meta-charset-require`** - Adds charset meta tag
38+
- **`meta-description-require`** - Adds description meta tag
39+
- **`meta-viewport-require`** - Adds viewport meta tag
40+
- **`spec-char-escape`** - Escapes special characters (`<`, `>`)
41+
- **`tagname-lowercase`** - Converts uppercase tag names to lowercase
42+
- **`title-require`** - Adds `<title>` tag to document
43+
2544
> **Note:** HTMLHint will only analyze open HTML files and does not search for HTML files in your project folder.
2645
2746
## Rules

htmlhint/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ let outputChannel: vscode.OutputChannel;
1515

1616
export function activate(context: vscode.ExtensionContext) {
1717
// Create output channel for logging
18-
outputChannel = vscode.window.createOutputChannel("HTMLHint");
18+
outputChannel = vscode.window.createOutputChannel("HTMLHint Extension");
1919
context.subscriptions.push(outputChannel);
2020

2121
// Register the create config command

htmlhint/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "HTMLHint",
44
"description": "VS Code integration for HTMLHint - A Static Code Analysis Tool for HTML",
55
"icon": "images/icon.png",
6-
"version": "1.10.1",
6+
"version": "1.10.2",
77
"publisher": "HTMLHint",
88
"galleryBanner": {
99
"color": "#333333",

test/autofix/.htmlhintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
"alt-require": true,
33
"attr-lowercase": true,
44
"attr-no-unnecessary-whitespace": true,
5-
"button-type-require": true,
65
"attr-value-double-quotes": true,
6+
"attr-value-no-duplication": true,
7+
"button-type-require": true,
78
"doctype-first": true,
89
"doctype-html5": true,
910
"html-lang-require": true,

test/autofix/test-autofixes.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
</p>
3737
</div>
3838

39+
< Hello >
40+
3941
<!-- More void elements -->
4042
<img src="footer.jpg">
4143
<br />

0 commit comments

Comments
 (0)