ember-template-lint
Version:
Linter for Ember or Handlebars templates.
158 lines (129 loc) • 3.94 kB
JavaScript
const { builders } = require('ember-template-recast');
const AstNodeInfo = require('../helpers/ast-node-info');
const createErrorMessage = require('../helpers/create-error-message');
const Rule = require('./base');
/**
Disallow usage of `<a target="_blank">` without an `rel="noopener"` attribute.
Good:
```
<a href="/some/where" target="_blank" rel="noopener"></a>
```
Bad:
```
<a href="/some/where" target="_blank"></a>
```
*/
const DEFAULT_CONFIG = {
regexp: /no(opener|referrer)/,
message: 'links with target="_blank" must have rel="noopener"',
configType: 'default',
};
const STRICT_CONFIG = {
regexp: /(.*\s)?noopener\s(.*\s)?noreferrer(\s.*)?|(.*\s)?noreferrer\s(.*\s)?noopener(\s.*)?/,
message:
'links with target="_blank" must have rel="noopener noreferrer" or rel="noreferrer noopener"',
configType: 'strict',
};
module.exports = class LinkRelNoopener extends Rule {
parseConfig(config) {
let configType = typeof config;
switch (configType) {
case 'boolean':
return config ? DEFAULT_CONFIG : false;
case 'string':
if (config === 'strict') {
return STRICT_CONFIG;
}
break;
case 'undefined':
return false;
}
let errorMessage = createErrorMessage(
this.ruleName,
[
' * boolean - `true` to enable / `false` to disable',
' * string -- `strict` to enable validation for both noopener AND noreferrer',
],
config
);
throw new Error(errorMessage);
}
visitor() {
return {
ElementNode(node) {
let isLink = AstNodeInfo.isLinkElement(node);
if (!isLink) {
return;
}
let targetBlank = hasTargetBlank(node);
if (!targetBlank) {
return;
}
let relNoopener = hasRelNoopener(node, this.config.regexp);
if (relNoopener) {
return;
}
if (this.mode === 'fix' && isFixable(node)) {
return fix(node, this.config);
}
this.log({
isFixable: isFixable(node),
message: this.config.message,
line: node.loc && node.loc.start.line,
column: node.loc && node.loc.start.column,
source: this.sourceForNode(node),
});
},
};
}
};
function isFixable(elementNode) {
let oldRel = AstNodeInfo.findAttribute(elementNode, 'rel');
return !oldRel || AstNodeInfo.isTextNode(oldRel.value);
}
function fix(node, config) {
let oldRel = AstNodeInfo.findAttribute(node, 'rel');
let oldRelValue =
oldRel && AstNodeInfo.isTextNode(oldRel.value)
? // normalize whitespace between values
oldRel.value.chars.trim().replace(/\s+/g, '')
: '';
// remove existing instances of noopener/noreferrer so we can add them back in
// the order the rule suggests in the error message
let newRelValue = oldRelValue.replace(/(noopener|noreferrer)/g, '');
newRelValue = `${newRelValue} ${
config.configType === 'default' ? 'noopener' : 'noopener noreferrer'
}`;
let oldRelIndex = oldRel ? node.attributes.indexOf(oldRel) : null;
let newRelNode = builders.attr('rel', builders.text(newRelValue.trim()));
if (oldRel) {
node.attributes.splice(oldRelIndex, 1, newRelNode);
} else {
node.attributes.push(newRelNode);
}
}
function hasTargetBlank(node) {
let targetAttribute = AstNodeInfo.findAttribute(node, 'target');
if (!targetAttribute) {
return false;
}
switch (targetAttribute.value.type) {
case 'TextNode':
return targetAttribute.value.chars === '_blank';
default:
return false;
}
}
function hasRelNoopener(node, regexp) {
let relAttribute = AstNodeInfo.findAttribute(node, 'rel');
if (!relAttribute) {
return false;
}
switch (relAttribute.value.type) {
case 'TextNode':
return regexp.test(relAttribute.value.chars);
default:
return false;
}
}
;