From a252cadffd0a497b3e9f5e3993570cae9efe5334 Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Fri, 28 Nov 2025 16:56:02 +0900 Subject: [PATCH 1/3] Add autofix for the `link-rel-canonical-require` rule --- htmlhint-server/src/server.ts | 94 +++++++++++++++++++++++++++ htmlhint/CHANGELOG.md | 1 + htmlhint/README.md | 1 + test/autofix/.htmlhintrc | 3 +- test/autofix/link-canonical-test.html | 12 ++++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 test/autofix/link-canonical-test.html diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts index 2646103..190aa42 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( + /]*>/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..a65db10 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.15.0 (2025-11-27) - 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., `
` → `
`) - **`form-method-require`** - Adds empty method attribute to forms - **`html-lang-require`** - Adds `lang` attribute to `` tag +- **`link-rel-canonical-require`** - Adds canonical link tag - **`meta-charset-require`** - Adds charset meta tag - **`meta-description-require`** - Adds description meta tag - **`meta-viewport-require`** - Adds viewport meta tag diff --git a/test/autofix/.htmlhintrc b/test/autofix/.htmlhintrc index f088227..1c532fd 100644 --- a/test/autofix/.htmlhintrc +++ b/test/autofix/.htmlhintrc @@ -9,9 +9,10 @@ "button-type-require": true, "doctype-first": true, "doctype-html5": true, - "empty-tag-not-self-closed": true, + "empty-tag-not-self-closed": false, "html-lang-require": true, "id-unique": true, + "link-rel-canonical-require": true, "meta-charset-require": true, "meta-description-require": true, "meta-viewport-require": true, diff --git a/test/autofix/link-canonical-test.html b/test/autofix/link-canonical-test.html new file mode 100644 index 0000000..a9c00e7 --- /dev/null +++ b/test/autofix/link-canonical-test.html @@ -0,0 +1,12 @@ + + + + + + + Link Canonical Test + + +

Link Canonical Test

+ + From fcbbaba6b54904f52e016fc10dd570b2b8576577 Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Fri, 28 Nov 2025 17:04:53 +0900 Subject: [PATCH 2/3] Broaden canonical link detection regex Updated the regular expression to match tags with the canonical rel attribute regardless of attribute order, improving robustness in canonical link detection. --- htmlhint-server/src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htmlhint-server/src/server.ts b/htmlhint-server/src/server.ts index 190aa42..26158d9 100644 --- a/htmlhint-server/src/server.ts +++ b/htmlhint-server/src/server.ts @@ -1052,7 +1052,7 @@ function createLinkRelCanonicalRequireFix( const headContent = headMatch[2]; const canonicalMatch = headContent.match( - /]*>/i, + /]*rel\s*=\s*["']canonical["'][^>]*>/i, ); if (canonicalMatch) { From 8b5b7c611d8f85bb5f05f946516e108e1c215708 Mon Sep 17 00:00:00 2001 From: Christian Oliff Date: Fri, 28 Nov 2025 17:06:43 +0900 Subject: [PATCH 3/3] Update CHANGELOG.md --- htmlhint/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/htmlhint/CHANGELOG.md b/htmlhint/CHANGELOG.md index a65db10..9240291 100644 --- a/htmlhint/CHANGELOG.md +++ b/htmlhint/CHANGELOG.md @@ -2,7 +2,7 @@ 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