1 | "use strict";
|
2 | var __extends = (this && this.__extends) || (function () {
|
3 | var extendStatics = Object.setPrototypeOf ||
|
4 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
5 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
6 | return function (d, b) {
|
7 | extendStatics(d, b);
|
8 | function __() { this.constructor = d; }
|
9 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
10 | };
|
11 | })();
|
12 | Object.defineProperty(exports, "__esModule", { value: true });
|
13 | var ts = require("typescript");
|
14 | var Lint = require("tslint");
|
15 | var ErrorTolerantWalker_1 = require("./utils/ErrorTolerantWalker");
|
16 | var Utils_1 = require("./utils/Utils");
|
17 | var getImplicitRole_1 = require("./utils/getImplicitRole");
|
18 | var JsxAttribute_1 = require("./utils/JsxAttribute");
|
19 | var ROLE_STRING = 'role';
|
20 | exports.NO_HASH_FAILURE_STRING = 'Do not use # as anchor href.';
|
21 | exports.LINK_TEXT_TOO_SHORT_FAILURE_STRING = 'Link text or the alt text of image in link should be at least 4 characters long. ' +
|
22 | 'If you are not using <a> element as anchor, please specify explicit role, e.g. role=\'button\'';
|
23 | exports.UNIQUE_ALT_FAILURE_STRING = 'Links with images and text content, the alt attribute should be unique to the text content or empty.';
|
24 | exports.SAME_HREF_SAME_TEXT_FAILURE_STRING = 'Links with the same HREF should have the same link text.';
|
25 | exports.DIFFERENT_HREF_DIFFERENT_TEXT_FAILURE_STRING = 'Links that point to different HREFs should have different link text.';
|
26 | var Rule = (function (_super) {
|
27 | __extends(Rule, _super);
|
28 | function Rule() {
|
29 | return _super !== null && _super.apply(this, arguments) || this;
|
30 | }
|
31 | Rule.prototype.apply = function (sourceFile) {
|
32 | if (sourceFile.languageVariant === ts.LanguageVariant.JSX) {
|
33 | var rule = new ReactA11yAnchorsRuleWalker(sourceFile, this.getOptions());
|
34 | this.applyWithWalker(rule);
|
35 | rule.validateAllAnchors();
|
36 | return rule.getFailures();
|
37 | }
|
38 | return [];
|
39 | };
|
40 | Rule.metadata = {
|
41 | ruleName: 'react-a11y-anchors',
|
42 | type: 'functionality',
|
43 | description: 'For accessibility of your website, anchor elements must have a href different from # and a text longer than 4.',
|
44 | options: null,
|
45 | optionsDescription: '',
|
46 | typescriptOnly: true,
|
47 | issueClass: 'Non-SDL',
|
48 | issueType: 'Warning',
|
49 | severity: 'Low',
|
50 | level: 'Opportunity for Excellence',
|
51 | group: 'Accessibility'
|
52 | };
|
53 | return Rule;
|
54 | }(Lint.Rules.AbstractRule));
|
55 | exports.Rule = Rule;
|
56 | var ReactA11yAnchorsRuleWalker = (function (_super) {
|
57 | __extends(ReactA11yAnchorsRuleWalker, _super);
|
58 | function ReactA11yAnchorsRuleWalker() {
|
59 | var _this = _super !== null && _super.apply(this, arguments) || this;
|
60 | _this.anchorInfoList = [];
|
61 | return _this;
|
62 | }
|
63 | ReactA11yAnchorsRuleWalker.prototype.validateAllAnchors = function () {
|
64 | var _this = this;
|
65 | var sameHrefDifferentTexts = [];
|
66 | var differentHrefSameText = [];
|
67 | var _loop_1 = function () {
|
68 | var current = this_1.anchorInfoList.shift();
|
69 | this_1.anchorInfoList.forEach(function (anchorInfo) {
|
70 | if (current.href &&
|
71 | current.href === anchorInfo.href &&
|
72 | (current.text !== anchorInfo.text || current.altText !== anchorInfo.altText) &&
|
73 | !Utils_1.Utils.contains(sameHrefDifferentTexts, anchorInfo)) {
|
74 | sameHrefDifferentTexts.push(anchorInfo);
|
75 | _this.addFailureAt(anchorInfo.start, anchorInfo.width, exports.SAME_HREF_SAME_TEXT_FAILURE_STRING + _this.firstPosition(current));
|
76 | }
|
77 | if (current.href !== anchorInfo.href &&
|
78 | current.text === anchorInfo.text &&
|
79 | current.altText === anchorInfo.altText &&
|
80 | !Utils_1.Utils.contains(differentHrefSameText, anchorInfo)) {
|
81 | differentHrefSameText.push(anchorInfo);
|
82 | _this.addFailureAt(anchorInfo.start, anchorInfo.width, exports.DIFFERENT_HREF_DIFFERENT_TEXT_FAILURE_STRING + _this.firstPosition(current));
|
83 | }
|
84 | });
|
85 | };
|
86 | var this_1 = this;
|
87 | while (this.anchorInfoList.length > 0) {
|
88 | _loop_1();
|
89 | }
|
90 | };
|
91 | ReactA11yAnchorsRuleWalker.prototype.firstPosition = function (anchorInfo) {
|
92 | var startPosition = this.createFailure(anchorInfo.start, anchorInfo.width, '').getStartPosition().getLineAndCharacter();
|
93 | var character = startPosition.character + 1;
|
94 | var line = startPosition.line + 1;
|
95 | return " First link at character: " + character + " line: " + line;
|
96 | };
|
97 | ReactA11yAnchorsRuleWalker.prototype.visitJsxSelfClosingElement = function (node) {
|
98 | this.validateAnchor(node, node);
|
99 | _super.prototype.visitJsxSelfClosingElement.call(this, node);
|
100 | };
|
101 | ReactA11yAnchorsRuleWalker.prototype.visitJsxElement = function (node) {
|
102 | this.validateAnchor(node, node.openingElement);
|
103 | _super.prototype.visitJsxElement.call(this, node);
|
104 | };
|
105 | ReactA11yAnchorsRuleWalker.prototype.validateAnchor = function (parent, openingElement) {
|
106 | if (openingElement.tagName.getText() === 'a') {
|
107 | var anchorInfo = {
|
108 | href: this.getAttribute(openingElement, 'href'),
|
109 | text: this.anchorText(parent),
|
110 | altText: this.imageAlt(parent),
|
111 | start: parent.getStart(),
|
112 | width: parent.getWidth()
|
113 | };
|
114 | if (anchorInfo.href === '#') {
|
115 | this.addFailureAt(anchorInfo.start, anchorInfo.width, exports.NO_HASH_FAILURE_STRING);
|
116 | }
|
117 | if (anchorInfo.altText && anchorInfo.altText === anchorInfo.text) {
|
118 | this.addFailureAt(anchorInfo.start, anchorInfo.width, exports.UNIQUE_ALT_FAILURE_STRING);
|
119 | }
|
120 | var anchorInfoTextLength = anchorInfo.text ? anchorInfo.text.length : 0;
|
121 | var anchorImageAltTextLength = anchorInfo.altText ? anchorInfo.altText.length : 0;
|
122 | if (this.anchorRole(openingElement) === 'link' &&
|
123 | anchorInfoTextLength < 4 &&
|
124 | anchorImageAltTextLength < 4) {
|
125 | this.addFailureAt(anchorInfo.start, anchorInfo.width, exports.LINK_TEXT_TOO_SHORT_FAILURE_STRING);
|
126 | }
|
127 | this.anchorInfoList.push(anchorInfo);
|
128 | }
|
129 | };
|
130 | ReactA11yAnchorsRuleWalker.prototype.getAttribute = function (openingElement, attributeName) {
|
131 | var attributes = JsxAttribute_1.getJsxAttributesFromJsxElement(openingElement);
|
132 | var attribute = attributes[attributeName];
|
133 | return attribute ? JsxAttribute_1.getStringLiteral(attribute) : '';
|
134 | };
|
135 | ReactA11yAnchorsRuleWalker.prototype.anchorText = function (root) {
|
136 | var _this = this;
|
137 | var title = '';
|
138 | if (root.kind === ts.SyntaxKind.JsxElement) {
|
139 | var jsxElement = root;
|
140 | jsxElement.children.forEach(function (child) {
|
141 | title += _this.anchorText(child);
|
142 | });
|
143 | }
|
144 | else if (root.kind === ts.SyntaxKind.JsxText) {
|
145 | var jsxText = root;
|
146 | title += jsxText.getText();
|
147 | }
|
148 | else if (root.kind === ts.SyntaxKind.StringLiteral) {
|
149 | var literal = root;
|
150 | title += literal.text;
|
151 | }
|
152 | else if (root.kind === ts.SyntaxKind.JsxExpression) {
|
153 | var expression = root;
|
154 | title += this.anchorText(expression.expression);
|
155 | }
|
156 | else if (root.kind !== ts.SyntaxKind.JsxSelfClosingElement) {
|
157 | title += '<unknown>';
|
158 | }
|
159 | return title;
|
160 | };
|
161 | ReactA11yAnchorsRuleWalker.prototype.anchorRole = function (root) {
|
162 | var attributesInElement = JsxAttribute_1.getJsxAttributesFromJsxElement(root);
|
163 | var roleProp = attributesInElement[ROLE_STRING];
|
164 | return roleProp ? JsxAttribute_1.getStringLiteral(roleProp) : getImplicitRole_1.getImplicitRole(root);
|
165 | };
|
166 | ReactA11yAnchorsRuleWalker.prototype.imageAltAttribute = function (openingElement) {
|
167 | if (openingElement.tagName.getText() === 'img') {
|
168 | var altAttribute = this.getAttribute(openingElement, 'alt');
|
169 | return altAttribute === undefined ? '<unknown>' : altAttribute;
|
170 | }
|
171 | return '';
|
172 | };
|
173 | ReactA11yAnchorsRuleWalker.prototype.imageAlt = function (root) {
|
174 | var _this = this;
|
175 | var altText = '';
|
176 | if (root.kind === ts.SyntaxKind.JsxElement) {
|
177 | var jsxElement = root;
|
178 | altText += this.imageAltAttribute(jsxElement.openingElement);
|
179 | jsxElement.children.forEach(function (child) {
|
180 | altText += _this.imageAlt(child);
|
181 | });
|
182 | }
|
183 | if (root.kind === ts.SyntaxKind.JsxSelfClosingElement) {
|
184 | var jsxSelfClosingElement = root;
|
185 | altText += this.imageAltAttribute(jsxSelfClosingElement);
|
186 | }
|
187 | return altText;
|
188 | };
|
189 | return ReactA11yAnchorsRuleWalker;
|
190 | }(ErrorTolerantWalker_1.ErrorTolerantWalker));
|
191 | var AnchorInfo = (function () {
|
192 | function AnchorInfo() {
|
193 | this.href = '';
|
194 | this.text = '';
|
195 | this.altText = '';
|
196 | this.start = 0;
|
197 | this.width = 0;
|
198 | }
|
199 | return AnchorInfo;
|
200 | }());
|
201 |
|
\ | No newline at end of file |