diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts index 02d34f6..345e24c 100644 --- a/htmlhint-server/src/server.ts +++ b/htmlhint-server/src/server.ts @@ -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 */ @@ -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); diff --git a/htmlhint/CHANGELOG.md b/htmlhint/CHANGELOG.md index 1268611..22c7a77 100644 --- a/htmlhint/CHANGELOG.md +++ b/htmlhint/CHANGELOG.md @@ -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) diff --git a/htmlhint/README.md b/htmlhint/README.md index d9b5c03..6975576 100644 --- a/htmlhint/README.md +++ b/htmlhint/README.md @@ -31,6 +31,7 @@ The extension provides automatic fixes for many common HTML issues. Currently su - **`attr-no-duplication`** - Removes duplicate attributes (only when values are identical) - **`attr-no-unnecessary-whitespace`** - Removes unnecessary whitespace around attributes - **`attr-value-double-quotes`** - Converts single quotes to double quotes in attributes +- **`attr-value-no-duplication`** - Removes duplicate values within attributes (e.g., `class="btn btn primary"` → `class="btn primary"`) - **`attr-whitespace`** - Removes leading and trailing whitespace from attribute values - **`button-type-require`** - Adds type attribute to buttons - **`doctype-first`** - Adds DOCTYPE declaration at the beginning diff --git a/test/autofix/.htmlhintrc b/test/autofix/.htmlhintrc index 5e3a8e2..e2b0c0e 100644 --- a/test/autofix/.htmlhintrc +++ b/test/autofix/.htmlhintrc @@ -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, diff --git a/test/autofix/alt-test.html b/test/autofix/alt-test.html index 8dd4787..987b34c 100644 --- a/test/autofix/alt-test.html +++ b/test/autofix/alt-test.html @@ -1,6 +1,8 @@ - +
+ +