@angular-eslint/eslint-plugin-template 15.2.2-alpha.9 → 16.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configs/README.md +10 -0
- package/dist/configs/accessibility.json +17 -0
- package/dist/configs/all.json +10 -10
- package/dist/configs/recommended.json +2 -1
- package/dist/index.js +95 -1
- package/dist/processors.js +210 -0
- package/dist/rules/alt-text.js +131 -0
- package/dist/rules/attributes-order.js +233 -0
- package/dist/rules/banana-in-box.js +47 -0
- package/dist/rules/button-has-type.js +76 -0
- package/dist/rules/click-events-have-key-events.js +57 -0
- package/dist/rules/conditional-complexity.js +107 -0
- package/dist/rules/cyclomatic-complexity.js +50 -0
- package/dist/rules/elements-content.js +70 -0
- package/dist/rules/eqeqeq.js +99 -0
- package/dist/rules/i18n.js +368 -0
- package/dist/rules/interactive-supports-focus.js +64 -0
- package/dist/rules/label-has-associated-control.js +103 -0
- package/dist/rules/mouse-events-have-key-events.js +40 -0
- package/dist/rules/no-any.js +59 -0
- package/dist/rules/no-autofocus.js +41 -0
- package/dist/rules/no-call-expression.js +68 -0
- package/dist/rules/no-distracting-elements.js +36 -0
- package/dist/rules/no-duplicate-attributes.js +97 -0
- package/dist/rules/no-inline-styles.js +104 -0
- package/dist/rules/no-interpolation-in-attributes.js +36 -0
- package/dist/rules/no-negated-async.js +60 -0
- package/dist/rules/no-positive-tabindex.js +43 -0
- package/dist/rules/role-has-required-aria.js +73 -0
- package/dist/rules/table-scope.js +41 -0
- package/dist/rules/use-track-by-function.js +56 -0
- package/dist/rules/valid-aria.js +126 -0
- package/dist/utils/attributes-comparator.js +19 -0
- package/dist/utils/constants.js +11 -0
- package/dist/utils/create-eslint-rule.js +26 -0
- package/dist/utils/get-attribute-value.js +39 -0
- package/dist/utils/get-dom-elements.js +9 -0
- package/dist/utils/get-nearest-node-from.js +17 -0
- package/dist/utils/get-original-attribute-name.js +29 -0
- package/dist/utils/is-child-node-of.js +12 -0
- package/dist/utils/is-content-editable.js +14 -0
- package/dist/utils/is-disabled-element.js +20 -0
- package/dist/utils/is-hidden-from-screen-reader.js +97 -0
- package/dist/utils/is-interactive-element/get-interactive-element-ax-object-schemas.js +27 -0
- package/dist/utils/is-interactive-element/get-interactive-element-role-schemas.js +38 -0
- package/dist/utils/is-interactive-element/get-non-interactive-element-role-schemas.js +44 -0
- package/dist/utils/is-interactive-element/index.js +45 -0
- package/dist/utils/is-presentation-role.js +14 -0
- package/dist/utils/is-semantic-role-element.js +36 -0
- package/dist/utils/to-pattern.js +7 -0
- package/package.json +4 -4
- package/dist/configs/base.json +0 -4
- package/dist/eslint-plugin-template/src/index.d.ts +0 -133
- package/dist/eslint-plugin-template/src/processors.d.ts +0 -37
- package/dist/eslint-plugin-template/src/rules/accessibility-alt-text.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/accessibility-elements-content.d.ts +0 -9
- package/dist/eslint-plugin-template/src/rules/accessibility-interactive-supports-focus.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/accessibility-label-for.d.ts +0 -11
- package/dist/eslint-plugin-template/src/rules/accessibility-label-has-associated-control.d.ts +0 -14
- package/dist/eslint-plugin-template/src/rules/accessibility-role-has-required-aria.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/accessibility-table-scope.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/accessibility-valid-aria.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/attributes-order.d.ts +0 -18
- package/dist/eslint-plugin-template/src/rules/banana-in-box.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/button-has-type.d.ts +0 -5
- package/dist/eslint-plugin-template/src/rules/click-events-have-key-events.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/conditional-complexity.d.ts +0 -7
- package/dist/eslint-plugin-template/src/rules/cyclomatic-complexity.d.ts +0 -7
- package/dist/eslint-plugin-template/src/rules/eqeqeq.d.ts +0 -8
- package/dist/eslint-plugin-template/src/rules/i18n.d.ts +0 -18
- package/dist/eslint-plugin-template/src/rules/mouse-events-have-key-events.d.ts +0 -5
- package/dist/eslint-plugin-template/src/rules/no-any.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/no-autofocus.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/no-call-expression.d.ts +0 -9
- package/dist/eslint-plugin-template/src/rules/no-distracting-elements.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/no-duplicate-attributes.d.ts +0 -10
- package/dist/eslint-plugin-template/src/rules/no-inline-styles.d.ts +0 -10
- package/dist/eslint-plugin-template/src/rules/no-interpolation-in-attributes.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/no-negated-async.d.ts +0 -7
- package/dist/eslint-plugin-template/src/rules/no-positive-tabindex.d.ts +0 -4
- package/dist/eslint-plugin-template/src/rules/use-track-by-function.d.ts +0 -4
- package/dist/eslint-plugin-template/src/utils/attributes-comparator.d.ts +0 -3
- package/dist/eslint-plugin-template/src/utils/constants.d.ts +0 -8
- package/dist/eslint-plugin-template/src/utils/create-eslint-rule.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/get-attribute-value.d.ts +0 -12
- package/dist/eslint-plugin-template/src/utils/get-dom-elements.d.ts +0 -1
- package/dist/eslint-plugin-template/src/utils/get-nearest-node-from.d.ts +0 -6
- package/dist/eslint-plugin-template/src/utils/get-original-attribute-name.d.ts +0 -13
- package/dist/eslint-plugin-template/src/utils/is-child-node-of.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-content-editable.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-disabled-element.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-hidden-from-screen-reader.d.ts +0 -9
- package/dist/eslint-plugin-template/src/utils/is-interactive-element/get-interactive-element-ax-object-schemas.d.ts +0 -9
- package/dist/eslint-plugin-template/src/utils/is-interactive-element/get-interactive-element-role-schemas.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-interactive-element/get-non-interactive-element-role-schemas.d.ts +0 -3
- package/dist/eslint-plugin-template/src/utils/is-interactive-element/index.d.ts +0 -9
- package/dist/eslint-plugin-template/src/utils/is-presentation-role.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/is-semantic-role-element.d.ts +0 -2
- package/dist/eslint-plugin-template/src/utils/to-pattern.d.ts +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = void 0;
|
|
4
|
+
const utils_1 = require("@angular-eslint/utils");
|
|
5
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
6
|
+
exports.RULE_NAME = 'cyclomatic-complexity';
|
|
7
|
+
const DEFAULT_MAX_COMPLEXITY = 5;
|
|
8
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
9
|
+
name: exports.RULE_NAME,
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'suggestion',
|
|
12
|
+
docs: {
|
|
13
|
+
description: `Checks cyclomatic complexity against a specified limit. It is a quantitative measure of the number of linearly independent paths through a program's source code`,
|
|
14
|
+
recommended: false,
|
|
15
|
+
},
|
|
16
|
+
schema: [
|
|
17
|
+
{
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
maxComplexity: {
|
|
21
|
+
type: 'number',
|
|
22
|
+
minimum: 1,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
messages: {
|
|
29
|
+
cyclomaticComplexity: 'The cyclomatic complexity {{totalComplexity}} exceeds the defined limit {{maxComplexity}}',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
defaultOptions: [{ maxComplexity: DEFAULT_MAX_COMPLEXITY }],
|
|
33
|
+
create(context, [{ maxComplexity }]) {
|
|
34
|
+
let totalComplexity = 0;
|
|
35
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
36
|
+
return {
|
|
37
|
+
'BoundAttribute[name=/^(ngForOf|ngIf|ngSwitchCase)$/], TextAttribute[name="ngSwitchDefault"]'({ sourceSpan, }) {
|
|
38
|
+
totalComplexity += 1;
|
|
39
|
+
if (totalComplexity <= maxComplexity)
|
|
40
|
+
return;
|
|
41
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
42
|
+
context.report({
|
|
43
|
+
messageId: 'cyclomaticComplexity',
|
|
44
|
+
loc,
|
|
45
|
+
data: { maxComplexity, totalComplexity },
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = void 0;
|
|
4
|
+
const utils_1 = require("@angular-eslint/utils");
|
|
5
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
6
|
+
const is_hidden_from_screen_reader_1 = require("../utils/is-hidden-from-screen-reader");
|
|
7
|
+
exports.RULE_NAME = 'elements-content';
|
|
8
|
+
const DEFAULT_SAFELIST_ATTRIBUTES = [
|
|
9
|
+
'aria-label',
|
|
10
|
+
'innerHtml',
|
|
11
|
+
'innerHTML',
|
|
12
|
+
'innerText',
|
|
13
|
+
'outerHTML',
|
|
14
|
+
'title',
|
|
15
|
+
];
|
|
16
|
+
const DEFAULT_OPTIONS = {
|
|
17
|
+
allowList: DEFAULT_SAFELIST_ATTRIBUTES,
|
|
18
|
+
};
|
|
19
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
20
|
+
name: exports.RULE_NAME,
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'suggestion',
|
|
23
|
+
docs: {
|
|
24
|
+
description: '[Accessibility] Ensures that the heading, anchor and button elements have content in it',
|
|
25
|
+
recommended: false,
|
|
26
|
+
},
|
|
27
|
+
schema: [
|
|
28
|
+
{
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
properties: {
|
|
31
|
+
allowList: {
|
|
32
|
+
items: { type: 'string' },
|
|
33
|
+
type: 'array',
|
|
34
|
+
uniqueItems: true,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
type: 'object',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
messages: {
|
|
41
|
+
elementsContent: '<{{element}}> should have content',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
45
|
+
create(context, [{ allowList }]) {
|
|
46
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
47
|
+
return {
|
|
48
|
+
'Element$1[name=/^(a|button|h1|h2|h3|h4|h5|h6)$/][children.length=0]'(node) {
|
|
49
|
+
if ((0, is_hidden_from_screen_reader_1.isHiddenFromScreenReader)(node))
|
|
50
|
+
return;
|
|
51
|
+
const { attributes, inputs, name: element, sourceSpan } = node;
|
|
52
|
+
const safelistAttributes = new Set([
|
|
53
|
+
...DEFAULT_SAFELIST_ATTRIBUTES,
|
|
54
|
+
...(allowList !== null && allowList !== void 0 ? allowList : []),
|
|
55
|
+
]);
|
|
56
|
+
const hasAttributeSafelisted = [...attributes, ...inputs]
|
|
57
|
+
.map(({ name }) => name)
|
|
58
|
+
.some((inputName) => safelistAttributes.has(inputName));
|
|
59
|
+
if (hasAttributeSafelisted)
|
|
60
|
+
return;
|
|
61
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
62
|
+
context.report({
|
|
63
|
+
loc,
|
|
64
|
+
messageId: 'elementsContent',
|
|
65
|
+
data: { element },
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = void 0;
|
|
4
|
+
const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-compiler");
|
|
5
|
+
const utils_1 = require("@angular-eslint/utils");
|
|
6
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
7
|
+
const get_nearest_node_from_1 = require("../utils/get-nearest-node-from");
|
|
8
|
+
exports.RULE_NAME = 'eqeqeq';
|
|
9
|
+
const DEFAULT_OPTIONS = { allowNullOrUndefined: false };
|
|
10
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
11
|
+
name: 'eqeqeq',
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'suggestion',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Requires `===` and `!==` in place of `==` and `!=`',
|
|
16
|
+
recommended: 'error',
|
|
17
|
+
},
|
|
18
|
+
hasSuggestions: true,
|
|
19
|
+
fixable: 'code',
|
|
20
|
+
schema: [
|
|
21
|
+
{
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
allowNullOrUndefined: {
|
|
25
|
+
type: 'boolean',
|
|
26
|
+
default: DEFAULT_OPTIONS.allowNullOrUndefined,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
messages: {
|
|
33
|
+
eqeqeq: 'Expected `{{expectedOperation}}` but received `{{actualOperation}}`',
|
|
34
|
+
suggestStrictEquality: 'Replace `{{actualOperation}}` with `{{expectedOperation}}`',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
38
|
+
create(context, [{ allowNullOrUndefined }]) {
|
|
39
|
+
(0, utils_1.ensureTemplateParser)(context);
|
|
40
|
+
const sourceCode = context.getSourceCode();
|
|
41
|
+
return {
|
|
42
|
+
'Binary[operation=/^(==|!=)$/]'(node) {
|
|
43
|
+
const { left, operation, right, sourceSpan: { start, end }, } = node;
|
|
44
|
+
if (allowNullOrUndefined && (isNilValue(left) || isNilValue(right))) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const data = {
|
|
48
|
+
actualOperation: operation,
|
|
49
|
+
expectedOperation: `${operation}=`,
|
|
50
|
+
};
|
|
51
|
+
context.report(Object.assign({ loc: {
|
|
52
|
+
start: sourceCode.getLocFromIndex(start),
|
|
53
|
+
end: sourceCode.getLocFromIndex(end),
|
|
54
|
+
}, messageId: 'eqeqeq', data }, (isStringNonNumericValue(left) || isStringNonNumericValue(right)
|
|
55
|
+
? {
|
|
56
|
+
fix: (fixer) => getFix({ node, left, right, start, end, fixer }),
|
|
57
|
+
}
|
|
58
|
+
: {
|
|
59
|
+
suggest: [
|
|
60
|
+
{
|
|
61
|
+
messageId: 'suggestStrictEquality',
|
|
62
|
+
fix: (fixer) => getFix({ node, left, right, start, end, fixer }),
|
|
63
|
+
data,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
})));
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
function getSpanLength({ span: { start, end } }) {
|
|
72
|
+
return end - start;
|
|
73
|
+
}
|
|
74
|
+
const getFix = ({ node, left, right, start, end, fixer, }) => {
|
|
75
|
+
var _a;
|
|
76
|
+
const { source } = (_a = (0, get_nearest_node_from_1.getNearestNodeFrom)(node, isASTWithSource)) !== null && _a !== void 0 ? _a : {};
|
|
77
|
+
if (!source)
|
|
78
|
+
return null;
|
|
79
|
+
return fixer.insertTextAfterRange([start + getSpanLength(left) + 1, end - getSpanLength(right) - 1], '=');
|
|
80
|
+
};
|
|
81
|
+
function isASTWithSource(node) {
|
|
82
|
+
return node instanceof bundled_angular_compiler_1.ASTWithSource;
|
|
83
|
+
}
|
|
84
|
+
function isLiteralPrimitive(node) {
|
|
85
|
+
return node instanceof bundled_angular_compiler_1.LiteralPrimitive;
|
|
86
|
+
}
|
|
87
|
+
function isNumeric(value) {
|
|
88
|
+
return (!Number.isNaN(Number.parseFloat(String(value))) &&
|
|
89
|
+
Number.isFinite(Number(value)));
|
|
90
|
+
}
|
|
91
|
+
function isString(value) {
|
|
92
|
+
return typeof value === 'string';
|
|
93
|
+
}
|
|
94
|
+
function isStringNonNumericValue(ast) {
|
|
95
|
+
return (isLiteralPrimitive(ast) && isString(ast.value) && !isNumeric(ast.value));
|
|
96
|
+
}
|
|
97
|
+
function isNilValue(ast) {
|
|
98
|
+
return (isLiteralPrimitive(ast) && (ast.value === null || ast.value === undefined));
|
|
99
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = void 0;
|
|
4
|
+
const bundled_angular_compiler_1 = require("@angular-eslint/bundled-angular-compiler");
|
|
5
|
+
const utils_1 = require("@angular-eslint/utils");
|
|
6
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
7
|
+
const get_nearest_node_from_1 = require("../utils/get-nearest-node-from");
|
|
8
|
+
// source: https://github.com/yury-dymov/js-regex-pl/blob/ff10757b2a98ad0bf0f62acebad085bab3748fc4/src/index.js#L7
|
|
9
|
+
const PL_PATTERN = /[A-Za-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16F1-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEF\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA7BF\uA7C2-\uA7C6\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB67\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]/;
|
|
10
|
+
const DEFAULT_ALLOWED_BOUND_TEXT_PATTERN = new RegExp(`[^${PL_PATTERN}]`);
|
|
11
|
+
const DEFAULT_ALLOWED_ATTRIBUTES = new Set([
|
|
12
|
+
'autocomplete',
|
|
13
|
+
'charset',
|
|
14
|
+
'class',
|
|
15
|
+
'color',
|
|
16
|
+
'colspan',
|
|
17
|
+
'dir',
|
|
18
|
+
'fill',
|
|
19
|
+
'for',
|
|
20
|
+
'formArrayName',
|
|
21
|
+
'formControlName',
|
|
22
|
+
'formGroupName',
|
|
23
|
+
'height',
|
|
24
|
+
'href',
|
|
25
|
+
'id',
|
|
26
|
+
'lang',
|
|
27
|
+
'list',
|
|
28
|
+
'name',
|
|
29
|
+
'ngClass',
|
|
30
|
+
'ngProjectAs',
|
|
31
|
+
'role',
|
|
32
|
+
'routerLink',
|
|
33
|
+
'routerLinkActive',
|
|
34
|
+
'src',
|
|
35
|
+
'stroke',
|
|
36
|
+
'stroke-width',
|
|
37
|
+
'style',
|
|
38
|
+
'svgIcon',
|
|
39
|
+
'tabindex',
|
|
40
|
+
'target',
|
|
41
|
+
'type',
|
|
42
|
+
'value',
|
|
43
|
+
'viewBox',
|
|
44
|
+
'width',
|
|
45
|
+
'xmlns',
|
|
46
|
+
]);
|
|
47
|
+
exports.RULE_NAME = 'i18n';
|
|
48
|
+
const DEFAULT_OPTIONS = {
|
|
49
|
+
checkAttributes: true,
|
|
50
|
+
checkId: true,
|
|
51
|
+
checkDuplicateId: true,
|
|
52
|
+
checkText: true,
|
|
53
|
+
ignoreAttributes: [...DEFAULT_ALLOWED_ATTRIBUTES],
|
|
54
|
+
};
|
|
55
|
+
const STYLE_GUIDE_LINK = 'https://angular.io/guide/i18n';
|
|
56
|
+
const STYLE_GUIDE_LINK_ATTRIBUTES = `${STYLE_GUIDE_LINK}#mark-element-attributes-for-translations`;
|
|
57
|
+
const STYLE_GUIDE_LINK_ICU = `${STYLE_GUIDE_LINK}#mark-plurals-and-alternates-for-translation`;
|
|
58
|
+
const STYLE_GUIDE_LINK_TEXTS = `${STYLE_GUIDE_LINK}#mark-text-for-translations`;
|
|
59
|
+
const STYLE_GUIDE_LINK_CUSTOM_IDS = `${STYLE_GUIDE_LINK}#manage-marked-text-with-custom-ids`;
|
|
60
|
+
const STYLE_GUIDE_LINK_UNIQUE_CUSTOM_IDS = `${STYLE_GUIDE_LINK}#define-unique-custom-ids`;
|
|
61
|
+
const STYLE_GUIDE_LINK_COMMON_PREPARE = `${STYLE_GUIDE_LINK}-common-prepare`;
|
|
62
|
+
const STYLE_GUIDE_LINK_METADATA_FOR_TRANSLATION = `${STYLE_GUIDE_LINK_COMMON_PREPARE}#i18n-metadata-for-translation`;
|
|
63
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
64
|
+
name: exports.RULE_NAME,
|
|
65
|
+
meta: {
|
|
66
|
+
type: 'suggestion',
|
|
67
|
+
docs: {
|
|
68
|
+
description: 'Ensures following best practices for i18n. ' +
|
|
69
|
+
'Checks for missing i18n attributes on elements and attributes containing ' +
|
|
70
|
+
'texts. ' +
|
|
71
|
+
'Can also check for texts without i18n attribute, elements that do not ' +
|
|
72
|
+
'use custom ID (@@) feature and duplicate custom IDs',
|
|
73
|
+
recommended: false,
|
|
74
|
+
},
|
|
75
|
+
fixable: 'code',
|
|
76
|
+
hasSuggestions: true,
|
|
77
|
+
schema: [
|
|
78
|
+
{
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
boundTextAllowedPattern: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
},
|
|
84
|
+
checkAttributes: {
|
|
85
|
+
type: 'boolean',
|
|
86
|
+
default: DEFAULT_OPTIONS.checkAttributes,
|
|
87
|
+
},
|
|
88
|
+
checkDuplicateId: {
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
default: DEFAULT_OPTIONS.checkDuplicateId,
|
|
91
|
+
},
|
|
92
|
+
checkId: {
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
default: DEFAULT_OPTIONS.checkId,
|
|
95
|
+
},
|
|
96
|
+
checkText: {
|
|
97
|
+
type: 'boolean',
|
|
98
|
+
default: DEFAULT_OPTIONS.checkText,
|
|
99
|
+
},
|
|
100
|
+
ignoreAttributes: {
|
|
101
|
+
type: 'array',
|
|
102
|
+
items: {
|
|
103
|
+
type: 'string',
|
|
104
|
+
},
|
|
105
|
+
default: [...DEFAULT_ALLOWED_ATTRIBUTES],
|
|
106
|
+
},
|
|
107
|
+
ignoreTags: {
|
|
108
|
+
type: 'array',
|
|
109
|
+
items: {
|
|
110
|
+
type: 'string',
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
requireDescription: {
|
|
114
|
+
type: 'boolean',
|
|
115
|
+
default: DEFAULT_OPTIONS.requireDescription,
|
|
116
|
+
},
|
|
117
|
+
requireMeaning: {
|
|
118
|
+
type: 'boolean',
|
|
119
|
+
default: DEFAULT_OPTIONS.requireMeaning,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
messages: {
|
|
126
|
+
i18nAttribute: `Attribute "{{attributeName}}" has no corresponding i18n attribute. See more at ${STYLE_GUIDE_LINK_ATTRIBUTES}`,
|
|
127
|
+
i18nAttributeOnIcuOrText: `Each element containing text node should have an i18n attribute. See more at ${STYLE_GUIDE_LINK_ICU} and ${STYLE_GUIDE_LINK_TEXTS}`,
|
|
128
|
+
i18nCustomIdOnAttribute: `Missing custom ID on attribute "i18n-{{attributeName}}". See more at ${STYLE_GUIDE_LINK_CUSTOM_IDS}`,
|
|
129
|
+
i18nCustomIdOnElement: `Missing custom ID on element. See more at ${STYLE_GUIDE_LINK_CUSTOM_IDS}`,
|
|
130
|
+
i18nDuplicateCustomId: `Duplicate custom ID "@@{{customId}}". See more at ${STYLE_GUIDE_LINK_UNIQUE_CUSTOM_IDS}`,
|
|
131
|
+
suggestAddI18nAttribute: 'Add the `i18n` attribute',
|
|
132
|
+
i18nMissingDescription: `Missing i18n description on element. See more at ${STYLE_GUIDE_LINK_METADATA_FOR_TRANSLATION}`,
|
|
133
|
+
i18nMissingMeaning: `Missing i18n meaning on element. See more at ${STYLE_GUIDE_LINK_METADATA_FOR_TRANSLATION}`,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
defaultOptions: [DEFAULT_OPTIONS],
|
|
137
|
+
create(context, [{ boundTextAllowedPattern, checkAttributes, checkId, checkDuplicateId, checkText, ignoreAttributes, ignoreTags, requireDescription, requireMeaning, },]) {
|
|
138
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
139
|
+
const sourceCode = context.getSourceCode();
|
|
140
|
+
const allowedBoundTextPattern = boundTextAllowedPattern
|
|
141
|
+
? new RegExp(boundTextAllowedPattern)
|
|
142
|
+
: DEFAULT_ALLOWED_BOUND_TEXT_PATTERN;
|
|
143
|
+
const allowedAttributes = new Set([
|
|
144
|
+
...DEFAULT_ALLOWED_ATTRIBUTES,
|
|
145
|
+
...(ignoreAttributes !== null && ignoreAttributes !== void 0 ? ignoreAttributes : []),
|
|
146
|
+
]);
|
|
147
|
+
const allowedTags = new Set(ignoreTags);
|
|
148
|
+
const collectedCustomIds = new Map();
|
|
149
|
+
function handleElementOrTemplate(node) {
|
|
150
|
+
var _a;
|
|
151
|
+
const { i18n, parent, sourceSpan } = node;
|
|
152
|
+
if (isTagAllowed(allowedTags, node) ||
|
|
153
|
+
isElementWithI18n(parent) ||
|
|
154
|
+
isTemplateWithI18n(parent)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
158
|
+
const customId = getI18nCustomId(i18n);
|
|
159
|
+
if (checkId) {
|
|
160
|
+
if (!customId) {
|
|
161
|
+
context.report({
|
|
162
|
+
messageId: 'i18nCustomIdOnElement',
|
|
163
|
+
loc,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const sourceSpans = (_a = collectedCustomIds.get(customId)) !== null && _a !== void 0 ? _a : [];
|
|
168
|
+
collectedCustomIds.set(customId, [...sourceSpans, sourceSpan]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (requireDescription && !i18n.description) {
|
|
172
|
+
context.report({
|
|
173
|
+
messageId: 'i18nMissingDescription',
|
|
174
|
+
loc,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (requireMeaning && !i18n.meaning) {
|
|
178
|
+
context.report({
|
|
179
|
+
messageId: 'i18nMissingMeaning',
|
|
180
|
+
loc,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function handleTextAttribute({ i18n, keySpan, name: attributeName, parent: { name: elementName }, sourceSpan, value, }) {
|
|
185
|
+
var _a;
|
|
186
|
+
if (allowedTags.has(elementName)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const keyOrSourceSpanLoc = parserServices.convertNodeSourceSpanToLoc(keySpan !== null && keySpan !== void 0 ? keySpan : sourceSpan);
|
|
190
|
+
if (i18n) {
|
|
191
|
+
const { customId, description } = i18n;
|
|
192
|
+
if (checkId) {
|
|
193
|
+
if (isEmpty(customId)) {
|
|
194
|
+
context.report({
|
|
195
|
+
messageId: 'i18nCustomIdOnAttribute',
|
|
196
|
+
loc: keyOrSourceSpanLoc,
|
|
197
|
+
data: { attributeName },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const sourceSpans = (_a = collectedCustomIds.get(customId)) !== null && _a !== void 0 ? _a : [];
|
|
202
|
+
collectedCustomIds.set(customId, [...sourceSpans, sourceSpan]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (requireDescription && isEmpty(description)) {
|
|
206
|
+
context.report({
|
|
207
|
+
messageId: 'i18nMissingDescription',
|
|
208
|
+
loc: keyOrSourceSpanLoc,
|
|
209
|
+
data: { attributeName },
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (i18n ||
|
|
214
|
+
!checkAttributes ||
|
|
215
|
+
isAttributeAllowed(allowedAttributes, elementName, attributeName, value)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
context.report({
|
|
219
|
+
messageId: 'i18nAttribute',
|
|
220
|
+
loc: keyOrSourceSpanLoc,
|
|
221
|
+
data: { attributeName },
|
|
222
|
+
fix: (fixer) => {
|
|
223
|
+
const { end } = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
224
|
+
const endIndex = sourceCode.getIndexFromLoc(end);
|
|
225
|
+
return fixer.insertTextAfterRange([endIndex, endIndex], ` i18n-${attributeName}`);
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function handleBoundTextOrIcuOrText(node) {
|
|
230
|
+
var _a;
|
|
231
|
+
const { parent, sourceSpan } = node;
|
|
232
|
+
if ((isBoundText(node) &&
|
|
233
|
+
isBoundTextAllowed(allowedBoundTextPattern, node)) ||
|
|
234
|
+
((isElement(parent) || isTemplate(parent)) &&
|
|
235
|
+
(parent.i18n || isTagAllowed(allowedTags, parent)))) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
239
|
+
const fix = (fixer) => getFixForIcuOrText(sourceCode, parserServices, fixer, loc, parent);
|
|
240
|
+
context.report(Object.assign({ messageId: 'i18nAttributeOnIcuOrText', loc }, (((_a = parent === null || parent === void 0 ? void 0 : parent.children) === null || _a === void 0 ? void 0 : _a.filter((child) => isElement(child) || isTemplate(child)).length)
|
|
241
|
+
? { suggest: [{ messageId: 'suggestAddI18nAttribute', fix }] }
|
|
242
|
+
: { fix })));
|
|
243
|
+
}
|
|
244
|
+
function reportDuplicatedCustomIds() {
|
|
245
|
+
if (checkDuplicateId) {
|
|
246
|
+
for (const [customId, sourceSpans] of collectedCustomIds) {
|
|
247
|
+
if (sourceSpans.length <= 1) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
for (const sourceSpan of sourceSpans) {
|
|
251
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(sourceSpan);
|
|
252
|
+
context.report({
|
|
253
|
+
messageId: 'i18nDuplicateCustomId',
|
|
254
|
+
loc,
|
|
255
|
+
data: { customId },
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
collectedCustomIds.clear();
|
|
261
|
+
}
|
|
262
|
+
return Object.assign(Object.assign(Object.assign(Object.assign({}, ((checkId || requireDescription || requireMeaning) && {
|
|
263
|
+
':matches(Element$1, Template[tagName="ng-template"])[i18n]'(node) {
|
|
264
|
+
handleElementOrTemplate(node);
|
|
265
|
+
},
|
|
266
|
+
})), ((checkAttributes ||
|
|
267
|
+
checkId ||
|
|
268
|
+
requireDescription ||
|
|
269
|
+
requireMeaning) && {
|
|
270
|
+
[`Element$1 > TextAttribute[value=${PL_PATTERN}]`](node) {
|
|
271
|
+
handleTextAttribute(node);
|
|
272
|
+
},
|
|
273
|
+
})), (checkText && {
|
|
274
|
+
[`BoundText, Icu$1, Text$3[value=${PL_PATTERN}]`](node) {
|
|
275
|
+
handleBoundTextOrIcuOrText(node);
|
|
276
|
+
},
|
|
277
|
+
})), { 'Program:exit'() {
|
|
278
|
+
reportDuplicatedCustomIds();
|
|
279
|
+
} });
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
function getFixForIcuOrTextWithParent(sourceCode, fixer, { start }, tagName) {
|
|
283
|
+
const startIndex = sourceCode.getIndexFromLoc(start);
|
|
284
|
+
const insertIndex = startIndex + tagName.length + 1;
|
|
285
|
+
return fixer.insertTextAfterRange([insertIndex, insertIndex], ' i18n');
|
|
286
|
+
}
|
|
287
|
+
function getFixForIcuOrTextWithoutParent(sourceCode, fixer, { start, end }) {
|
|
288
|
+
const startIndex = sourceCode.getIndexFromLoc(start);
|
|
289
|
+
const endIndex = sourceCode.getIndexFromLoc(end);
|
|
290
|
+
return [
|
|
291
|
+
fixer.insertTextBeforeRange([startIndex, startIndex], '<ng-container i18n>'),
|
|
292
|
+
fixer.insertTextAfterRange([endIndex, endIndex], '</ng-container>'),
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
function getFixForIcuOrText(sourceCode, parserServices, fixer, loc, parent) {
|
|
296
|
+
if (!isElement(parent) && !isTemplate(parent)) {
|
|
297
|
+
return getFixForIcuOrTextWithoutParent(sourceCode, fixer, loc);
|
|
298
|
+
}
|
|
299
|
+
if ((0, get_nearest_node_from_1.getNearestNodeFrom)(parent, isElementOrTemplateWithI18n)) {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
const tagName = getTagName(parent);
|
|
303
|
+
if (!tagName) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
const parentLoc = parserServices.convertNodeSourceSpanToLoc(parent.sourceSpan);
|
|
307
|
+
return getFixForIcuOrTextWithParent(sourceCode, fixer, parentLoc, tagName);
|
|
308
|
+
}
|
|
309
|
+
function isBoundText(ast) {
|
|
310
|
+
return ast instanceof bundled_angular_compiler_1.TmplAstBoundText;
|
|
311
|
+
}
|
|
312
|
+
function isElement(ast) {
|
|
313
|
+
return ast instanceof bundled_angular_compiler_1.TmplAstElement;
|
|
314
|
+
}
|
|
315
|
+
function isElementWithI18n(ast) {
|
|
316
|
+
return Boolean(isElement(ast) && ast.i18n);
|
|
317
|
+
}
|
|
318
|
+
function isBooleanLike(value) {
|
|
319
|
+
return value === 'false' || value === 'true';
|
|
320
|
+
}
|
|
321
|
+
function isEmpty(value) {
|
|
322
|
+
if (value) {
|
|
323
|
+
return value.trim().length === 0;
|
|
324
|
+
}
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
function isNumeric(value) {
|
|
328
|
+
const valueAsFloat = Number.parseFloat(value);
|
|
329
|
+
return !Number.isNaN(valueAsFloat) && Number.isFinite(valueAsFloat);
|
|
330
|
+
}
|
|
331
|
+
function isAttributeAllowed(allowedAttributes, elementName, attributeName, attributeValue) {
|
|
332
|
+
return (allowedAttributes.has(attributeName) ||
|
|
333
|
+
allowedAttributes.has(`${elementName}[${attributeName}]`) ||
|
|
334
|
+
isEmpty(attributeValue) ||
|
|
335
|
+
isBooleanLike(attributeValue) ||
|
|
336
|
+
isNumeric(attributeValue));
|
|
337
|
+
}
|
|
338
|
+
// A `BoundText` is considered "allowed" if it doesn't contain letters (including latin characters) or if it was deliberately allowed via `boundTextAllowedPattern` option.
|
|
339
|
+
function isBoundTextAllowed(allowedBoundTextPattern, { value: { ast: { strings }, }, }) {
|
|
340
|
+
const text = strings.join('').trim();
|
|
341
|
+
return !PL_PATTERN.test(text) || allowedBoundTextPattern.test(text);
|
|
342
|
+
}
|
|
343
|
+
function isTemplate(ast) {
|
|
344
|
+
return ast instanceof bundled_angular_compiler_1.TmplAstTemplate;
|
|
345
|
+
}
|
|
346
|
+
function isTemplateWithI18n(ast) {
|
|
347
|
+
return Boolean(isTemplate(ast) && ast.i18n);
|
|
348
|
+
}
|
|
349
|
+
function isElementOrTemplateWithI18n(ast) {
|
|
350
|
+
return isElementWithI18n(ast) || isTemplateWithI18n(ast);
|
|
351
|
+
}
|
|
352
|
+
function getTagName(node) {
|
|
353
|
+
if (isElement(node)) {
|
|
354
|
+
return node.name;
|
|
355
|
+
}
|
|
356
|
+
if (isTemplate(node)) {
|
|
357
|
+
return node.tagName;
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
function isTagAllowed(allowedTags, node) {
|
|
362
|
+
const tagName = getTagName(node);
|
|
363
|
+
return Boolean(tagName && allowedTags.has(tagName));
|
|
364
|
+
}
|
|
365
|
+
// `customId` could be `undefined` in case of non-`Message`.
|
|
366
|
+
function getI18nCustomId(i18n) {
|
|
367
|
+
return i18n.customId;
|
|
368
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RULE_NAME = void 0;
|
|
4
|
+
const utils_1 = require("@angular-eslint/utils");
|
|
5
|
+
const create_eslint_rule_1 = require("../utils/create-eslint-rule");
|
|
6
|
+
const get_dom_elements_1 = require("../utils/get-dom-elements");
|
|
7
|
+
const is_hidden_from_screen_reader_1 = require("../utils/is-hidden-from-screen-reader");
|
|
8
|
+
const is_interactive_element_1 = require("../utils/is-interactive-element");
|
|
9
|
+
const is_content_editable_1 = require("../utils/is-content-editable");
|
|
10
|
+
const is_disabled_element_1 = require("../utils/is-disabled-element");
|
|
11
|
+
const is_presentation_role_1 = require("../utils/is-presentation-role");
|
|
12
|
+
exports.RULE_NAME = 'interactive-supports-focus';
|
|
13
|
+
exports.default = (0, create_eslint_rule_1.createESLintRule)({
|
|
14
|
+
name: exports.RULE_NAME,
|
|
15
|
+
meta: {
|
|
16
|
+
type: 'suggestion',
|
|
17
|
+
docs: {
|
|
18
|
+
description: '[Accessibility] Ensures that elements with interactive handlers like `(click)` are focusable.',
|
|
19
|
+
recommended: false,
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
messages: {
|
|
23
|
+
interactiveSupportsFocus: 'Elements with interaction handlers must be focusable.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
Element$1(node) {
|
|
30
|
+
const elementType = node.name;
|
|
31
|
+
if (!(0, get_dom_elements_1.getDomElements)().has(elementType)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const interactiveOutput = node.outputs.find((output) => output.name === 'click' ||
|
|
35
|
+
output.name.startsWith('keyup') ||
|
|
36
|
+
output.name.startsWith('keydown') ||
|
|
37
|
+
output.name.startsWith('keypress'));
|
|
38
|
+
if (!interactiveOutput ||
|
|
39
|
+
(0, is_disabled_element_1.isDisabledElement)(node) ||
|
|
40
|
+
(0, is_hidden_from_screen_reader_1.isHiddenFromScreenReader)(node) ||
|
|
41
|
+
(0, is_presentation_role_1.isPresentationRole)(node)) {
|
|
42
|
+
// Presentation is an intentional signal from the author
|
|
43
|
+
// that this element is not meant to be perceivable.
|
|
44
|
+
// For example, a click screen overlay to close a dialog.
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const tabIndex = [...node.attributes, ...node.inputs].find((attr) => attr.name === 'tabindex');
|
|
48
|
+
if (interactiveOutput &&
|
|
49
|
+
!tabIndex &&
|
|
50
|
+
!(0, is_interactive_element_1.isInteractiveElement)(node) &&
|
|
51
|
+
!(0, is_interactive_element_1.isNonInteractiveRole)(node) &&
|
|
52
|
+
!(0, is_content_editable_1.isContentEditable)(node)) {
|
|
53
|
+
const parserServices = (0, utils_1.getTemplateParserServices)(context);
|
|
54
|
+
const loc = parserServices.convertNodeSourceSpanToLoc(node.sourceSpan);
|
|
55
|
+
const messageId = 'interactiveSupportsFocus';
|
|
56
|
+
context.report({
|
|
57
|
+
loc,
|
|
58
|
+
messageId,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|