Skip to content

Commit 907b854

Browse files
authored
Merge pull request #340 from htmlhint/dev/coliff/improve-autofix-void-tag
Smarter autofix for rules which accommodates for `tag-self-close` rule.
2 parents 106275a + e0d6c5a commit 907b854

File tree

6 files changed

+100
-25
lines changed

6 files changed

+100
-25
lines changed

htmlhint-server/src/server.ts

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,67 @@ function loadConfigurationFile(configFile: string): HtmlHintConfig | null {
262262
return ruleset;
263263
}
264264

265+
function isHtmlHintRuleEnabled(value: unknown): boolean {
266+
if (value === undefined || value === null) {
267+
return false;
268+
}
269+
270+
if (Array.isArray(value)) {
271+
if (value.length === 0) {
272+
return false;
273+
}
274+
return isHtmlHintRuleEnabled(value[0]);
275+
}
276+
277+
if (typeof value === "boolean") {
278+
return value;
279+
}
280+
281+
if (typeof value === "number") {
282+
return value !== 0;
283+
}
284+
285+
if (typeof value === "string") {
286+
const normalized = value.trim().toLowerCase();
287+
return normalized !== "false" && normalized !== "0" && normalized !== "";
288+
}
289+
290+
if (typeof value === "object") {
291+
return true;
292+
}
293+
294+
return false;
295+
}
296+
297+
function isRuleEnabledForDocument(
298+
document: TextDocument,
299+
ruleId: string,
300+
): boolean {
301+
try {
302+
const parsedUri = URI.parse(document.uri);
303+
304+
if (parsedUri.scheme !== "file") {
305+
trace(
306+
`[DEBUG] isRuleEnabledForDocument: Non-file scheme for ${document.uri}, rule ${ruleId} treated as disabled`,
307+
);
308+
return false;
309+
}
310+
311+
const config = getConfiguration(parsedUri.fsPath);
312+
const ruleValue = (config as Record<string, unknown>)[ruleId];
313+
const enabled = isHtmlHintRuleEnabled(ruleValue);
314+
trace(
315+
`[DEBUG] isRuleEnabledForDocument: Rule ${ruleId} enabled=${enabled} for ${document.uri}`,
316+
);
317+
return enabled;
318+
} catch (error) {
319+
trace(
320+
`[DEBUG] isRuleEnabledForDocument: Failed to determine rule ${ruleId} for ${document.uri}: ${error}`,
321+
);
322+
return false;
323+
}
324+
}
325+
265326
function isErrorWithMessage(err: unknown): err is { message: string } {
266327
return (
267328
typeof err === "object" &&
@@ -789,7 +850,9 @@ function createMetaCharsetRequireFix(
789850
// Insert charset meta tag at the beginning of head (right after <head>)
790851
const headStart = headMatch.index! + headMatch[0].indexOf(">") + 1;
791852
const insertPosition = headStart;
792-
const newText = '\n <meta charset="UTF-8">';
853+
const shouldSelfClose = isRuleEnabledForDocument(document, "tag-self-close");
854+
const newText =
855+
'\n <meta charset="UTF-8"' + (shouldSelfClose ? " />" : ">");
793856

794857
const edit: TextEdit = {
795858
range: {
@@ -846,27 +909,28 @@ function createMetaViewportRequireFix(
846909
/<meta\s+charset\s*=\s*["'][^"']*["'][^>]*>/i,
847910
);
848911

912+
const shouldSelfClose = isRuleEnabledForDocument(document, "tag-self-close");
913+
trace(
914+
`[DEBUG] createMetaViewportRequireFix: tag-self-close enabled=${shouldSelfClose}`,
915+
);
916+
const viewportSnippet = `\n <meta name="viewport" content="width=device-width, initial-scale=1.0"${shouldSelfClose ? " />" : ">"}`;
917+
849918
let insertPosition: number;
850-
let newText: string;
851919

852920
if (metaCharsetMatch) {
853921
const metaCharsetEnd =
854922
headStart + metaCharsetMatch.index! + metaCharsetMatch[0].length;
855923
insertPosition = metaCharsetEnd;
856-
newText =
857-
'\n <meta name="viewport" content="width=device-width, initial-scale=1.0">';
858924
} else {
859925
insertPosition = headStart;
860-
newText =
861-
'\n <meta name="viewport" content="width=device-width, initial-scale=1.0">';
862926
}
863927

864928
const edit: TextEdit = {
865929
range: {
866930
start: document.positionAt(insertPosition),
867931
end: document.positionAt(insertPosition),
868932
},
869-
newText: newText,
933+
newText: viewportSnippet,
870934
};
871935

872936
const workspaceEdit: WorkspaceEdit = {
@@ -923,32 +987,32 @@ function createMetaDescriptionRequireFix(
923987
);
924988

925989
let insertPosition: number;
926-
let newText: string;
990+
const shouldSelfClose = isRuleEnabledForDocument(document, "tag-self-close");
991+
const descriptionSnippet =
992+
'\n <meta name="description" content=""' +
993+
(shouldSelfClose ? " />" : ">");
927994

928995
if (metaViewportMatch) {
929996
// Insert after viewport meta tag
930997
const metaViewportEnd =
931998
headStart + metaViewportMatch.index! + metaViewportMatch[0].length;
932999
insertPosition = metaViewportEnd;
933-
newText = '\n <meta name="description" content="">';
9341000
} else if (metaCharsetMatch) {
9351001
// Insert after charset meta tag
9361002
const metaCharsetEnd =
9371003
headStart + metaCharsetMatch.index! + metaCharsetMatch[0].length;
9381004
insertPosition = metaCharsetEnd;
939-
newText = '\n <meta name="description" content="">';
9401005
} else {
9411006
// Insert at the beginning of head
9421007
insertPosition = headStart;
943-
newText = '\n <meta name="description" content="">';
9441008
}
9451009

9461010
const edit: TextEdit = {
9471011
range: {
9481012
start: document.positionAt(insertPosition),
9491013
end: document.positionAt(insertPosition),
9501014
},
951-
newText: newText,
1015+
newText: descriptionSnippet,
9521016
};
9531017

9541018
const workspaceEdit: WorkspaceEdit = {
@@ -1977,7 +2041,10 @@ function createAttrValueNoDuplicationFix(
19772041
`[DEBUG] createAttrValueNoDuplicationFix called with diagnostic: ${JSON.stringify(diagnostic)}`,
19782042
);
19792043

1980-
if (!diagnostic.data || diagnostic.data.ruleId !== "attr-value-no-duplication") {
2044+
if (
2045+
!diagnostic.data ||
2046+
diagnostic.data.ruleId !== "attr-value-no-duplication"
2047+
) {
19812048
trace(
19822049
`[DEBUG] createAttrValueNoDuplicationFix: Invalid diagnostic data or ruleId`,
19832050
);
@@ -1990,7 +2057,9 @@ function createAttrValueNoDuplicationFix(
19902057
// Use robust tag boundary detection
19912058
const tagBoundaries = findTagBoundaries(text, diagnosticOffset);
19922059
if (!tagBoundaries) {
1993-
trace(`[DEBUG] createAttrValueNoDuplicationFix: Could not find tag boundaries`);
2060+
trace(
2061+
`[DEBUG] createAttrValueNoDuplicationFix: Could not find tag boundaries`,
2062+
);
19942063
return null;
19952064
}
19962065

@@ -2017,8 +2086,9 @@ function createAttrValueNoDuplicationFix(
20172086

20182087
if (values.length !== uniqueValues.length) {
20192088
// Found duplicates, create an edit to fix them
2020-
const newAttrValue = uniqueValues.join(' ');
2021-
const quote = match[2] !== undefined ? '"' : match[3] !== undefined ? "'" : '';
2089+
const newAttrValue = uniqueValues.join(" ");
2090+
const quote =
2091+
match[2] !== undefined ? '"' : match[3] !== undefined ? "'" : "";
20222092
const newFullMatch = quote
20232093
? `${attrName}=${quote}${newAttrValue}${quote}`
20242094
: `${attrName}=${newAttrValue}`;

htmlhint/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to the "vscode-htmlhint" extension will be documented in this file.
44

5+
### v1.15.0 (2025-11-27)
6+
7+
- Smarter autofix for rules which accommodates for `tag-self-close` rule.
8+
59
### v1.14.0 (2025-11-26)
610

711
- Add autofix for the `attr-no-duplication` rule

htmlhint/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

htmlhint/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "HTMLHint",
44
"description": "VS Code integration for HTMLHint - A Static Code Analysis Tool for HTML",
55
"icon": "images/icon.png",
6-
"version": "1.14.0",
6+
"version": "1.15.0",
77
"publisher": "HTMLHint",
88
"galleryBanner": {
99
"color": "#333333",

package-lock.json

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "1.0.0",
44
"private": true,
55
"description": "VS Code extension to support HTMLHint, an HTML linter.",
6-
"homepage": "https://github.com/htmlhint/vscode-htmlhint#readme",
6+
"homepage": "https://htmlhint.com",
77
"bugs": {
88
"url": "https://github.com/htmlhint/vscode-htmlhint/issues"
99
},
@@ -43,7 +43,7 @@
4343
"glob": "^13.0.0",
4444
"globals": "^16.5.0",
4545
"mocha": "^11.7.5",
46-
"prettier": "3.6.2",
46+
"prettier": "3.7.1",
4747
"ts-node": "^10.9.2",
4848
"typescript": "5.5.4"
4949
},

0 commit comments

Comments
 (0)