From 06c6143227b766b30c763797994ee6cd602946e8 Mon Sep 17 00:00:00 2001
From: coliff
Date: Thu, 19 Jun 2025 11:56:35 +0900
Subject: [PATCH] feat: Improve autofix feature (add `spec-char-escape` )
---
htmlhint-server/src/server.ts | 96 ++++++++++++++++++++++++++++++++
htmlhint/CHANGELOG.md | 5 ++
htmlhint/README.md | 21 ++++++-
htmlhint/extension.ts | 2 +-
htmlhint/package.json | 2 +-
test/autofix/.htmlhintrc | 3 +-
test/autofix/test-autofixes.html | 2 +
7 files changed, 127 insertions(+), 4 deletions(-)
diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts
index 4d9fa0b..e33c50d 100644
--- a/htmlhint-server/src/server.ts
+++ b/htmlhint-server/src/server.ts
@@ -1267,6 +1267,98 @@ function createAttrNoUnnecessaryWhitespaceFix(
};
}
+/**
+ * Create auto-fix action for spec-char-escape rule
+ */
+function createSpecCharEscapeFix(
+ document: TextDocument,
+ diagnostic: Diagnostic,
+): CodeAction | null {
+ if (
+ !diagnostic.data ||
+ diagnostic.data.ruleId !== "spec-char-escape" ||
+ typeof diagnostic.data.line !== "number" ||
+ typeof diagnostic.data.col !== "number"
+ ) {
+ return null;
+ }
+
+ const text = document.getText();
+ const lines = text.split("\n");
+ const line = lines[diagnostic.data.line - 1];
+
+ if (!line) {
+ return null;
+ }
+
+ // Find unescaped special characters that need to be escaped
+ // We need to be careful not to escape characters that are already in HTML tags or attributes
+ const specialCharPattern = /([<>])/g;
+ let match;
+ const edits: TextEdit[] = [];
+
+ while ((match = specialCharPattern.exec(line)) !== null) {
+ const startCol = match.index;
+ const endCol = startCol + match[1].length;
+ const char = match[1];
+
+ // Check if this match is at or near the diagnostic position
+ const diagnosticCol = diagnostic.data.col - 1;
+ if (Math.abs(startCol - diagnosticCol) <= 5) {
+ // Determine if this character is inside a tag (should not be escaped)
+ const beforeMatch = line.substring(0, startCol);
+ const lastOpenBracket = beforeMatch.lastIndexOf("<");
+ const lastCloseBracket = beforeMatch.lastIndexOf(">");
+
+ // If we're inside a tag (after < but before >), don't escape
+ if (lastOpenBracket > lastCloseBracket) {
+ continue;
+ }
+
+ const lineStartPos = document.positionAt(
+ text
+ .split("\n")
+ .slice(0, diagnostic.data.line - 1)
+ .join("\n").length + (diagnostic.data.line > 1 ? 1 : 0),
+ );
+ const startPos = { line: lineStartPos.line, character: startCol };
+ const endPos = { line: lineStartPos.line, character: endCol };
+
+ // Map characters to their HTML entities
+ const entityMap: { [key: string]: string } = {
+ "<": "<",
+ ">": ">",
+ };
+
+ const replacement = entityMap[char];
+ if (replacement) {
+ edits.push({
+ range: { start: startPos, end: endPos },
+ newText: replacement,
+ });
+ break; // Only fix the first occurrence near the diagnostic
+ }
+ }
+ }
+
+ if (edits.length === 0) {
+ return null;
+ }
+
+ const workspaceEdit: WorkspaceEdit = {
+ changes: {
+ [document.uri]: edits,
+ },
+ };
+
+ return {
+ title: "Escape special character",
+ kind: CodeActionKind.QuickFix,
+ edit: workspaceEdit,
+ isPreferred: true,
+ };
+}
+
/**
* Create auto-fix actions for supported rules
*/
@@ -1345,6 +1437,10 @@ async function createAutoFixes(
trace(`[DEBUG] Calling createAttrNoUnnecessaryWhitespaceFix`);
fix = createAttrNoUnnecessaryWhitespaceFix(document, diagnostic);
break;
+ case "spec-char-escape":
+ trace(`[DEBUG] Calling createSpecCharEscapeFix`);
+ fix = createSpecCharEscapeFix(document, diagnostic);
+ break;
default:
trace(`[DEBUG] No autofix function found for rule: ${ruleId}`);
break;
diff --git a/htmlhint/CHANGELOG.md b/htmlhint/CHANGELOG.md
index e25547c..1a70ee3 100644
--- a/htmlhint/CHANGELOG.md
+++ b/htmlhint/CHANGELOG.md
@@ -2,6 +2,11 @@
All notable changes to the "vscode-htmlhint" extension will be documented in this file.
+### v1.10.2 (2025-06-19)
+
+- Add autofix for `spec-char-escape` rule
+- Rename extension output channel to "HTMLHint Extension" for better debugging
+
### v1.10.1 (2025-06-19)
- Update HTMLHint to v1.6.3
diff --git a/htmlhint/README.md b/htmlhint/README.md
index c6c0cc6..0714b52 100644
--- a/htmlhint/README.md
+++ b/htmlhint/README.md
@@ -6,7 +6,7 @@ Integrates the [HTMLHint](https://github.com/htmlhint/HTMLHint) static analysis
## Configuration
-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).
+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).
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`.
@@ -22,6 +22,25 @@ Many problems can now be fixed automatically by clicking on the lightbulb icon i

+### Auto-fix Support
+
+The extension provides automatic fixes for many common HTML issues. Currently supported auto-fixes include:
+
+- **`alt-require`** - Adds alt attribute to images
+- **`attr-lowercase`** - Converts uppercase attribute names to lowercase
+- **`attr-no-unnecessary-whitespace`** - Removes unnecessary whitespace around attributes
+- **`attr-value-double-quotes`** - Converts single quotes to double quotes in attributes
+- **`button-type-require`** - Adds type attribute to buttons
+- **`doctype-first`** - Adds DOCTYPE declaration at the beginning
+- **`doctype-html5`** - Updates DOCTYPE to HTML5
+- **`html-lang-require`** - Adds `lang` attribute to `` tag
+- **`meta-charset-require`** - Adds charset meta tag
+- **`meta-description-require`** - Adds description meta tag
+- **`meta-viewport-require`** - Adds viewport meta tag
+- **`spec-char-escape`** - Escapes special characters (`<`, `>`)
+- **`tagname-lowercase`** - Converts uppercase tag names to lowercase
+- **`title-require`** - Adds `` tag to document
+
> **Note:** HTMLHint will only analyze open HTML files and does not search for HTML files in your project folder.
## Rules
diff --git a/htmlhint/extension.ts b/htmlhint/extension.ts
index 5b9bc5e..bb18522 100644
--- a/htmlhint/extension.ts
+++ b/htmlhint/extension.ts
@@ -15,7 +15,7 @@ let outputChannel: vscode.OutputChannel;
export function activate(context: vscode.ExtensionContext) {
// Create output channel for logging
- outputChannel = vscode.window.createOutputChannel("HTMLHint");
+ outputChannel = vscode.window.createOutputChannel("HTMLHint Extension");
context.subscriptions.push(outputChannel);
// Register the create config command
diff --git a/htmlhint/package.json b/htmlhint/package.json
index 97911ef..b755e42 100644
--- a/htmlhint/package.json
+++ b/htmlhint/package.json
@@ -3,7 +3,7 @@
"displayName": "HTMLHint",
"description": "VS Code integration for HTMLHint - A Static Code Analysis Tool for HTML",
"icon": "images/icon.png",
- "version": "1.10.1",
+ "version": "1.10.2",
"publisher": "HTMLHint",
"galleryBanner": {
"color": "#333333",
diff --git a/test/autofix/.htmlhintrc b/test/autofix/.htmlhintrc
index 2ba7a7c..f7eef73 100644
--- a/test/autofix/.htmlhintrc
+++ b/test/autofix/.htmlhintrc
@@ -2,8 +2,9 @@
"alt-require": true,
"attr-lowercase": true,
"attr-no-unnecessary-whitespace": true,
- "button-type-require": true,
"attr-value-double-quotes": true,
+ "attr-value-no-duplication": true,
+ "button-type-require": true,
"doctype-first": true,
"doctype-html5": true,
"html-lang-require": true,
diff --git a/test/autofix/test-autofixes.html b/test/autofix/test-autofixes.html
index 88ba159..af75963 100644
--- a/test/autofix/test-autofixes.html
+++ b/test/autofix/test-autofixes.html
@@ -36,6 +36,8 @@
+ < Hello >
+