Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

ember-template-lint

Package Overview
Dependencies
Maintainers
1
Versions
215
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ember-template-lint - npm Package Compare versions

Comparing version 0.5.10 to 0.5.11

lib/helpers/is-interactive-element.js

17

CHANGELOG.md
Changelog
=========
## v0.5.11
- Add internal helper for determining if a given element is an interactive element.
- Update `nested-interactive` rule to use the new `isInteractiveElement` helper function.
- Change `nested-interactive` configuration. Now uses an object (instead of an array). Example:
```js
rules: {
'nested-interactive': {
ignoredTags: ['a', 'button'], // list of tag names to ignore
ignoreTabindex: true, // ignore the tabindex check
ignoreUsemapAttribute: ['img', 'object'], // ignore `usemap` check for specific tag names
additionalInteractiveTags: ['some-custom-tag'], // not sure this is needed, but it seams neat :P
}
}
```
## v0.5.10

@@ -5,0 +22,0 @@

56

lib/helpers/ast-node-info.js
'use strict';
function AstNodeInfo() {}
AstNodeInfo.isConfigurationHtmlComment = function(node) {
function isConfigurationHtmlComment(node) {
return node.type === 'CommentStatement' && node.value.trim().indexOf('template-lint ') === 0;
};
}
AstNodeInfo.isNonConfigurationHtmlComment = function(node) {
function isNonConfigurationHtmlComment(node) {
return node.type === 'CommentStatement' && node.value.trim().indexOf('template-lint ') !== 0;
};
}
AstNodeInfo.isTextNode = function(node) {
function isTextNode(node) {
return node.type === 'TextNode';
};
}
AstNodeInfo.isElementNode = function(node) {
return node.type === 'ElementNode';
};
function isElementNode(node) {
return node && node.type === 'ElementNode';
}
AstNodeInfo.isBlockStatement = function(node) {
function isComponentNode(node) {
return node && node.type === 'ComponentNode';
}
function isBlockStatement(node) {
return node.type === 'BlockStatement';
}
function hasAttribute(node, attributeName) {
var attribute = findAttribute(node, attributeName);
return !!attribute;
}
function findAttribute(node, attributeName) {
for (var i = 0; i < node.attributes.length; i++) {
var attribute = node.attributes[i];
if (attribute.name === attributeName) {
return attribute;
}
}
}
module.exports = {
isConfigurationHtmlComment: isConfigurationHtmlComment,
isNonConfigurationHtmlComment: isNonConfigurationHtmlComment,
isTextNode: isTextNode,
isElementNode: isElementNode,
isComponentNode: isComponentNode,
isBlockStatement: isBlockStatement,
hasAttribute: hasAttribute,
findAttribute: findAttribute
};
module.exports = AstNodeInfo;

@@ -30,6 +30,6 @@ 'use strict';

this._log = log;
this.ruleName = ruleName;
this.severity = defaultSeverity;
this.config = this.parseConfig(config);
this._log = log;
}

@@ -36,0 +36,0 @@

@@ -20,17 +20,72 @@ 'use strict';

var buildPlugin = require('./base');
var isInteractiveElement = require('../helpers/is-interactive-element');
var ARRAY_DEPRECATION_MESSAGE = 'Specifying an array as the configurate for the `nested-interactive` rule is deprecated and will be removed in future versions. Please update `.template-lintrc.js` to use the newer object format.';
function configValid(config) {
for (var key in config) {
var value = config[key];
switch (key) {
case 'ignoredTags':
case 'additionalInteractiveTags':
if (!Array.isArray(value)) {
return false;
}
break;
case 'ignoreTabindex':
case 'ignoreUsemapAttribute':
if (typeof value !== 'boolean') {
return false;
}
break;
default:
return false;
}
}
return true;
}
function convertConfigArrayToObject(config) {
var base = {
ignoredTags: [],
ignoreTabindex: false,
ignoreUsemapAttribute: false
};
for (var i = 0; i < config.length; i++) {
var value = config[i];
switch (value) {
case 'tabindex':
base.ignoreTabindex = true;
break;
case 'usemap':
base.ignoreUsemapAttribute = true;
break;
default:
base.ignoredTags.push(value);
}
}
return base;
}
module.exports = function(addonContext) {
var LogNestedInteractive = buildPlugin(addonContext, 'nested-interactive');
function isElementNode(node) {
return node.type === 'ElementNode';
}
LogNestedInteractive.prototype.parseConfig = function(config) {
var configType = typeof config;
var errorMessage = 'The nested-interactive rule accepts one of the following values.\n ' +
' * boolean - `true` to enable / `false` to disable\n' +
' * array -- an array of strings to whitelist\n' +
'\nYou specified `' + JSON.stringify(config) + '`';
var errorMessage = [
'The nested-interactive rule accepts one of the following values.',
' * boolean - `true` to enable / `false` to disable',
' * object - Containing the following values:',
' * `ignoredTags` - An array of element tag names that should be whitelisted. Default to `[]`.',
' * `ignoreTabindex` - When `true` tabindex will be ignored. Defaults to `false`.',
' * `ignoreUsemapAttribute` - When `true` ignores the `usemap` attribute on `img` and `object` elements. Defaults `false`.',
' * `additionalInteractiveTags` - An array of element tag names that should also be considered as interactive. Defaults to `[]`.',
'You specified `' + JSON.stringify(config) + '`'
].join('\n');

@@ -41,4 +96,11 @@ switch (configType) {

case 'object':
if (Array.isArray(config)) {
if (configValid(config)) {
return config;
} else if (Array.isArray(config)) {
this.log({
message: ARRAY_DEPRECATION_MESSAGE,
source: JSON.stringify(config),
severity: 1
});
return convertConfigArrayToObject(config);
} else {

@@ -58,15 +120,3 @@ throw new Error(errorMessage);

} else {
return [
'a',
'button',
'details',
'embed',
'iframe',
'img',
'input',
'object',
'select',
'tabindex',
'textarea'
];
return [];
}

@@ -76,189 +126,90 @@ };

LogNestedInteractive.prototype.visitors = function() {
var pluginContext = this;
pluginContext._parentInteractiveNode = null;
this._parentInteractiveNode = null;
return {
CommentStatement: function(node) {
if (node.value.indexOf('template-lint') > -1) {
pluginContext._processConfigNode(node);
var visitor = {
enter: function(node) {
var isInteractive = isInteractiveElement(node);
if (this.isCustomInteractiveElement(node)) {
isInteractive = true;
}
},
ElementNode: {
enter: function(node) {
if (pluginContext.isDisabled()) { return; }
if (!isInteractive) { return; }
if (this.isInteractiveExcluded(node)) { return; }
var isInteractive = pluginContext.isInteractiveElement(node);
if (this._parentInteractiveNode) {
this.log({
message: this.getLogMessage(node, this._parentInteractiveNode),
line: node.loc && node.loc.start.line,
column: node.loc && node.loc.start.column,
source: this.sourceForNode(node)
});
} else {
this._parentInteractiveNode = node;
}
},
if (!isInteractive) { return; }
if (pluginContext._parentInteractiveNode) {
pluginContext.log({
message: pluginContext.getLogMessage(node, pluginContext._parentInteractiveNode),
line: node.loc && node.loc.start.line,
column: node.loc && node.loc.start.column,
source: pluginContext.sourceForNode(node)
});
} else {
pluginContext._parentInteractiveNode = node;
}
},
exit: function(node) {
if (pluginContext._parentInteractiveNode === node) {
pluginContext._parentInteractiveNode = null;
}
exit: function(node) {
if (this._parentInteractiveNode === node) {
this._parentInteractiveNode = null;
}
}
};
};
LogNestedInteractive.prototype.hasNodeHaveAttribute = function(node, attributeName) {
return node.attributes.some(function(attribute) {
return attribute.name === attributeName;
});
return {
ElementNode: visitor,
ComponentNode: visitor
};
};
LogNestedInteractive.prototype.isNodeLink = function(node, whitelistTests) {
return (
whitelistTests.indexOf('a') !== -1 &&
node.tag === 'a' && this.hasNodeHaveAttribute(node, 'href')
);
};
LogNestedInteractive.prototype.isCustomInteractiveElement = function(node) {
LogNestedInteractive.prototype.isNodeNotHiddenInput = function(node, whitelistTests) {
if (whitelistTests.indexOf('input') === -1) {
var additionalInteractiveTags = this.config.additionalInteractiveTags || [];
if (additionalInteractiveTags.indexOf(node.tag) > -1) {
return true;
} else {
return false;
}
if (node.tag === 'input') {
var isInputTypeHidden = node.attributes.some(function(attribute) {
return (
attribute.name === 'type' &&
attribute.value &&
attribute.value.chars === 'hidden'
);
});
// NOTE: `!`
if (!isInputTypeHidden) {
return true;
}
}
return false;
};
LogNestedInteractive.prototype.hasNodeHaveTabIndex = function(node, whitelistTests) {
return (
whitelistTests.indexOf('tabindex') !== -1 &&
this.hasNodeHaveAttribute(node, 'tabindex')
);
};
LogNestedInteractive.prototype.isInteractiveExcluded = function(node) {
var reason = isInteractiveElement.reason(node);
var ignoredTags = this.config.ignoredTags || [];
var ignoreTabindex = this.config.ignoreTabindex;
var ignoreUsemapAttribute = this.config.ignoreUsemapAttribute;
LogNestedInteractive.prototype.isNodeUseMapInteractive = function(node, whitelistTests) {
return (
whitelistTests.indexOf(node.tag) !== -1 &&
(node.tag === 'img' || node.tag === 'object') &&
this.hasNodeHaveAttribute(node, 'usemap')
);
};
LogNestedInteractive.prototype.isNodeInteractiveTag = function(node, whitelistTests) {
var interactiveTags = [
'button',
'details',
'embed',
'iframe',
'select',
'textarea'
];
return interactiveTags.some(function(tagName) {
return whitelistTests.indexOf(tagName) !== -1 && tagName === node.tag;
});
};
/**
* NOTE: `<label>` was omitted due to the ability nesting a label with an input tag.
* NOTE: `<audio>` and `<video>` also omitted because use legacy browser support
* there is a need to use it nested with `<object>` and `<a>`
*/
LogNestedInteractive.prototype.isInteractiveElement = function(node) {
var whitelistTests = this.getConfigWhiteList();
if (!node) {
return false;
}
if (this.isNodeInteractiveTag(node, whitelistTests)) {
if (ignoredTags.indexOf(node.tag) > -1) {
return true;
}
if (this.isNodeLink(node, whitelistTests)) {
if (ignoreTabindex && reason.indexOf('tabindex') > -1) {
return true;
}
if (this.isNodeNotHiddenInput(node, whitelistTests)) {
if (ignoreUsemapAttribute && reason.indexOf('usemap') > -1) {
return true;
}
if (this.hasNodeHaveTabIndex(node, whitelistTests)) {
return true;
}
if (this.isNodeUseMapInteractive(node, whitelistTests)) {
return true;
}
return false;
};
LogNestedInteractive.prototype.getLogMessage = function(node, parentNode) {
var isParentHasTabIndexAttribute = this.hasNodeHaveAttribute(parentNode, 'tabindex');
var isParentHasUseMapAttribute = this.hasNodeHaveAttribute(parentNode, 'usemap');
var parentNodeError = '<' + parentNode.tag + '>';
var parentReason = isInteractiveElement.reason(parentNode);
var childReason = isInteractiveElement.reason(node);
if (isParentHasTabIndexAttribute) {
parentNodeError = 'an element with attribute `tabindex`';
} else if (isParentHasUseMapAttribute) {
parentNodeError = 'an element with attribute `usemap`';
// `reason` for `additionalInteractiveTags` would be `null`
// so we need to handle that and update the reason correctly
if (this.isCustomInteractiveElement(parentNode)) {
parentReason = '<' + parentNode.tag + '>';
}
var isChildHasTabIndexAttribute = this.hasNodeHaveAttribute(node, 'tabindex');
var isChildHasUseMapAttribute = this.hasNodeHaveAttribute(node, 'usemap');
var childNodeError = '<' + node.tag + '>';
if (isChildHasTabIndexAttribute) {
childNodeError = 'an element with attribute `tabindex`';
} else if (isChildHasUseMapAttribute) {
childNodeError = 'an element with attribute `usemap`';
if (this.isCustomInteractiveElement(node)) {
childReason = '<' + node.tag + '>';
}
return 'Do not use ' + childNodeError + ' inside ' + parentNodeError;
return 'Do not use ' + childReason + ' inside ' + parentReason;
};
LogNestedInteractive.prototype.findNestedInteractiveElements = function(node, parentInteractiveNode, whitelistTests) {
if (this.isInteractiveElement(parentInteractiveNode, whitelistTests)) {
if (this.isInteractiveElement(node, whitelistTests)) {
this.log({
message: this.getLogMessage(node, parentInteractiveNode),
line: node.loc && node.loc.start.line,
column: node.loc && node.loc.start.column,
source: this.sourceForNode(node)
});
return;
}
}
node.children
.filter(isElementNode)
.forEach(function(childNode) {
this.findNestedInteractiveElements(childNode, node, whitelistTests);
}, this);
};
return LogNestedInteractive;
};
module.exports.ARRAY_DEPRECATION_MESSAGE = ARRAY_DEPRECATION_MESSAGE;
{
"name": "ember-template-lint",
"version": "0.5.10",
"version": "0.5.11",
"description": "Lint your templates.",

@@ -5,0 +5,0 @@ "scripts": {

@@ -194,17 +194,8 @@ # ember-template-lint

* boolean -- `true` indicates all whitelist test will run, `false` indicates that the rule is disabled.
* array -- an array of whitelisted tests as the following
* object - Containing the following values:
* `ignoredTags` - An array of element tag names that should be whitelisted. Default to `[]`.
* `ignoreTabindex` - When `true` tabindex will be ignored. Defaults to `false`.
* `ignoreUsemapAttribute` - When `true` ignores the `usemap` attribute on `img` and `object` elements. Defaults `false`.
* `additionalInteractiveTags` - An array of element tag names that should also be considered as interactive. Defaults to `[]`.'
Whitelist of option in the configuration (are tags name or element attributes):
* `button`
* `details`
* `embed`
* `iframe`
* `img`
* `input`
* `object`
* `select`
* `textarea`
* `tabindex`
### Deprecations

@@ -211,0 +202,0 @@

'use strict';
var generateRuleTests = require('../../helpers/rule-test-harness');
var ARRAY_DEPRECATION_MESSAGE = require('../../../lib/rules/lint-nested-interactive').ARRAY_DEPRECATION_MESSAGE;

@@ -8,2 +9,3 @@ generateRuleTests({

config: true,

@@ -19,37 +21,20 @@

{
config: ['button', 'details', 'embed', 'iframe', 'img', 'input', 'object', 'select', 'tabindex', 'textarea'],
template: '<a href="/">button<a href="/">!</a></a>'
}, {
config: ['a', 'details', 'embed', 'iframe', 'img', 'input', 'object', 'select', 'tabindex', 'textarea'],
template: '<a href="/">button<button>!</button></a>'
}, {
config: ['a', 'details', 'embed', 'iframe', 'img', 'input', 'object', 'select', 'tabindex', 'textarea'],
template: '<button>button<button>!</button></button>'
}, {
config: ['a', 'button', 'details', 'embed', 'iframe', 'img', 'object', 'select', 'tabindex', 'textarea'],
template: '<button><input type="text"></button>'
}, {
config: ['a', 'button', 'embed', 'iframe', 'img', 'input', 'object', 'select', 'tabindex', 'textarea'],
template: '<button><details><summary>Some details</summary><p>!</p></details></button>'
}, {
config: ['a', 'button', 'details', 'iframe', 'img', 'input', 'object', 'select', 'tabindex', 'textarea'],
template: '<button><embed type="video/quicktime" src="movie.mov" width="640" height="480"></button>'
}, {
config: ['a', 'button', 'details', 'embed', 'img', 'input', 'object', 'select', 'tabindex', 'textarea'],
template: '<button><iframe src="/frame.html" width="640" height="480"></iframe></button>'
}, {
config: ['a', 'button', 'details', 'embed', 'iframe', 'img', 'input', 'object', 'tabindex', 'textarea'],
template: '<button><select></select></button>'
}, {
config: ['a', 'button', 'details', 'embed', 'iframe', 'img', 'input', 'object', 'select', 'tabindex'],
template: '<button><textarea></textarea></button>'
}, {
config: ['a', 'button', 'details', 'embed', 'iframe', 'img', 'input', 'object', 'select', 'textarea'],
template: '<div tabindex="1"><button></button></div>'
}, {
config: ['a', 'button', 'details', 'embed', 'iframe', 'input', 'object', 'select', 'tabindex', 'textarea'],
config: {
ignoredTags: ['button']
},
template: '<button><input></button>'
},
{
config: {
ignoreTabindex: true
},
template: '<button><div tabindex=-1></div></button>'
},
{
config: {
ignoreUsemapAttribute: true
},
template: '<button><img usemap=""></button>'
}, {
config: ['a', 'button', 'details', 'embed', 'iframe', 'img', 'input', 'select', 'tabindex', 'textarea'],
template: '<object usemap=""><button></button></object>'
}

@@ -60,3 +45,2 @@ ],

{
config: ['a'],
template: '<a href="/">button<a href="/">!</a></a>',

@@ -66,3 +50,3 @@

rule: 'nested-interactive',
message: 'Do not use <a> inside <a>',
message: 'Do not use an <a> element with the `href` attribute inside an <a> element with the `href` attribute',
moduleId: 'layout.hbs',

@@ -75,3 +59,2 @@ source: '<a href=\"/\">!</a>',

{
config: ['a', 'button'],
template: '<a href="/">button<button>!</button></a>',

@@ -81,3 +64,3 @@

rule: 'nested-interactive',
message: 'Do not use <button> inside <a>',
message: 'Do not use <button> inside an <a> element with the `href` attribute',
moduleId: 'layout.hbs',

@@ -90,3 +73,2 @@ source: '<button>!</button>',

{
config: ['a', 'button'],
template: '<button>button<a href="/">!</a></button>',

@@ -96,3 +78,3 @@

rule: 'nested-interactive',
message: 'Do not use <a> inside <button>',
message: 'Do not use an <a> element with the `href` attribute inside <button>',
moduleId: 'layout.hbs',

@@ -105,3 +87,2 @@ source: '<a href=\"/\">!</a>',

{
config: ['button'],
template: '<button>button<button>!</button></button>',

@@ -119,3 +100,2 @@

{
config: ['button', 'input'],
template: '<button><input type="text"></button>',

@@ -133,3 +113,2 @@

{
config: ['button', 'details'],
template: '<button><details><summary>Some details</summary><p>!</p></details></button>',

@@ -147,3 +126,2 @@

{
config: ['button', 'embed'],
template: '<button><embed type="video/quicktime" src="movie.mov" width="640" height="480"></button>',

@@ -161,3 +139,2 @@

{
config: ['button', 'iframe'],
template: '<button><iframe src="/frame.html" width="640" height="480"></iframe></button>',

@@ -175,3 +152,2 @@

{
config: ['button', 'select'],
template: '<button><select></select></button>',

@@ -189,3 +165,2 @@

{
config: ['button', 'textarea'],
template: '<button><textarea></textarea></button>',

@@ -203,3 +178,2 @@

{
config: ['button', 'tabindex'],
template: '<div tabindex="1"><button></button></div>',

@@ -209,3 +183,3 @@

rule: 'nested-interactive',
message: 'Do not use <button> inside an element with attribute `tabindex`',
message: 'Do not use <button> inside an element with the `tabindex` attribute',
moduleId: 'layout.hbs',

@@ -218,3 +192,2 @@ source: '<button></button>',

{
config: ['button', 'tabindex'],
template: '<button><div tabindex="1"></div></button>',

@@ -224,3 +197,3 @@

rule: 'nested-interactive',
message: 'Do not use an element with attribute `tabindex` inside <button>',
message: 'Do not use an element with the `tabindex` attribute inside <button>',
moduleId: 'layout.hbs',

@@ -233,3 +206,2 @@ source: '<div tabindex=\"1\"></div>',

{
config: ['button', 'img'],
template: '<button><img usemap=""></button>',

@@ -239,3 +211,3 @@

rule: 'nested-interactive',
message: 'Do not use an element with attribute `usemap` inside <button>',
message: 'Do not use an <img> element with the `usemap` attribute inside <button>',
moduleId: 'layout.hbs',

@@ -248,3 +220,2 @@ source: '<img usemap=\"\">',

{
config: ['button', 'object'],
template: '<object usemap=""><button></button></object>',

@@ -254,3 +225,3 @@

rule: 'nested-interactive',
message: 'Do not use <button> inside an element with attribute `usemap`',
message: 'Do not use <button> inside an <object> element with the `usemap` attribute',
moduleId: 'layout.hbs',

@@ -261,4 +232,55 @@ source: '<button></button>',

}
},
{
config: {
additionalInteractiveTags: ['my-special-input']
},
template: '<button><my-special-input></my-special-input></button>',
result: {
rule: 'nested-interactive',
message: 'Do not use <my-special-input> inside <button>',
moduleId: 'layout.hbs',
source: '<my-special-input></my-special-input>',
line: 1,
column: 8
}
},
// deprecated
{
config: ['button'],
template: '<button><input></button>',
result: {
rule: 'nested-interactive',
message: ARRAY_DEPRECATION_MESSAGE,
source: '["button"]',
severity: 1
}
},
{
config: ['tabindex'],
template: '<button><div tabindex=-1></div></button>',
result: {
rule: 'nested-interactive',
message: ARRAY_DEPRECATION_MESSAGE,
moduleId: 'layout.hbs',
source: '["tabindex"]',
severity: 1
}
},
{
config: ['usemap'],
template: '<button><img usemap=""></button>',
result: {
rule: 'nested-interactive',
message: ARRAY_DEPRECATION_MESSAGE,
source: '["usemap"]',
severity: 1
}
}
]
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc