mip-validator
Version:
213 lines (196 loc) • 5.54 kB
JavaScript
const _ = require('lodash')
const regexSyntax = /^(!?)\/(.*)\/(\w*)$/
const logger = require('./logger.js')('mip-validator:matcher')
/*
* convert regex-like string to regex
* @param {str} the regex-like string to convert
*/
function stringToRegex(str) {
var match = str.match(regexSyntax)
if (!match) {
return null
}
var regex = new RegExp(match[2], match[3])
if (match[1] === '!') {
regex.negate = true
}
return regex
}
/*
* match string with string rule
* @param {String} src the string to match
* @param {String} target the string or regex-like string to match with
*/
function matchValue(src, target) {
var re
if ((re = stringToRegex(target))) {
var result = re.test(src)
return re.negate ? (!result) : result
} else {
return src === target
}
}
/*
* object match: match src with target
* @param {Object} src the object to match
* @param {Object} target the object to match with
* legal:
* match({
* id: 'modal-user'
* }, {
* id: '/^modal-.+$/'
* });
*/
function match(src, target) {
var ret = true
_.forOwn(target, (value, key) => {
if (!matchValue(src[key], value)) {
ret = false
}
})
return ret
}
/*
* attributes match
* @param {ASTNode} node the node of which attributes will be matched
* @param {Object} target the attribute list object to match with
* legal:
* matchAttrs(node, {
* style: 'color:red',
* id: '/mip-.+/'
* });
*/
function matchAttrs(node, target) {
var attrSet = _.chain(node.attrs)
.map(attr => [attr.name, attr.value])
.fromPairs()
.value()
return match(attrSet, target)
}
/*
* match ancestor name
* @param {ASTNode} node the node of which parent will be matched
* @param {String} ancestorNodeName string or regex-like string to match with
* legal:
* matchAncestor(node, 'form');
* matchAncestor(node, '/form|div|section/'
*/
function matchAncestor(node, ancestorNodeName) {
// match_ancestor disabled
if (!ancestorNodeName) return true
while ((node = node.parentNode)) {
if (matchValue(node.nodeName, ancestorNodeName)) return true
}
return false
}
/*
* match parent name
* @param {ASTNode} node the node of which parent will be matched
* @param {String} parentNodeName string or regex-like string to match with
* legal:
* matchParent(node, 'form');
* matchParent(node, '/form|div|section/'
*/
function matchParent(node, parentNodeName) {
// match disabled
if (!parentNodeName) return true
logger.debug('matching parent:', parentNodeName)
return matchValue(node.parentNode.nodeName, parentNodeName)
}
/*
* match descendant node name
* @param {ASTNode} node the node of which parent will be matched
* @param {String} descendantNodeName string or regex-like string to match with
* legal:
* matchDescendant(node, 'form');
* matchDescendant(node, '/form|div|section/'
*/
function matchDescendant(node, descendantNodeName) {
// match disabled
if (!descendantNodeName) return true
// is there a match?
return dfsUntil(node, child => matchValue(child.nodeName, descendantNodeName))
}
/*
* nomatch descendant node name
* @param {ASTNode} node the node of which parent will be matched
* @param {String} descendantNodeName string or regex-like string to match with
* legal:
* nomatchDescendant(node, 'form');
* nomatchDescendant(node, '/form|div|section/'
*/
function nomatchDescendant(node, descendantNodeName) {
logger.debug('nomatching descendant:', descendantNodeName)
// match disabled, pass
if (!descendantNodeName) return true
// is there a match?
return !matchDescendant(node, descendantNodeName)
}
function dfsUntil(node, predict) {
// #text node do NOT have childNodes defined
return predict(node) || (node.childNodes || []).some(child => dfsUntil(child, predict))
}
/*
* Create a ASTNode for given nodeName and attribute object
*/
function createNode(nodeName, attrsObj) {
return {
nodeName,
attrs: _.chain(attrsObj)
.toPairs()
.map(pair => ({
name: pair[0],
value: pair[1]
}))
.value()
}
}
/*
* Generate a fingerprint for given nodeName and attributes
* legal:
* fingerprintByObject('div', {id: 'modal'}); // returns: <div id="modal">
*/
function fingerprintByObject(nodeName, attrsObj) {
var tag = createNode(nodeName, attrsObj)
return fingerprintByTag(tag)
}
/*
* Generate a fingerprint for given node
* @param {ASTNode} node
*/
function fingerprintByTag(node) {
var attrStr = _.chain(node.attrs)
.map(attr => `${attr.name}="${attr.value}"`)
.join(' ')
.value()
if (attrStr.length) {
attrStr = ' ' + attrStr
}
return `<${node.nodeName}${attrStr}>`
}
/*
* Match tagnames from the given HTML
* @param {Array} tagNames
* @param {String} html
* legal: matchTagNames(['div', 'head', 'iframe'], '<div><iframe></div>')
*/
function matchTagNames(tagNames, html) {
var tagsStr = tagNames.join('|')
var re = new RegExp(`<\\s*(${tagsStr})(?:\\s+[^>]*)*>`, 'ig')
return (html || '').match(re) || []
}
module.exports = {
match,
matchAttrs,
matchValue,
regexSyntax,
fingerprintByTag,
createNode,
fingerprintByObject,
stringToRegex,
matchTagNames,
matchParent,
matchAncestor,
nomatchDescendant,
matchDescendant
}