From 32379698bfc352781f63823cfb43f2d0b08ad02a Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Fri, 28 Nov 2025 16:35:36 +0900 Subject: [PATCH 1/2] Add autofix for empty-tag-not-self-closed rule Introduces an auto-fix action for the 'empty-tag-not-self-closed' rule, converting void HTML elements to self-closing format. Updates documentation and changelog, enables the rule in test config, and adds comprehensive test cases for void elements. --- htmlhint-server/src/server.ts | 140 ++++++++++++++++++ htmlhint/CHANGELOG.md | 3 +- htmlhint/README.md | 1 + test/autofix/.htmlhintrc | 1 + .../empty-tag-not-self-closed-test.html | 62 ++++++++ 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 test/autofix/empty-tag-not-self-closed-test.html diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts index f904fe3..2646103 100644 --- a/htmlhint-server/src/server.ts +++ b/htmlhint-server/src/server.ts @@ -2230,6 +2230,142 @@ function createFormMethodRequireFix( }; } +/** + * Create auto-fix action for empty-tag-not-self-closed rule + * + * This fixes void HTML elements by converting them to proper self-closing format. + * Void elements should be self-closing and never have closing tags. + * Only applies to void HTML elements (br, img, hr, input, etc.). + * + * Example transformations: + * -

+ * - + * -

+ */ +function createEmptyTagNotSelfClosedFix( + document: TextDocument, + diagnostic: Diagnostic, +): CodeAction | null { + trace( + `[DEBUG] createEmptyTagNotSelfClosedFix called with diagnostic: ${JSON.stringify(diagnostic)}`, + ); + + if ( + !diagnostic.data || + diagnostic.data.ruleId !== "empty-tag-not-self-closed" + ) { + trace( + `[DEBUG] createEmptyTagNotSelfClosedFix: Invalid diagnostic data or ruleId: ${JSON.stringify(diagnostic.data)}`, + ); + return null; + } + + const text = document.getText(); + const diagnosticOffset = document.offsetAt(diagnostic.range.start); + + // List of void elements that should be self-closing + const voidElements = [ + "area", + "base", + "br", + "col", + "embed", + "hr", + "img", + "input", + "link", + "meta", + "source", + "track", + "wbr", + ]; + + // Find the tag boundaries around the diagnostic position + const tagBoundaries = findTagBoundaries(text, diagnosticOffset); + if (!tagBoundaries) { + trace( + `[DEBUG] createEmptyTagNotSelfClosedFix: Could not find tag boundaries`, + ); + return null; + } + + const { tagStart, tagEnd } = tagBoundaries; + + // Extract the content around the diagnostic position to find void elements + const searchStart = Math.max(0, tagStart - 50); + const searchEnd = Math.min(text.length, tagEnd + 50); + const searchText = text.substring(searchStart, searchEnd); + + // Pattern to match void elements that are not self-closing: or + const voidElementPattern = /<(\w+)(\s[^>]*?)?\s*>/gi; + let match; + + while ((match = voidElementPattern.exec(searchText)) !== null) { + const matchStart = searchStart + match.index; + const matchEnd = matchStart + match[0].length; + const tagName = match[1].toLowerCase(); + const attributes = match[2] || ""; + const fullMatch = match[0]; + + // Skip if already self-closing + if (fullMatch.endsWith("/>")) { + continue; + } + + // Check if this match contains our diagnostic position + if (matchStart <= diagnosticOffset && diagnosticOffset <= matchEnd) { + // Verify this is a void element + if (!voidElements.includes(tagName)) { + trace( + `[DEBUG] createEmptyTagNotSelfClosedFix: ${tagName} is not a void element`, + ); + return null; + } + + trace( + `[DEBUG] createEmptyTagNotSelfClosedFix: Found non-self-closing ${tagName} tag at position ${matchStart}-${matchEnd}`, + ); + + // Create the self-closing replacement + const selfClosingTag = attributes.trim() + ? `<${tagName}${attributes} />` + : `<${tagName} />`; + + const edit: TextEdit = { + range: { + start: document.positionAt(matchStart), + end: document.positionAt(matchEnd), + }, + newText: selfClosingTag, + }; + + trace( + `[DEBUG] createEmptyTagNotSelfClosedFix: Will replace "${fullMatch}" with "${selfClosingTag}"`, + ); + + const action = CodeAction.create( + `Convert ${tagName} tag to self-closing`, + { + changes: { + [document.uri]: [edit], + }, + }, + CodeActionKind.QuickFix, + ); + + action.diagnostics = [diagnostic]; + action.isPreferred = true; + + return action; + } + } + + trace( + `[DEBUG] createEmptyTagNotSelfClosedFix: No matching empty tag pair found`, + ); + return null; +} + /** * Create auto-fix actions for supported rules */ @@ -2336,6 +2472,10 @@ async function createAutoFixes( trace(`[DEBUG] Calling createFormMethodRequireFix`); fix = createFormMethodRequireFix(document, diagnostic); break; + case "empty-tag-not-self-closed": + trace(`[DEBUG] Calling createEmptyTagNotSelfClosedFix`); + fix = createEmptyTagNotSelfClosedFix(document, diagnostic); + break; default: trace(`[DEBUG] No autofix function found for rule: ${ruleId}`); break; diff --git a/htmlhint/CHANGELOG.md b/htmlhint/CHANGELOG.md index fe81b23..23cb610 100644 --- a/htmlhint/CHANGELOG.md +++ b/htmlhint/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to the "vscode-htmlhint" extension will be documented in thi ### v1.15.0 (2025-11-27) -- Smarter autofix for rules which accommodates for `tag-self-close` rule. +- Add autofix for the `empty-tag-not-self-closed` rule +- Smarter autofix for rules which accommodates for `tag-self-close` rule ### v1.14.0 (2025-11-26) diff --git a/htmlhint/README.md b/htmlhint/README.md index 6975576..aec2be9 100644 --- a/htmlhint/README.md +++ b/htmlhint/README.md @@ -36,6 +36,7 @@ The extension provides automatic fixes for many common HTML issues. Currently su - **`button-type-require`** - Adds type attribute to buttons - **`doctype-first`** - Adds DOCTYPE declaration at the beginning - **`doctype-html5`** - Updates DOCTYPE to HTML5 +- **`empty-tag-not-self-closed`** - Converts void elements to self-closing format (e.g., `
` → `
`) - **`form-method-require`** - Adds empty method attribute to forms - **`html-lang-require`** - Adds `lang` attribute to `` tag - **`meta-charset-require`** - Adds charset meta tag diff --git a/test/autofix/.htmlhintrc b/test/autofix/.htmlhintrc index e2b0c0e..f088227 100644 --- a/test/autofix/.htmlhintrc +++ b/test/autofix/.htmlhintrc @@ -9,6 +9,7 @@ "button-type-require": true, "doctype-first": true, "doctype-html5": true, + "empty-tag-not-self-closed": true, "html-lang-require": true, "id-unique": true, "meta-charset-require": true, diff --git a/test/autofix/empty-tag-not-self-closed-test.html b/test/autofix/empty-tag-not-self-closed-test.html new file mode 100644 index 0000000..63263ba --- /dev/null +++ b/test/autofix/empty-tag-not-self-closed-test.html @@ -0,0 +1,62 @@ + + + + + + + Empty Tag Not Self-Closed Test + + + +
+ + + Test + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Test 2 +
+ + +
Content
+

Paragraph content

+ + +
+

+ + From a1e0e7d1e427cc22e7c772b20f10886a6739d18c Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Fri, 28 Nov 2025 16:40:36 +0900 Subject: [PATCH 2/2] Add test cases for void elements with trailing whitespace Expanded the test suite to include void HTML elements (hr, input, br, img) with trailing whitespace to verify they are converted to self-closing tags. Also updated test case numbering for clarity. --- test/autofix/empty-tag-not-self-closed-test.html | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/autofix/empty-tag-not-self-closed-test.html b/test/autofix/empty-tag-not-self-closed-test.html index 63263ba..3b3aec0 100644 --- a/test/autofix/empty-tag-not-self-closed-test.html +++ b/test/autofix/empty-tag-not-self-closed-test.html @@ -46,16 +46,22 @@ - + +
+ +
+ Test 3 + +
Test 2
- +
Content

Paragraph content

- +