'use strict';
const utils = require( '../utils.js' );
// HTML regex (modified from jQuery)
const rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*)$/;
// Single tag regex (from jQuery)
const rsingleTag = /^<([a-z][^/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;
const rsingleTagMinimal = /^<([a-z][^/\0>:\x20\t\r\n\f]*)>$/i;
const rsingleTagSelfClosing = /^<([a-z][^/\0>:\x20\t\r\n\f]*)\/>$/i;
function allLiteral( node ) {
if ( node.type === 'BinaryExpression' ) {
return allLiteral( node.left ) && allLiteral( node.right );
} else {
return node.type === 'Literal';
}
}
function joinLiterals( node ) {
if ( node.type === 'BinaryExpression' ) {
return joinLiterals( node.left ) + joinLiterals( node.right );
}
/* istanbul ignore else */
if ( node.type === 'Literal' ) {
return node.value;
}
/* istanbul ignore next */
throw new Error( 'Non-literal node passed to joinLiteral' );
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'Disallows parsing of HTML literal strings using either the jQuery method [`$()`](https://api.jquery.com/jquery/) or ' +
utils.jQueryGlobalLink( 'parseHTML' ) + '. ' +
'Single tags are still allowed for creating new nodes as these don\'t tirgger the HTML parser. ' +
'DOM build and manipulation methods should be used instead.\n\n' +
'The format of single tags can be specified using the `singleTagStyle` option:\n' +
'* `"minimal"` (default) no whitespace or self-closing i.e. `
`\n' +
'* `"self-closing"` no whitespace and self-closing i.e. `
`\n' +
'* `"any"` no style enforced'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
singleTagStyle: {
enum: [ 'minimal', 'self-closing', 'any' ]
}
},
additionalProperties: false
}
]
},
create: function ( context ) {
return {
'CallExpression:exit': function ( node ) {
let allowSingle,
message = 'Prefer DOM building to parsing HTML literals';
if ( node.callee.type === 'Identifier' ) {
if ( !(
utils.isjQueryConstructor( context, node.callee.name ) &&
node.arguments[ 0 ] &&
(
node.arguments[ 0 ].type === 'Literal' ||
node.arguments[ 0 ].type === 'BinaryExpression'
)
) ) {
return;
}
allowSingle = true;
} else if ( node.callee.type === 'MemberExpression' ) {
if (
utils.isjQueryConstructor( context, node.callee.object.name ) &&
node.callee.property.name === 'parseHTML'
) {
allowSingle = false;
} else if (
[ 'html', 'append', 'add' ].includes( node.callee.property.name ) &&
utils.isjQuery( context, node )
) {
allowSingle = true;
} else {
return;
}
} else {
return;
}
let expectedTag;
const arg = node.arguments[ 0 ];
if ( allowSingle ) {
const value = arg && allLiteral( arg ) && joinLiterals( arg );
if ( !( typeof value === 'string' && value ) || !rquickExpr.exec( value ) ) {
// Empty or non-string, or non-HTML
return;
}
let match;
if ( ( match = rsingleTag.exec( value ) ) ) {
// Single tag
const singleTagStyle = ( context.options[ 0 ] && context.options[ 0 ].singleTagStyle ) || 'minimal';
if ( singleTagStyle === 'minimal' ) {
if ( !rsingleTagMinimal.exec( value ) ) {
expectedTag = '<' + match[ 1 ] + '>';
message = 'Single tag must use the format: ' + expectedTag;
} else {
return;
}
} else if ( singleTagStyle === 'self-closing' ) {
if ( !rsingleTagSelfClosing.exec( value ) ) {
expectedTag = '<' + match[ 1 ] + '/>';
message = 'Single tag must use the format: ' + expectedTag;
} else {
return;
}
} else {
// singleTagStyle === 'any'
return;
}
}
} else if ( !( arg && allLiteral( arg ) ) ) {
// Non literals passed to $.parseHTML
return;
}
context.report( {
node: node,
message: message,
fix: function ( fixer ) {
if ( expectedTag ) {
return fixer.replaceText( arg, '"' + expectedTag + '"' );
}
return null;
}
} );
}
};
}
};