Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions htmlhint-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {
"<": "&lt;",
">": "&gt;",
};

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
*/
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions htmlhint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion htmlhint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -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 `<html>` 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 `<title>` tag to document

> **Note:** HTMLHint will only analyze open HTML files and does not search for HTML files in your project folder.

## Rules
Expand Down
2 changes: 1 addition & 1 deletion htmlhint/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion htmlhint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion test/autofix/.htmlhintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions test/autofix/test-autofixes.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
</p>
</div>

< Hello >

<!-- More void elements -->
<img src="footer.jpg">
<br />
Expand Down