|
import { warn } from '../util'; |
|
import * as is from '../is'; |
|
import exprs from './expressions'; |
|
import newQuery from './new-query'; |
|
import Type from './type'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
const consumeExpr = ( remaining ) => { |
|
let expr; |
|
let match; |
|
let name; |
|
|
|
for( let j = 0; j < exprs.length; j++ ){ |
|
let e = exprs[ j ]; |
|
let n = e.name; |
|
|
|
let m = remaining.match( e.regexObj ); |
|
|
|
if( m != null ){ |
|
match = m; |
|
expr = e; |
|
name = n; |
|
|
|
let consumed = m[0]; |
|
remaining = remaining.substring( consumed.length ); |
|
|
|
break; |
|
} |
|
} |
|
|
|
return { |
|
expr: expr, |
|
match: match, |
|
name: name, |
|
remaining: remaining |
|
}; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const consumeWhitespace = ( remaining ) => { |
|
let match = remaining.match( /^\s+/ ); |
|
|
|
if( match ){ |
|
let consumed = match[0]; |
|
remaining = remaining.substring( consumed.length ); |
|
} |
|
|
|
return remaining; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
const parse = function( selector ){ |
|
let self = this; |
|
|
|
let remaining = self.inputText = selector; |
|
|
|
let currentQuery = self[0] = newQuery(); |
|
self.length = 1; |
|
|
|
remaining = consumeWhitespace( remaining ); |
|
|
|
for( ;; ){ |
|
let exprInfo = consumeExpr( remaining ); |
|
|
|
if( exprInfo.expr == null ){ |
|
warn( 'The selector `' + selector + '`is invalid' ); |
|
return false; |
|
} else { |
|
let args = exprInfo.match.slice( 1 ); |
|
|
|
|
|
let ret = exprInfo.expr.populate( self, currentQuery, args ); |
|
|
|
if( ret === false ){ |
|
return false; |
|
} else if( ret != null ){ |
|
currentQuery = ret; |
|
} |
|
} |
|
|
|
remaining = exprInfo.remaining; |
|
|
|
|
|
if( remaining.match( /^\s*$/ ) ){ |
|
break; |
|
} |
|
} |
|
|
|
let lastQ = self[self.length - 1]; |
|
|
|
if( self.currentSubject != null ){ |
|
lastQ.subject = self.currentSubject; |
|
} |
|
|
|
lastQ.edgeCount = self.edgeCount; |
|
lastQ.compoundCount = self.compoundCount; |
|
|
|
for( let i = 0; i < self.length; i++ ){ |
|
let q = self[i]; |
|
|
|
|
|
if( q.compoundCount > 0 && q.edgeCount > 0 ){ |
|
warn( 'The selector `' + selector + '` is invalid because it uses both a compound selector and an edge selector' ); |
|
return false; |
|
} |
|
|
|
if( q.edgeCount > 1 ){ |
|
warn( 'The selector `' + selector + '` is invalid because it uses multiple edge selectors' ); |
|
return false; |
|
} else if( q.edgeCount === 1 ){ |
|
warn( 'The selector `' + selector + '` is deprecated. Edge selectors do not take effect on changes to source and target nodes after an edge is added, for performance reasons. Use a class or data selector on edges instead, updating the class or data of an edge when your app detects a change in source or target nodes.' ); |
|
} |
|
} |
|
|
|
return true; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export const toString = function(){ |
|
if( this.toStringCache != null ){ |
|
return this.toStringCache; |
|
} |
|
|
|
let clean = function( obj ){ |
|
if( obj == null ){ |
|
return ''; |
|
} else { |
|
return obj; |
|
} |
|
}; |
|
|
|
let cleanVal = function( val ){ |
|
if( is.string( val ) ){ |
|
return '"' + val + '"'; |
|
} else { |
|
return clean( val ); |
|
} |
|
}; |
|
|
|
let space = ( val ) => { |
|
return ' ' + val + ' '; |
|
}; |
|
|
|
let checkToString = ( check, subject ) => { |
|
let { type, value } = check; |
|
|
|
switch( type ){ |
|
case Type.GROUP: { |
|
let group = clean( value ); |
|
|
|
return group.substring( 0, group.length - 1 ); |
|
} |
|
|
|
case Type.DATA_COMPARE: { |
|
let { field, operator } = check; |
|
|
|
return '[' + field + space( clean( operator ) ) + cleanVal( value ) + ']'; |
|
} |
|
|
|
case Type.DATA_BOOL: { |
|
let { operator, field } = check; |
|
|
|
return '[' + clean( operator ) + field + ']'; |
|
} |
|
|
|
case Type.DATA_EXIST: { |
|
let { field } = check; |
|
|
|
return '[' + field + ']'; |
|
} |
|
|
|
case Type.META_COMPARE: { |
|
let { operator, field } = check; |
|
|
|
return '[[' + field + space( clean( operator ) ) + cleanVal( value ) + ']]'; |
|
} |
|
|
|
case Type.STATE: { |
|
return value; |
|
} |
|
|
|
case Type.ID: { |
|
return '#' + value; |
|
} |
|
|
|
case Type.CLASS: { |
|
return '.' + value; |
|
} |
|
|
|
case Type.PARENT: |
|
case Type.CHILD: { |
|
return queryToString(check.parent, subject) + space('>') + queryToString(check.child, subject); |
|
} |
|
|
|
case Type.ANCESTOR: |
|
case Type.DESCENDANT: { |
|
return queryToString(check.ancestor, subject) + ' ' + queryToString(check.descendant, subject); |
|
} |
|
|
|
case Type.COMPOUND_SPLIT: { |
|
let lhs = queryToString(check.left, subject); |
|
let sub = queryToString(check.subject, subject); |
|
let rhs = queryToString(check.right, subject); |
|
|
|
return lhs + (lhs.length > 0 ? ' ' : '') + sub + rhs; |
|
} |
|
|
|
case Type.TRUE: { |
|
return ''; |
|
} |
|
} |
|
}; |
|
|
|
let queryToString = ( query, subject ) => { |
|
return query.checks.reduce((str, chk, i) => { |
|
return str + (subject === query && i === 0 ? '$' : '') + checkToString(chk, subject); |
|
}, ''); |
|
}; |
|
|
|
let str = ''; |
|
|
|
for( let i = 0; i < this.length; i++ ){ |
|
let query = this[ i ]; |
|
|
|
str += queryToString( query, query.subject ); |
|
|
|
if( this.length > 1 && i < this.length - 1 ){ |
|
str += ', '; |
|
} |
|
} |
|
|
|
this.toStringCache = str; |
|
|
|
return str; |
|
}; |
|
|
|
export default { parse, toString }; |
|
|