diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts index 2646103..26158d9 100644 --- a/htmlhint-server/src/server.ts +++ b/htmlhint-server/src/server.ts @@ -1029,6 +1029,96 @@ function createMetaDescriptionRequireFix( }; } +/** + * Create auto-fix action for link-rel-canonical-require rule + */ +function createLinkRelCanonicalRequireFix( + document: TextDocument, + diagnostic: Diagnostic, +): CodeAction | null { + if ( + !diagnostic.data || + diagnostic.data.ruleId !== "link-rel-canonical-require" + ) { + return null; + } + + const text = document.getText(); + const headMatch = text.match(/
]*)?>([\s\S]*?)<\/head>/i); + + if (!headMatch) { + return null; + } + + const headContent = headMatch[2]; + const canonicalMatch = headContent.match( + /]*rel\s*=\s*["']canonical["'][^>]*>/i, + ); + + if (canonicalMatch) { + return null; // Canonical link tag already exists + } + + // Find a good position to insert canonical link tag (after meta tags if they exist, otherwise at the beginning of head) + const headStart = headMatch.index! + headMatch[0].indexOf(">") + 1; + const metaCharsetMatch = headContent.match( + /]*>/i, + ); + const metaViewportMatch = headContent.match( + /]*>/i, + ); + const metaDescriptionMatch = headContent.match( + /]*>/i, + ); + + let insertPosition: number; + const shouldSelfClose = isRuleEnabledForDocument(document, "tag-self-close"); + const canonicalSnippet = + '\n " : ">"); + + if (metaDescriptionMatch) { + // Insert after description meta tag + const metaDescriptionEnd = + headStart + metaDescriptionMatch.index! + metaDescriptionMatch[0].length; + insertPosition = metaDescriptionEnd; + } else if (metaViewportMatch) { + // Insert after viewport meta tag + const metaViewportEnd = + headStart + metaViewportMatch.index! + metaViewportMatch[0].length; + insertPosition = metaViewportEnd; + } else if (metaCharsetMatch) { + // Insert after charset meta tag + const metaCharsetEnd = + headStart + metaCharsetMatch.index! + metaCharsetMatch[0].length; + insertPosition = metaCharsetEnd; + } else { + // Insert at the beginning of head + insertPosition = headStart; + } + + const edit: TextEdit = { + range: { + start: document.positionAt(insertPosition), + end: document.positionAt(insertPosition), + }, + newText: canonicalSnippet, + }; + + const workspaceEdit: WorkspaceEdit = { + changes: { + [document.uri]: [edit], + }, + }; + + return { + title: 'Add tag', + kind: CodeActionKind.QuickFix, + edit: workspaceEdit, + isPreferred: true, + }; +} + /** * Create auto-fix action for alt-require rule */ @@ -2432,6 +2522,10 @@ async function createAutoFixes( trace(`[DEBUG] Calling createMetaDescriptionRequireFix`); fix = await createMetaDescriptionRequireFix(document, diagnostic); break; + case "link-rel-canonical-require": + trace(`[DEBUG] Calling createLinkRelCanonicalRequireFix`); + fix = await createLinkRelCanonicalRequireFix(document, diagnostic); + break; case "alt-require": trace(`[DEBUG] Calling createAltRequireFix`); fix = createAltRequireFix(document, diagnostic); diff --git a/htmlhint/CHANGELOG.md b/htmlhint/CHANGELOG.md index 23cb610..9240291 100644 --- a/htmlhint/CHANGELOG.md +++ b/htmlhint/CHANGELOG.md @@ -2,9 +2,10 @@ All notable changes to the "vscode-htmlhint" extension will be documented in this file. -### v1.15.0 (2025-11-27) +### v1.15.0 (TBD) - Add autofix for the `empty-tag-not-self-closed` rule +- Add autofix for the `link-rel-canonical-require` 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 aec2be9..327ddb0 100644 --- a/htmlhint/README.md +++ b/htmlhint/README.md @@ -39,6 +39,7 @@ The extension provides automatic fixes for many common HTML issues. Currently su - **`empty-tag-not-self-closed`** - Converts void elements to self-closing format (e.g., `