UNPKG

14 kBJavaScriptView Raw
1"use strict";
2var __extends = (this && this.__extends) || (function () {
3 var extendStatics = function (d, b) {
4 extendStatics = Object.setPrototypeOf ||
5 ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 return extendStatics(d, b);
8 }
9 return function (d, b) {
10 extendStatics(d, b);
11 function __() { this.constructor = d; }
12 d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 };
14})();
15var __makeTemplateObject = (this && this.__makeTemplateObject) || function (cooked, raw) {
16 if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
17 return cooked;
18};
19Object.defineProperty(exports, "__esModule", { value: true });
20var ts = require("typescript");
21var Lint = require("tslint");
22var tsutils = require("tsutils");
23var Utils_1 = require("./utils/Utils");
24var getImplicitRole_1 = require("./utils/getImplicitRole");
25var JsxAttribute_1 = require("./utils/JsxAttribute");
26var TypeGuard_1 = require("./utils/TypeGuard");
27exports.OPTION_IGNORE_CASE = 'ignore-case';
28exports.OPTION_IGNORE_WHITESPACE = 'ignore-whitespace';
29var ROLE_STRING = 'role';
30exports.NO_HASH_FAILURE_STRING = 'Do not use # as anchor href.';
31exports.MISSING_HREF_FAILURE_STRING = 'Do not leave href undefined or null';
32exports.LINK_TEXT_TOO_SHORT_FAILURE_STRING = 'Link text or the alt text of image in link should be at least 4 characters long. ' +
33 "If you are not using <a> element as anchor, please specify explicit role, e.g. role='button'";
34exports.UNIQUE_ALT_FAILURE_STRING = 'Links with images and text content, the alt attribute should be unique to the text content or empty.';
35exports.SAME_HREF_SAME_TEXT_FAILURE_STRING = 'Links with the same HREF should have the same link text.';
36exports.DIFFERENT_HREF_DIFFERENT_TEXT_FAILURE_STRING = 'Links that point to different HREFs should have different link text.';
37exports.ACCESSIBLE_HIDDEN_CONTENT_FAILURE_STRING = 'Link content can not be hidden for screen-readers by using aria-hidden attribute.';
38var Rule = (function (_super) {
39 __extends(Rule, _super);
40 function Rule() {
41 return _super !== null && _super.apply(this, arguments) || this;
42 }
43 Rule.prototype.apply = function (sourceFile) {
44 if (sourceFile.languageVariant === ts.LanguageVariant.JSX) {
45 return this.applyWithFunction(sourceFile, walk, this.parseOptions(this.getOptions()));
46 }
47 return [];
48 };
49 Rule.prototype.parseOptions = function (options) {
50 var parsed = {
51 ignoreCase: false,
52 ignoreWhitespace: ''
53 };
54 options.ruleArguments.forEach(function (opt) {
55 if (typeof opt === 'string' && opt === exports.OPTION_IGNORE_CASE) {
56 parsed.ignoreCase = true;
57 }
58 if (TypeGuard_1.isObject(opt)) {
59 parsed.ignoreWhitespace = opt[exports.OPTION_IGNORE_WHITESPACE];
60 }
61 });
62 return parsed;
63 };
64 Rule.metadata = {
65 ruleName: 'react-a11y-anchors',
66 type: 'functionality',
67 description: 'For accessibility of your website, anchor elements must have a href different from # and a text longer than 4.',
68 rationale: "References:\n <ul>\n <li><a href=\"http://oaa-accessibility.org/wcag20/rule/38\">WCAG Rule 38: Link text should be as least four 4 characters long</a></li>\n <li><a href=\"http://oaa-accessibility.org/wcag20/rule/39\">WCAG Rule 39: Links with the same HREF should have the same link text</a></li>\n <li><a href=\"http://oaa-accessibility.org/wcag20/rule/41\">WCAG Rule 41: Links that point to different HREFs should have different link text</a></li>\n <li><a href=\"http://oaa-accessibility.org/wcag20/rule/43\">WCAG Rule 43: Links with images and text content, the alt attribute should be unique to the text content or empty</a></li>\n </ul>",
69 options: {
70 type: 'array',
71 items: {
72 type: 'string',
73 enum: [exports.OPTION_IGNORE_CASE, exports.OPTION_IGNORE_WHITESPACE]
74 },
75 minLength: 0,
76 maxLength: 2
77 },
78 optionsDescription: Lint.Utils.dedent(templateObject_1 || (templateObject_1 = __makeTemplateObject(["\n Optional arguments to relax the same HREF same link text rule are provided:\n * `", "` ignore differences in cases.\n * `{\"", "\": \"trim\"}` ignore differences only in leading/trailing whitespace.\n * `{\"", "\": \"all\"}` ignore differences in all whitespace.\n "], ["\n Optional arguments to relax the same HREF same link text rule are provided:\n * \\`", "\\` ignore differences in cases.\n * \\`{\"", "\": \"trim\"}\\` ignore differences only in leading/trailing whitespace.\n * \\`{\"", "\": \"all\"}\\` ignore differences in all whitespace.\n "])), exports.OPTION_IGNORE_CASE, exports.OPTION_IGNORE_WHITESPACE, exports.OPTION_IGNORE_WHITESPACE),
79 typescriptOnly: true,
80 issueClass: 'Non-SDL',
81 issueType: 'Warning',
82 severity: 'Low',
83 level: 'Opportunity for Excellence',
84 group: 'Accessibility'
85 };
86 return Rule;
87}(Lint.Rules.AbstractRule));
88exports.Rule = Rule;
89function walk(ctx) {
90 var anchorInfoList = [];
91 function validateAllAnchors() {
92 var sameHrefDifferentTexts = [];
93 var differentHrefSameText = [];
94 var _loop_1 = function () {
95 var current = anchorInfoList.shift();
96 anchorInfoList.forEach(function (anchorInfo) {
97 if (current.href &&
98 current.href === anchorInfo.href &&
99 !compareAnchorsText(current, anchorInfo) &&
100 !Utils_1.Utils.contains(sameHrefDifferentTexts, anchorInfo)) {
101 sameHrefDifferentTexts.push(anchorInfo);
102 ctx.addFailureAt(anchorInfo.start, anchorInfo.width, exports.SAME_HREF_SAME_TEXT_FAILURE_STRING + firstPosition(current));
103 }
104 if (current.href !== anchorInfo.href &&
105 compareAnchorsText(current, anchorInfo) &&
106 !Utils_1.Utils.contains(differentHrefSameText, anchorInfo)) {
107 differentHrefSameText.push(anchorInfo);
108 ctx.addFailureAt(anchorInfo.start, anchorInfo.width, exports.DIFFERENT_HREF_DIFFERENT_TEXT_FAILURE_STRING + firstPosition(current));
109 }
110 });
111 };
112 while (anchorInfoList.length > 0) {
113 _loop_1();
114 }
115 }
116 function compareAnchorsText(anchor1, anchor2) {
117 var text1 = anchor1.text;
118 var text2 = anchor2.text;
119 var altText1 = anchor1.altText;
120 var altText2 = anchor2.altText;
121 if (ctx.options.ignoreCase) {
122 text1 = text1.toLowerCase();
123 text2 = text2.toLowerCase();
124 altText1 = altText1.toLowerCase();
125 altText2 = altText2.toLowerCase();
126 }
127 if (ctx.options.ignoreWhitespace === 'trim') {
128 text1 = text1.trim();
129 text2 = text2.trim();
130 altText1 = altText1.trim();
131 altText2 = altText2.trim();
132 }
133 if (ctx.options.ignoreWhitespace === 'all') {
134 var regex = /\s/g;
135 text1 = text1.replace(regex, '');
136 text2 = text2.replace(regex, '');
137 altText1 = altText1.replace(regex, '');
138 altText2 = altText2.replace(regex, '');
139 }
140 return text1 === text2 && altText1 === altText2;
141 }
142 function firstPosition(anchorInfo) {
143 var startPosition = ctx.sourceFile.getLineAndCharacterOfPosition(Math.min(anchorInfo.start, ctx.sourceFile.end));
144 var character = startPosition.character + 1;
145 var line = startPosition.line + 1;
146 return " First link at character: " + character + " line: " + line;
147 }
148 function validateAnchor(parent, openingElement) {
149 if (openingElement.tagName.getText() === 'a') {
150 var hrefAttribute = getAttribute(openingElement, 'href');
151 var anchorInfo = {
152 href: hrefAttribute ? JsxAttribute_1.getStringLiteral(hrefAttribute) || '' : '',
153 text: anchorText(parent),
154 altText: imageAlt(parent),
155 hasAriaHiddenCount: jsxElementAriaHidden(parent),
156 start: parent.getStart(),
157 width: parent.getWidth()
158 };
159 if (JsxAttribute_1.isEmpty(hrefAttribute)) {
160 ctx.addFailureAt(anchorInfo.start, anchorInfo.width, exports.MISSING_HREF_FAILURE_STRING);
161 }
162 if (anchorInfo.href === '#') {
163 ctx.addFailureAt(anchorInfo.start, anchorInfo.width, exports.NO_HASH_FAILURE_STRING);
164 }
165 if (anchorInfo.hasAriaHiddenCount > 0) {
166 ctx.addFailureAt(anchorInfo.start, anchorInfo.width, exports.ACCESSIBLE_HIDDEN_CONTENT_FAILURE_STRING);
167 }
168 if (anchorInfo.altText && anchorInfo.altText === anchorInfo.text) {
169 ctx.addFailureAt(anchorInfo.start, anchorInfo.width, exports.UNIQUE_ALT_FAILURE_STRING);
170 }
171 var anchorInfoTextLength = anchorInfo.text ? anchorInfo.text.length : 0;
172 var anchorImageAltTextLength = anchorInfo.altText ? anchorInfo.altText.length : 0;
173 if (anchorRole(openingElement) === 'link' && anchorInfoTextLength < 4 && anchorImageAltTextLength < 4) {
174 ctx.addFailureAt(anchorInfo.start, anchorInfo.width, exports.LINK_TEXT_TOO_SHORT_FAILURE_STRING);
175 }
176 anchorInfoList.push(anchorInfo);
177 }
178 }
179 function getAttribute(openingElement, attributeName) {
180 var attributes = JsxAttribute_1.getJsxAttributesFromJsxElement(openingElement);
181 return attributes[attributeName];
182 }
183 function anchorText(root, isChild) {
184 if (isChild === void 0) { isChild = false; }
185 var title = '';
186 if (root === undefined) {
187 return title;
188 }
189 else if (root.kind === ts.SyntaxKind.JsxElement) {
190 var jsxElement = root;
191 jsxElement.children.forEach(function (child) {
192 title += anchorText(child, true);
193 });
194 }
195 else if (root.kind === ts.SyntaxKind.JsxText) {
196 var jsxText = root;
197 title += jsxText.getText();
198 }
199 else if (root.kind === ts.SyntaxKind.StringLiteral) {
200 var literal = root;
201 title += literal.text;
202 }
203 else if (root.kind === ts.SyntaxKind.JsxExpression) {
204 var expression = root;
205 title += anchorText(expression.expression);
206 }
207 else if (isChild && root.kind === ts.SyntaxKind.JsxSelfClosingElement) {
208 var jsxSelfClosingElement = root;
209 if (jsxSelfClosingElement.tagName.getText() !== 'img') {
210 title += '<component>';
211 }
212 }
213 else if (root.kind !== ts.SyntaxKind.JsxSelfClosingElement) {
214 title += '<unknown>';
215 }
216 return title;
217 }
218 function anchorRole(root) {
219 var attributesInElement = JsxAttribute_1.getJsxAttributesFromJsxElement(root);
220 var roleProp = attributesInElement[ROLE_STRING];
221 return roleProp ? JsxAttribute_1.getStringLiteral(roleProp) : getImplicitRole_1.getImplicitRole(root);
222 }
223 function imageAltAttribute(openingElement) {
224 if (openingElement.tagName.getText() === 'img') {
225 var altAttribute = JsxAttribute_1.getStringLiteral(getAttribute(openingElement, 'alt'));
226 return altAttribute === undefined ? '<unknown>' : altAttribute;
227 }
228 return '';
229 }
230 function imageAlt(root) {
231 var altText = '';
232 if (root.kind === ts.SyntaxKind.JsxElement) {
233 var jsxElement = root;
234 altText += imageAltAttribute(jsxElement.openingElement);
235 jsxElement.children.forEach(function (child) {
236 altText += imageAlt(child);
237 });
238 }
239 if (root.kind === ts.SyntaxKind.JsxSelfClosingElement) {
240 var jsxSelfClosingElement = root;
241 altText += imageAltAttribute(jsxSelfClosingElement);
242 }
243 return altText;
244 }
245 function ariaHiddenAttribute(openingElement) {
246 return getAttribute(openingElement, 'aria-hidden') === undefined;
247 }
248 function jsxElementAriaHidden(root) {
249 var hasAriaHiddenCount = 0;
250 if (root.kind === ts.SyntaxKind.JsxElement) {
251 var jsxElement = root;
252 hasAriaHiddenCount += ariaHiddenAttribute(jsxElement.openingElement) ? 0 : 1;
253 jsxElement.children.forEach(function (child) {
254 hasAriaHiddenCount += jsxElementAriaHidden(child);
255 });
256 }
257 if (root.kind === ts.SyntaxKind.JsxSelfClosingElement) {
258 var jsxSelfClosingElement = root;
259 hasAriaHiddenCount += ariaHiddenAttribute(jsxSelfClosingElement) ? 0 : 1;
260 }
261 return hasAriaHiddenCount;
262 }
263 function cb(node) {
264 if (tsutils.isJsxSelfClosingElement(node)) {
265 validateAnchor(node, node);
266 }
267 else if (tsutils.isJsxElement(node)) {
268 validateAnchor(node, node.openingElement);
269 }
270 return ts.forEachChild(node, cb);
271 }
272 ts.forEachChild(ctx.sourceFile, cb);
273 validateAllAnchors();
274}
275var templateObject_1;
276//# sourceMappingURL=reactA11yAnchorsRule.js.map
\No newline at end of file