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
102 changes: 86 additions & 16 deletions htmlhint-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,67 @@ function loadConfigurationFile(configFile: string): HtmlHintConfig | null {
return ruleset;
}

function isHtmlHintRuleEnabled(value: unknown): boolean {
if (value === undefined || value === null) {
return false;
}

if (Array.isArray(value)) {
if (value.length === 0) {
return false;
}
return isHtmlHintRuleEnabled(value[0]);
}

if (typeof value === "boolean") {
return value;
}

if (typeof value === "number") {
return value !== 0;
}

if (typeof value === "string") {
const normalized = value.trim().toLowerCase();
return normalized !== "false" && normalized !== "0" && normalized !== "";
}

if (typeof value === "object") {
return true;
}

return false;
}

function isRuleEnabledForDocument(
document: TextDocument,
ruleId: string,
): boolean {
try {
const parsedUri = URI.parse(document.uri);

if (parsedUri.scheme !== "file") {
trace(
`[DEBUG] isRuleEnabledForDocument: Non-file scheme for ${document.uri}, rule ${ruleId} treated as disabled`,
);
return false;
}

const config = getConfiguration(parsedUri.fsPath);
const ruleValue = (config as Record<string, unknown>)[ruleId];
const enabled = isHtmlHintRuleEnabled(ruleValue);
trace(
`[DEBUG] isRuleEnabledForDocument: Rule ${ruleId} enabled=${enabled} for ${document.uri}`,
);
return enabled;
} catch (error) {
trace(
`[DEBUG] isRuleEnabledForDocument: Failed to determine rule ${ruleId} for ${document.uri}: ${error}`,
);
return false;
}
}

function isErrorWithMessage(err: unknown): err is { message: string } {
return (
typeof err === "object" &&
Expand Down Expand Up @@ -789,7 +850,9 @@ function createMetaCharsetRequireFix(
// Insert charset meta tag at the beginning of head (right after <head>)
const headStart = headMatch.index! + headMatch[0].indexOf(">") + 1;
const insertPosition = headStart;
const newText = '\n <meta charset="UTF-8">';
const shouldSelfClose = isRuleEnabledForDocument(document, "tag-self-close");
const newText =
'\n <meta charset="UTF-8"' + (shouldSelfClose ? " />" : ">");

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

const shouldSelfClose = isRuleEnabledForDocument(document, "tag-self-close");
trace(
`[DEBUG] createMetaViewportRequireFix: tag-self-close enabled=${shouldSelfClose}`,
);
const viewportSnippet = `\n <meta name="viewport" content="width=device-width, initial-scale=1.0"${shouldSelfClose ? " />" : ">"}`;

let insertPosition: number;
let newText: string;

if (metaCharsetMatch) {
const metaCharsetEnd =
headStart + metaCharsetMatch.index! + metaCharsetMatch[0].length;
insertPosition = metaCharsetEnd;
newText =
'\n <meta name="viewport" content="width=device-width, initial-scale=1.0">';
} else {
insertPosition = headStart;
newText =
'\n <meta name="viewport" content="width=device-width, initial-scale=1.0">';
}

const edit: TextEdit = {
range: {
start: document.positionAt(insertPosition),
end: document.positionAt(insertPosition),
},
newText: newText,
newText: viewportSnippet,
};

const workspaceEdit: WorkspaceEdit = {
Expand Down Expand Up @@ -923,32 +987,32 @@ function createMetaDescriptionRequireFix(
);

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

if (metaViewportMatch) {
// Insert after viewport meta tag
const metaViewportEnd =
headStart + metaViewportMatch.index! + metaViewportMatch[0].length;
insertPosition = metaViewportEnd;
newText = '\n <meta name="description" content="">';
} else if (metaCharsetMatch) {
// Insert after charset meta tag
const metaCharsetEnd =
headStart + metaCharsetMatch.index! + metaCharsetMatch[0].length;
insertPosition = metaCharsetEnd;
newText = '\n <meta name="description" content="">';
} else {
// Insert at the beginning of head
insertPosition = headStart;
newText = '\n <meta name="description" content="">';
}

const edit: TextEdit = {
range: {
start: document.positionAt(insertPosition),
end: document.positionAt(insertPosition),
},
newText: newText,
newText: descriptionSnippet,
};

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

if (!diagnostic.data || diagnostic.data.ruleId !== "attr-value-no-duplication") {
if (
!diagnostic.data ||
diagnostic.data.ruleId !== "attr-value-no-duplication"
) {
trace(
`[DEBUG] createAttrValueNoDuplicationFix: Invalid diagnostic data or ruleId`,
);
Expand All @@ -1990,7 +2057,9 @@ function createAttrValueNoDuplicationFix(
// Use robust tag boundary detection
const tagBoundaries = findTagBoundaries(text, diagnosticOffset);
if (!tagBoundaries) {
trace(`[DEBUG] createAttrValueNoDuplicationFix: Could not find tag boundaries`);
trace(
`[DEBUG] createAttrValueNoDuplicationFix: Could not find tag boundaries`,
);
return null;
}

Expand All @@ -2017,8 +2086,9 @@ function createAttrValueNoDuplicationFix(

if (values.length !== uniqueValues.length) {
// Found duplicates, create an edit to fix them
const newAttrValue = uniqueValues.join(' ');
const quote = match[2] !== undefined ? '"' : match[3] !== undefined ? "'" : '';
const newAttrValue = uniqueValues.join(" ");
const quote =
match[2] !== undefined ? '"' : match[3] !== undefined ? "'" : "";
const newFullMatch = quote
? `${attrName}=${quote}${newAttrValue}${quote}`
: `${attrName}=${newAttrValue}`;
Expand Down
4 changes: 4 additions & 0 deletions htmlhint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

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

### v1.15.0 (2025-11-27)

- Smarter autofix for rules which accommodates for `tag-self-close` rule.

### v1.14.0 (2025-11-26)

- Add autofix for the `attr-no-duplication` rule
Expand Down
4 changes: 2 additions & 2 deletions htmlhint/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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.14.0",
"version": "1.15.0",
"publisher": "HTMLHint",
"galleryBanner": {
"color": "#333333",
Expand Down
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"description": "VS Code extension to support HTMLHint, an HTML linter.",
"homepage": "https://github.com/htmlhint/vscode-htmlhint#readme",
"homepage": "https://htmlhint.com",
"bugs": {
"url": "https://github.com/htmlhint/vscode-htmlhint/issues"
},
Expand Down Expand Up @@ -43,7 +43,7 @@
"glob": "^13.0.0",
"globals": "^16.5.0",
"mocha": "^11.7.5",
"prettier": "3.6.2",
"prettier": "3.7.1",
"ts-node": "^10.9.2",
"typescript": "5.5.4"
},
Expand Down