forked from ember-cli/eslint-plugin-ember
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtemplate-no-invalid-link-href.js
More file actions
98 lines (88 loc) · 3.54 KB
/
template-no-invalid-link-href.js
File metadata and controls
98 lines (88 loc) · 3.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
'use strict';
const { getStaticAttrValue } = require('../utils/static-attr-value');
// Matches the `javascript:` protocol when it is the actual URL scheme.
// HTML's URL parser trims leading/trailing ASCII whitespace from hrefs before
// parsing (WHATWG URL §3.1 "basic URL parser"), so `" javascript:void(0)"`
// resolves to the javascript: scheme. Any other leading characters (`./`,
// `#`, etc.) mean the value is a relative URL or fragment, not a javascript:
// URL, and must not match.
const JS_PROTOCOL_REGEX = /^\s*javascript:/iu;
// C0 control characters (U+0000–U+001F). The WHATWG URL parser strips these
// before scheme detection, so `\x00javascript:...` resolves to the javascript:
// scheme. String.prototype.trim() does NOT remove them, so we strip them
// explicitly before the javascript: check.
const C0_CONTROLS_RE = /^[ -]+/;
function isInvalidHrefValue(value) {
if (value === undefined || value === null) {
return false;
}
const trimmed = value.trim();
if (trimmed === '') {
return true;
}
// Anchor pointing to nothing navigable — "#" or "#!" are common "no-op"
// placeholders that break screen-reader navigation semantics.
if (trimmed === '#' || trimmed === '#!') {
return true;
}
// Strip leading C0 control characters before the javascript: check — the
// WHATWG URL parser removes them before scheme detection, so
// `\x00javascript:` still resolves to javascript: even though trim() leaves
// those bytes in place.
const stripped = trimmed.replace(C0_CONTROLS_RE, '');
if (JS_PROTOCOL_REGEX.test(stripped)) {
return true;
}
return false;
}
function getStaticHrefValue(attr) {
// Uses the shared `getStaticAttrValue` helper so mustache-literal hrefs
// (`{{"#"}}`, `{{"javascript:void(0)"}}`) and single-part concat
// equivalents are validated the same as their text-node counterparts.
// Returns `undefined` for dynamic values (PathExpressions, concat with a
// dynamic part) so the rule can skip them.
return getStaticAttrValue(attr?.value);
}
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallow invalid href values on anchor elements',
category: 'Accessibility',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-invalid-link-href.md',
templateMode: 'both',
},
fixable: null,
schema: [],
messages: {
invalidHref:
'The href attribute requires a valid, navigable URL. Values like "#", "javascript:...", or an empty string break link semantics — use a native non-link element (e.g. <button> for <a>) when the element does not navigate.',
},
},
create(context) {
return {
GlimmerElementNode(node) {
// Both <a href> and <area href> carry URL semantics per HTML §4.5.1
// / §4.8.14. Same validity rules apply — empty/`#`/`javascript:`
// href values are equally invalid on either.
if (node.tag !== 'a' && node.tag !== 'area') {
return;
}
const hrefAttr = node.attributes?.find((a) => a.name === 'href');
if (!hrefAttr) {
// Missing href — handled by template-link-href-attributes, not this rule.
return;
}
const value = getStaticHrefValue(hrefAttr);
if (value === undefined) {
// Dynamic value — can't validate statically.
return;
}
if (isInvalidHrefValue(value)) {
context.report({ node: hrefAttr, messageId: 'invalidHref' });
}
},
};
},
};