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 ![hover](https://github.com/htmlhint/vscode-htmlhint/raw/main/htmlhint/images/hover.png) +### 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 @@ </p> </div> + < Hello > + <!-- More void elements --> <img src="footer.jpg"> <br />