diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts
index 2ac9e4b..3e1f6de 100644
--- a/htmlhint-server/src/server.ts
+++ b/htmlhint-server/src/server.ts
@@ -2232,6 +2232,89 @@ async function createAutoFixes(
return actions;
}
+/**
+ * Suppress a rule for the diagnostic's line by adding an
+ * comment above it. If the line above
+ * already has such a comment, the rule is appended to it.
+ */
+function createDisableNextLineAction(
+ document: TextDocument,
+ diagnostic: Diagnostic,
+): CodeAction | null {
+ const ruleId = diagnostic.data?.ruleId;
+ if (typeof ruleId !== "string" || !ruleId) {
+ return null;
+ }
+
+ const lineIndex = diagnostic.range.start.line;
+ const text = document.getText();
+ const lines = text.split(/\r?\n/);
+ const lineText = lines[lineIndex];
+ if (lineText === undefined) {
+ return null;
+ }
+
+ const title = `Disable ${ruleId} for this line`;
+ const previousLine = lineIndex > 0 ? lines[lineIndex - 1] : undefined;
+ // HTMLHint only honours the first directive on a line, wherever it appears
+ const existing = previousLine?.match(
+ //i,
+ );
+
+ if (
+ existing?.index !== undefined &&
+ existing[1].toLowerCase() === "disable" &&
+ existing[2]
+ ) {
+ // Without a rule list the comment already disables every rule
+ if (!existing[3]) {
+ return null;
+ }
+
+ const rules = existing[3].split(/\s+/).filter((rule) => rule.length > 0);
+ if (rules.includes(ruleId)) {
+ return null;
+ }
+
+ // Replace only the directive so any surrounding content on the line is kept
+ return makeFix(
+ document,
+ title,
+ [
+ {
+ range: {
+ start: { line: lineIndex - 1, character: existing.index },
+ end: {
+ line: lineIndex - 1,
+ character: existing.index + existing[0].length,
+ },
+ },
+ newText: ``,
+ },
+ ],
+ diagnostic,
+ );
+ }
+
+ const indent = lineText.match(/^\s*/)?.[0] ?? "";
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
+
+ return makeFix(
+ document,
+ title,
+ [
+ {
+ range: {
+ start: { line: lineIndex, character: 0 },
+ end: { line: lineIndex, character: 0 },
+ },
+ newText: `${indent}${eol}`,
+ },
+ ],
+ diagnostic,
+ );
+}
+
connection.onInitialize(
(_params: InitializeParams, _token: CancellationToken) => {
// Since Files API is no longer available, we'll use embedded htmlhint directly
@@ -2549,7 +2632,9 @@ connection.onRequest(
}
// Create auto-fixes for each diagnostic
- const codeActions: LSPCodeAction[] = [];
+ const fixActions: CodeAction[] = [];
+ const disableActions: CodeAction[] = [];
+ const disabledRuleLines = new Set();
for (const diagnostic of filteredDiagnostics) {
trace(
`[DEBUG] Creating fixes for diagnostic: ${JSON.stringify(diagnostic)}`,
@@ -2581,41 +2666,59 @@ connection.onRequest(
trace(
`[DEBUG] Created ${fixes.length} fixes for diagnostic: ${JSON.stringify(enhancedDiagnostic)}`,
);
- codeActions.push(
- ...fixes.map((fix) => ({
- title: fix.title,
- kind: fix.kind,
- diagnostics: fix.diagnostics,
- isPreferred: fix.isPreferred,
- edit: fix.edit
- ? {
- changes: fix.edit.documentChanges
- ? {
- [uri]: (fix.edit.documentChanges[0] as any).edits || [],
- }
- : fix.edit.changes && fix.edit.changes[uri]
- ? {
- [uri]: fix.edit.changes[uri].map((change) => ({
- range: {
- start: {
- line: change.range.start.line,
- character: change.range.start.character,
- },
- end: {
- line: change.range.end.line,
- character: change.range.end.character,
- },
- },
- newText: change.newText,
- })),
- }
- : { [uri]: [] },
- }
- : undefined,
- })),
- );
+
+ // Offer one disable action per rule and line, listed after all autofixes
+ const disableKey = `${enhancedDiagnostic.range.start.line}:${enhancedDiagnostic.data?.ruleId}`;
+ if (!disabledRuleLines.has(disableKey)) {
+ disabledRuleLines.add(disableKey);
+ const disableAction = createDisableNextLineAction(
+ document,
+ enhancedDiagnostic,
+ );
+ if (disableAction) {
+ disableAction.isPreferred = false;
+ disableActions.push(disableAction);
+ }
+ }
+
+ fixActions.push(...fixes);
}
+ const codeActions: LSPCodeAction[] = [
+ ...fixActions,
+ ...disableActions,
+ ].map((fix) => ({
+ title: fix.title,
+ kind: fix.kind,
+ diagnostics: fix.diagnostics,
+ isPreferred: fix.isPreferred,
+ edit: fix.edit
+ ? {
+ changes: fix.edit.documentChanges
+ ? {
+ [uri]: (fix.edit.documentChanges[0] as any).edits || [],
+ }
+ : fix.edit.changes && fix.edit.changes[uri]
+ ? {
+ [uri]: fix.edit.changes[uri].map((change) => ({
+ range: {
+ start: {
+ line: change.range.start.line,
+ character: change.range.start.character,
+ },
+ end: {
+ line: change.range.end.line,
+ character: change.range.end.character,
+ },
+ },
+ newText: change.newText,
+ })),
+ }
+ : { [uri]: [] },
+ }
+ : undefined,
+ }));
+
trace(`[DEBUG] Created ${codeActions.length} auto-fix actions`);
trace(`[DEBUG] Code actions: ${JSON.stringify(codeActions)}`);
diff --git a/htmlhint/CHANGELOG.md b/htmlhint/CHANGELOG.md
index a9e8ed7..0848c4f 100644
--- a/htmlhint/CHANGELOG.md
+++ b/htmlhint/CHANGELOG.md
@@ -2,9 +2,10 @@
All notable changes to the "vscode-htmlhint" extension will be documented in this file.
-### v1.18.0 (2026-09-26)
+### v1.18.0 (Unreleased)
- Add autofix for the `attr-unsafe-chars`, `attr-value-single-quotes` and `frame-title-require` rules
+- Add "Disable rule for this line" Quick Fix for every rule, which inserts an `` comment
### v1.17.0 (2026-09-23)
diff --git a/htmlhint/LICENSE.md b/htmlhint/LICENSE.md
index 8362552..c7a9dba 100644
--- a/htmlhint/LICENSE.md
+++ b/htmlhint/LICENSE.md
@@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright (c) 2015-2022 Microsoft
-Copyright (c) 2022-2025 HTMLHint
+Copyright (c) 2022-2026 HTMLHint
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/htmlhint/README.md b/htmlhint/README.md
index 9d031a2..801eb87 100644
--- a/htmlhint/README.md
+++ b/htmlhint/README.md
@@ -20,6 +20,8 @@ Errors in HTML files are highlighted with squiggles and you can hover over the s
Many problems can now be fixed automatically by clicking on the lightbulb icon in problems panel or right-clicking on the error in the HTML file and selecting "Quick Fix".
+Every problem also offers a "Disable rule-id for this line" Quick Fix, which adds an `` comment above the line (or appends the rule to an existing one).
+

### Auto-fix Support
diff --git a/htmlhint/package-lock.json b/htmlhint/package-lock.json
index 3fb5963..590e517 100644
--- a/htmlhint/package-lock.json
+++ b/htmlhint/package-lock.json
@@ -9,7 +9,7 @@
"version": "1.18.0",
"license": "SEE LICENSE IN LICENSE.md",
"devDependencies": {
- "@types/node": "^22.20.4",
+ "@types/node": "^22.20.5",
"@types/vscode": "^1.101.0",
"@vscode/test-electron": "^3.1.0",
"esbuild": "^0.28.2",
@@ -463,9 +463,9 @@
}
},
"node_modules/@types/node": {
- "version": "22.20.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.4.tgz",
- "integrity": "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA==",
+ "version": "22.20.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz",
+ "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/htmlhint/package.json b/htmlhint/package.json
index 32953b7..2d4a976 100644
--- a/htmlhint/package.json
+++ b/htmlhint/package.json
@@ -91,7 +91,7 @@
"package": "vsce package"
},
"devDependencies": {
- "@types/node": "^22.20.4",
+ "@types/node": "^22.20.5",
"@types/vscode": "^1.101.0",
"@vscode/test-electron": "^3.1.0",
"esbuild": "^0.28.2",
diff --git a/test/autofix/.htmlhintrc b/test/autofix/.htmlhintrc
index 1c532fd..68e6903 100644
--- a/test/autofix/.htmlhintrc
+++ b/test/autofix/.htmlhintrc
@@ -3,13 +3,16 @@
"attr-lowercase": true,
"attr-no-duplication": true,
"attr-no-unnecessary-whitespace": true,
+ "attr-unsafe-chars": true,
"attr-value-double-quotes": true,
"attr-value-no-duplication": true,
+ "attr-value-single-quotes": false,
"attr-whitespace": true,
"button-type-require": true,
"doctype-first": true,
"doctype-html5": true,
"empty-tag-not-self-closed": false,
+ "frame-title-require": true,
"html-lang-require": true,
"id-unique": true,
"link-rel-canonical-require": true,
diff --git a/test/autofix/test-autofixes.html b/test/autofix/test-autofixes.html
index 93a5ea8..43a677d 100644
--- a/test/autofix/test-autofixes.html
+++ b/test/autofix/test-autofixes.html
@@ -63,6 +63,17 @@
XML
+
+ Unsafe link
+ Soft hyphen in title
+
+
+ Quotes
+
+
+
+
+