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 `