Skip to content

Commit 6e3f612

Browse files
authored
Merge pull request #342 from htmlhint/dev/coliff/link-rel-canonical-require-autofix
Add autofix for the `link-rel-canonical-require` rule
2 parents d65e29c + 8b5b7c6 commit 6e3f612

File tree

5 files changed

+111
-2
lines changed

5 files changed

+111
-2
lines changed

htmlhint-server/src/server.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,96 @@ function createMetaDescriptionRequireFix(
10291029
};
10301030
}
10311031

1032+
/**
1033+
* Create auto-fix action for link-rel-canonical-require rule
1034+
*/
1035+
function createLinkRelCanonicalRequireFix(
1036+
document: TextDocument,
1037+
diagnostic: Diagnostic,
1038+
): CodeAction | null {
1039+
if (
1040+
!diagnostic.data ||
1041+
diagnostic.data.ruleId !== "link-rel-canonical-require"
1042+
) {
1043+
return null;
1044+
}
1045+
1046+
const text = document.getText();
1047+
const headMatch = text.match(/<head(\s[^>]*)?>([\s\S]*?)<\/head>/i);
1048+
1049+
if (!headMatch) {
1050+
return null;
1051+
}
1052+
1053+
const headContent = headMatch[2];
1054+
const canonicalMatch = headContent.match(
1055+
/<link\s+[^>]*rel\s*=\s*["']canonical["'][^>]*>/i,
1056+
);
1057+
1058+
if (canonicalMatch) {
1059+
return null; // Canonical link tag already exists
1060+
}
1061+
1062+
// Find a good position to insert canonical link tag (after meta tags if they exist, otherwise at the beginning of head)
1063+
const headStart = headMatch.index! + headMatch[0].indexOf(">") + 1;
1064+
const metaCharsetMatch = headContent.match(
1065+
/<meta\s+charset\s*=\s*["'][^"']*["'][^>]*>/i,
1066+
);
1067+
const metaViewportMatch = headContent.match(
1068+
/<meta\s+name\s*=\s*["']viewport["'][^>]*>/i,
1069+
);
1070+
const metaDescriptionMatch = headContent.match(
1071+
/<meta\s+name\s*=\s*["']description["'][^>]*>/i,
1072+
);
1073+
1074+
let insertPosition: number;
1075+
const shouldSelfClose = isRuleEnabledForDocument(document, "tag-self-close");
1076+
const canonicalSnippet =
1077+
'\n <link rel="canonical" href=""' +
1078+
(shouldSelfClose ? " />" : ">");
1079+
1080+
if (metaDescriptionMatch) {
1081+
// Insert after description meta tag
1082+
const metaDescriptionEnd =
1083+
headStart + metaDescriptionMatch.index! + metaDescriptionMatch[0].length;
1084+
insertPosition = metaDescriptionEnd;
1085+
} else if (metaViewportMatch) {
1086+
// Insert after viewport meta tag
1087+
const metaViewportEnd =
1088+
headStart + metaViewportMatch.index! + metaViewportMatch[0].length;
1089+
insertPosition = metaViewportEnd;
1090+
} else if (metaCharsetMatch) {
1091+
// Insert after charset meta tag
1092+
const metaCharsetEnd =
1093+
headStart + metaCharsetMatch.index! + metaCharsetMatch[0].length;
1094+
insertPosition = metaCharsetEnd;
1095+
} else {
1096+
// Insert at the beginning of head
1097+
insertPosition = headStart;
1098+
}
1099+
1100+
const edit: TextEdit = {
1101+
range: {
1102+
start: document.positionAt(insertPosition),
1103+
end: document.positionAt(insertPosition),
1104+
},
1105+
newText: canonicalSnippet,
1106+
};
1107+
1108+
const workspaceEdit: WorkspaceEdit = {
1109+
changes: {
1110+
[document.uri]: [edit],
1111+
},
1112+
};
1113+
1114+
return {
1115+
title: 'Add <link rel="canonical"> tag',
1116+
kind: CodeActionKind.QuickFix,
1117+
edit: workspaceEdit,
1118+
isPreferred: true,
1119+
};
1120+
}
1121+
10321122
/**
10331123
* Create auto-fix action for alt-require rule
10341124
*/
@@ -2432,6 +2522,10 @@ async function createAutoFixes(
24322522
trace(`[DEBUG] Calling createMetaDescriptionRequireFix`);
24332523
fix = await createMetaDescriptionRequireFix(document, diagnostic);
24342524
break;
2525+
case "link-rel-canonical-require":
2526+
trace(`[DEBUG] Calling createLinkRelCanonicalRequireFix`);
2527+
fix = await createLinkRelCanonicalRequireFix(document, diagnostic);
2528+
break;
24352529
case "alt-require":
24362530
trace(`[DEBUG] Calling createAltRequireFix`);
24372531
fix = createAltRequireFix(document, diagnostic);

htmlhint/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
All notable changes to the "vscode-htmlhint" extension will be documented in this file.
44

5-
### v1.15.0 (2025-11-27)
5+
### v1.15.0 (TBD)
66

77
- Add autofix for the `empty-tag-not-self-closed` rule
8+
- Add autofix for the `link-rel-canonical-require` rule
89
- Smarter autofix for rules which accommodates for `tag-self-close` rule
910

1011
### v1.14.0 (2025-11-26)

htmlhint/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ The extension provides automatic fixes for many common HTML issues. Currently su
3939
- **`empty-tag-not-self-closed`** - Converts void elements to self-closing format (e.g., `<br>``<br/>`)
4040
- **`form-method-require`** - Adds empty method attribute to forms
4141
- **`html-lang-require`** - Adds `lang` attribute to `<html>` tag
42+
- **`link-rel-canonical-require`** - Adds canonical link tag
4243
- **`meta-charset-require`** - Adds charset meta tag
4344
- **`meta-description-require`** - Adds description meta tag
4445
- **`meta-viewport-require`** - Adds viewport meta tag

test/autofix/.htmlhintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
"button-type-require": true,
1010
"doctype-first": true,
1111
"doctype-html5": true,
12-
"empty-tag-not-self-closed": true,
12+
"empty-tag-not-self-closed": false,
1313
"html-lang-require": true,
1414
"id-unique": true,
15+
"link-rel-canonical-require": true,
1516
"meta-charset-require": true,
1617
"meta-description-require": true,
1718
"meta-viewport-require": true,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="description" content="Link Canonical Test" />
7+
<title>Link Canonical Test</title>
8+
</head>
9+
<body>
10+
<h1>Link Canonical Test</h1>
11+
</body>
12+
</html>

0 commit comments

Comments
 (0)