Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
100 changes: 100 additions & 0 deletions htmlhint-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1965,6 +1965,102 @@ function createAttrNoDuplicationFix(
};
}

/**
* Create auto-fix action for attr-value-no-duplication rule
* Removes duplicate values within attribute values (e.g., class="btn btn primary" -> class="btn primary")
*/
function createAttrValueNoDuplicationFix(
document: TextDocument,
diagnostic: Diagnostic,
): CodeAction | null {
trace(
`[DEBUG] createAttrValueNoDuplicationFix called with diagnostic: ${JSON.stringify(diagnostic)}`,
);

if (!diagnostic.data || diagnostic.data.ruleId !== "attr-value-no-duplication") {
trace(
`[DEBUG] createAttrValueNoDuplicationFix: Invalid diagnostic data or ruleId`,
);
return null;
}

const text = document.getText();
const diagnosticOffset = document.offsetAt(diagnostic.range.start);

// Use robust tag boundary detection
const tagBoundaries = findTagBoundaries(text, diagnosticOffset);
if (!tagBoundaries) {
trace(`[DEBUG] createAttrValueNoDuplicationFix: Could not find tag boundaries`);
return null;
}

const { tagStart, tagEnd } = tagBoundaries;
const tagContent = text.substring(tagStart, tagEnd + 1);
trace(`[DEBUG] createAttrValueNoDuplicationFix: Found tag: ${tagContent}`);

// Parse attributes from the tag to find the one with duplicate values
const attrPattern = /(\w+(?:-\w+)*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g;
let match;
const edits: TextEdit[] = [];

while ((match = attrPattern.exec(tagContent)) !== null) {
const attrName = match[1];
const attrValue = match[2] || match[3] || match[4] || "";
const fullMatch = match[0];
const attrStartIndex = match.index;
const attrEndIndex = match.index + fullMatch.length;

// Check if this attribute contains duplicate values
if (attrValue.trim()) {
const values = attrValue.trim().split(/\s+/);
const uniqueValues = [...new Set(values)]; // Remove duplicates while preserving order

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 newFullMatch = quote
? `${attrName}=${quote}${newAttrValue}${quote}`
: `${attrName}=${newAttrValue}`;

const absoluteStart = tagStart + attrStartIndex;
const absoluteEnd = tagStart + attrEndIndex;

edits.push({
range: {
start: document.positionAt(absoluteStart),
end: document.positionAt(absoluteEnd),
},
newText: newFullMatch,
});

trace(
`[DEBUG] createAttrValueNoDuplicationFix: Will replace "${fullMatch}" with "${newFullMatch}"`,
);
break; // Only fix one attribute per diagnostic
}
}
}

if (edits.length === 0) {
trace(`[DEBUG] createAttrValueNoDuplicationFix: No edits created`);
return null;
}

const workspaceEdit: WorkspaceEdit = {
changes: {
[document.uri]: edits,
},
};

return {
title: "Remove duplicate values from attribute",
kind: CodeActionKind.QuickFix,
edit: workspaceEdit,
isPreferred: true,
};
}

/**
* Create auto-fix action for form-method-require rule
*/
Expand Down Expand Up @@ -2162,6 +2258,10 @@ async function createAutoFixes(
trace(`[DEBUG] Calling createAttrNoDuplicationFix`);
fix = createAttrNoDuplicationFix(document, diagnostic);
break;
case "attr-value-no-duplication":
trace(`[DEBUG] Calling createAttrValueNoDuplicationFix`);
fix = createAttrValueNoDuplicationFix(document, diagnostic);
break;
case "form-method-require":
trace(`[DEBUG] Calling createFormMethodRequireFix`);
fix = createFormMethodRequireFix(document, diagnostic);
Expand Down
1 change: 1 addition & 0 deletions htmlhint/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to the "vscode-htmlhint" extension will be documented in thi
### v1.14.0 (2025-11-26)

- Add autofix for the `attr-no-duplication` rule
- Add autofix for the `attr-value-no-duplication` rule
- Add autofix for the `form-method-require` rule

### v1.13.0 (2025-11-25)
Expand Down
1 change: 1 addition & 0 deletions htmlhint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ The extension provides automatic fixes for many common HTML issues. Currently su
- **`attr-lowercase`** - Converts uppercase attribute names to lowercase
- **`attr-no-duplication`** - Removes duplicate attributes (only when values are identical)
- **`attr-no-unnecessary-whitespace`** - Removes unnecessary whitespace around attributes
- **`attr-value-no-duplication`** - Removes duplicate values within attributes (e.g., `class="btn btn primary"` → `class="btn primary"`)
- **`attr-value-double-quotes`** - Converts single quotes to double quotes in attributes
- **`attr-whitespace`** - Removes leading and trailing whitespace from attribute values
- **`button-type-require`** - Adds type attribute to buttons
Expand Down
1 change: 1 addition & 0 deletions test/autofix/.htmlhintrc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"alt-require": true,
"attr-lowercase": true,
"attr-no-duplication": true,
"attr-no-unnecessary-whitespace": true,
"attr-value-double-quotes": true,
"attr-value-no-duplication": true,
Expand Down
4 changes: 3 additions & 1 deletion test/autofix/alt-test.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Alt Attribute Test</title>
</head>
<body>
Expand Down
33 changes: 33 additions & 0 deletions test/autofix/attr-no-duplication-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Attribute No Duplication Test</title>
</head>
<body>
<!-- Test case 1: Duplicate attributes with same value (should be fixed) -->
<div class="btn" class="btn">Button 1</div>

<!-- Test case 2: Duplicate attributes with different values (should NOT be fixed) -->
<div id="test1" id="test2">Button 2</div>

<!-- Test case 3: Multiple duplicate attributes with same values (should be fixed) -->
<div class="primary" data-test="value" class="primary" data-test="value">Button 3</div>

<!-- Test case 4: Mixed case attribute names with same values (should be fixed) -->
<div CLASS="btn" class="btn">Button 4</div>

<!-- Test case 5: Duplicate attributes with empty values (should be fixed) -->
<div data-empty="" data-empty="">Button 5</div>

<!-- Test case 6: No duplicates - should not trigger -->
<div class="btn" id="unique">Button 6</div>

<!-- Test case 7: Self-closing tag with duplicates -->
<input type="text" name="test" type="text" />

<!-- Test case 8: Multiple spaces around duplicate attributes -->
<div class="btn" class="btn" >Button 8</div>
</body>
</html>
33 changes: 33 additions & 0 deletions test/autofix/attr-value-no-duplication-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Attribute Value No Duplication Test</title>
</head>
<body>
<!-- Test case 1: Duplicate values in class attribute (should be fixed) -->
<div class="btn btn primary">Button 1</div>

<!-- Test case 2: Multiple duplicates in class attribute (should be fixed) -->
<div class="btn primary btn secondary primary">Button 2</div>

<!-- Test case 3: No duplicates - should not trigger -->
<div class="btn primary secondary">Button 3</div>

<!-- Test case 4: Single class value - should not trigger -->
<div class="btn">Button 4</div>

<!-- Test case 5: Empty class - should not trigger -->
<div class="">Button 5</div>

<!-- Test case 6: Duplicate values with extra whitespace -->
<div class=" btn btn primary ">Button 6</div>

<!-- Test case 7: Other attributes that might have duplicate values -->
<div data-tags="tag1 tag2 tag1 tag3">Button 7</div>

<!-- Test case 8: Mixed case duplicates (case-sensitive) -->
<div class="btn Btn BTN">Button 8</div>
</body>
</html>
4 changes: 2 additions & 2 deletions test/autofix/button-test.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Button Type Test</title>
</head>
<body>
Expand Down
4 changes: 2 additions & 2 deletions test/autofix/meta-description-test.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<html lang="en">
<head>
<!-- Test case 1: Basic head with charset - description should be added after charset -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Meta Description Test</title>
</head>
<body>
Expand Down