diff --git a/benchmark/selector-filter.js b/benchmark/selector-filter.js new file mode 100644 index 0000000000..1f15f88296 --- /dev/null +++ b/benchmark/selector-filter.js @@ -0,0 +1,24 @@ +var Suite = require('./suite'); + +var eles; + +var suite = new Suite('eles.filter(selector)', { + setup: function( cytoscape ){ + var cy = cytoscape({ elements: require('./graphs/gal') }); + + eles = cy.nodes(); + + return cy; + } +}); + +suite + .add( function( cy ) { + // n.b. + // - use a selector that matches all nodes so we really compare the selector matching rather than letting the matches exit early + // - only create one selector : compare matching perf, not creation perf + eles.filter('node:unselected:grabbable[gal80Rexp > 0][SUID > 0][Stress >= 5][AverageShortestPathLength > 0]'); + }) +; + +module.exports = suite; diff --git a/benchmark/single/index.js b/benchmark/single/index.js index 3a8ce43ea0..5c98e45fec 100644 --- a/benchmark/single/index.js +++ b/benchmark/single/index.js @@ -1,5 +1,5 @@ // set this to run just a single suite via `gulp benchmark-single` // (useful when working on a specific function) -var suite = require('../karger-stein'); +var suite = require('../selector-filter'); suite.run({ async: true }); diff --git a/documentation/md/performance.md b/documentation/md/performance.md index b69743f2ee..ab62f66fdf 100644 --- a/documentation/md/performance.md +++ b/documentation/md/performance.md @@ -26,12 +26,13 @@ You can get much better performance out of Cytoscape.js by tuning your options, * Background and outlines increase the expense of rendering labels. * **Simplify edge style** : Drawing edges can be expensive. * Set your edges `curve-style` to `haystack` in your stylesheet. Haystack edges are straight lines, which are much less expensive to render than `bezier` edges. This is the default edge style. - * Use solid edges. Dotted and dashed edges are much more expensive to draw, so you will get increased performance by not using them. + * Use solid edges. Dotted and dashed edges are much more expensive to draw, so you will get increased performance by not using them. * Edge arrows are expensive to render, so consider not using them if they do not have any semantic meaning in your graph. * Opaque edges with arrows are more than twice as fast as semitransparent edges with arrows. * **Simplify node style** : Certain styles for nodes can be expensive. * Background images are very expensive in certain cases. The most performant background images are non-repeating (`background-repeat: no-repeat`) and non-clipped (`background-clip: none`). For simple node shapes like squares or circles, you can use `background-fit` for scaling and preclip your images to simulate software clipping (e.g. with [Gulp](https://github.com/scalableminds/gulp-image-resize) so it's automated). In lieu of preclipping, you could make clever use of PNGs with transparent backgrounds. * Node borders can be slightly expensive, so you can experiment with removing them to see if it makes a noticeable difference for your use case. +* **Avoid compound and edge selectors** : Compound selectors (e.g. `$node node`) and edge selectors (e.g. `$node -> node`) can be expensive because traversals are necessary. * **Set a lower pixel ratio** : Because it is more expensive to render more pixels, you can set `pixelRatio` to `1` [in the initialisation options](#init-opts/pixelRatio) to increase performance for large graphs on high density displays. However, this makes the rendering less crisp. * **Compound nodes** : [Compound nodes](#notation/compound-nodes) make style calculations and rendering more expensive. If your graph does not require compound nodes, you can improve performance by not using compound parent nodes. * **Hide edges during interactivity** : Set `hideEdgesOnViewport` to `true` in your [initialisation options](#core/initialisation). This can make interactivity less expensive for very large graphs by hiding edges during pan, mouse wheel zoom, pinch-to-zoom, and node drag actions. This option makes a difference on only very, very large graphs. diff --git a/documentation/md/selectors.md b/documentation/md/selectors.md index bcbbcb4542..90d68cbe5c 100644 --- a/documentation/md/selectors.md +++ b/documentation/md/selectors.md @@ -51,52 +51,52 @@ Matches element with the matching ID (e.g. `#foo` is the same as `[id = 'foo']`) ## Data -**`[name]`** +**`[name]`** Matches elements if they have the specified data attribute defined, i.e. not `undefined` (e.g. `[foo]` for an attribute named "foo"). Here, `null` is considered a defined value. -**`[^name]`** +**`[^name]`** Matches elements if the specified data attribute is not defined, i.e. `undefined` (e.g `[^foo]`). Here, `null` is considered a defined value. -**`[?name]`** +**`[?name]`** Matches elements if the specified data attribute is a [truthy](http://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/) value (e.g. `[?foo]`). -**`[!name]`** +**`[!name]`** Matches elements if the specified data attribute is a [falsey](http://javascriptweblog.wordpress.com/2011/02/07/truth-equality-and-javascript/) value (e.g. `[!foo]`). -**`[name = value]`** +**`[name = value]`** Matches elements if their data attribute matches a specified value (e.g. `[foo = 'bar']` or `[num = 2]`). -**`[name != value]`** +**`[name != value]`** Matches elements if their data attribute doesn't match a specified value (e.g. `[foo != 'bar']` or `[num != 2]`). -**`[name > value]`** +**`[name > value]`** Matches elements if their data attribute is greater than a specified value (e.g. `[foo > 'bar']` or `[num > 2]`). -**`[name >= value]`** +**`[name >= value]`** Matches elements if their data attribute is greater than or equal to a specified value (e.g. `[foo >= 'bar']` or `[num >= 2]`). -**`[name < value]`** +**`[name < value]`** Matches elements if their data attribute is less than a specified value (e.g. `[foo < 'bar']` or `[num < 2]`). -**`[name <= value]`** +**`[name <= value]`** Matches elements if their data attribute is less than or equal to a specified value (e.g. `[foo <= 'bar']` or `[num <= 2]`). -**`[name *= value]`** +**`[name *= value]`** Matches elements if their data attribute contains the specified value as a substring (e.g. `[foo *= 'bar']`). -**`[name ^= value]`** +**`[name ^= value]`** Matches elements if their data attribute starts with the specified value (e.g. `[foo ^= 'bar']`). -**`[name $= value]`** +**`[name $= value]`** Matches elements if their data attribute ends with the specified value (e.g. `[foo $= 'bar']`). -**`@` (data attribute operator modifier)** +**`@` (data attribute operator modifier)** Prepended to an operator so that is case insensitive (e.g. `[foo @$= 'ar']`, `[foo @>= 'a']`, `[foo @= 'bar']`) -**`!` (data attribute operator modifier)** +**`!` (data attribute operator modifier)** Prepended to an operator so that it is negated (e.g. `[foo !$= 'ar']`, `[foo !>= 'a']`) -**`[[]]` (metadata brackets)** +**`[[]]` (metadata brackets)** Use double square brackets in place of square ones to match against metadata instead of data (e.g. `[[degree > 2]]` matches elements of degree greater than 2). The properties that are supported include `degree`, `indegree`, and `outdegree`. ## Edges @@ -107,16 +107,19 @@ Matches edges for which the source and target subselectors match (e.g. `.src -> **`<->` (undirected edge selector)** Matches edges for which the connected node subselectors match (e.g. `.foo <-> .bar`) +**`$` (subject selector)** +Sets the subject of the selector (e.g. `$node -> node` to select the source nodes instead of the edges). + ## Compound nodes -**`>` (child selector)** +**`>` (child selector)** Matches direct children of the parent node (e.g. `node > node`). -**  (descendant selector)** +**  (descendant selector)** Matches descendants of the parent node (e.g. `node node`). -**`$` (subject selector)** -Sets the subject of the selector (e.g. `$node > node` to select the parent nodes instead of the children). A subject selector may not be used with an edge selector, because the edge ought to be the subject. +**`$` (subject selector)** +Sets the subject of the selector (e.g. `$node > node` to select the parent nodes instead of the children). ## State diff --git a/src/selector/data.js b/src/selector/data.js new file mode 100644 index 0000000000..69292930c6 --- /dev/null +++ b/src/selector/data.js @@ -0,0 +1,92 @@ +import * as is from '../is'; + +export const valCmp = (fieldVal, operator, value) => { + let matches; + let isFieldStr = is.string( fieldVal ); + let isFieldNum = is.number( fieldVal ); + let isValStr = is.string(value); + let fieldStr, valStr; + let caseInsensitive = false; + let notExpr = false; + let isIneqCmp = false; + + if( operator.indexOf( '!' ) >= 0 ){ + operator = operator.replace( '!', '' ); + notExpr = true; + } + + if( operator.indexOf( '@' ) >= 0 ){ + operator = operator.replace( '@', '' ); + caseInsensitive = true; + } + + if( isFieldStr || isValStr || caseInsensitive ){ + fieldStr = !isFieldStr && !isFieldNum ? '' : '' + fieldVal; + valStr = '' + value; + } + + // if we're doing a case insensitive comparison, then we're using a STRING comparison + // even if we're comparing numbers + if( caseInsensitive ){ + fieldVal = fieldStr = fieldStr.toLowerCase(); + value = valStr = valStr.toLowerCase(); + } + + switch( operator ){ + case '*=': + matches = fieldStr.indexOf( valStr ) >= 0; + break; + case '$=': + matches = fieldStr.indexOf( valStr, fieldStr.length - valStr.length ) >= 0; + break; + case '^=': + matches = fieldStr.indexOf( valStr ) === 0; + break; + case '=': + matches = fieldVal === value; + break; + case '>': + isIneqCmp = true; + matches = fieldVal > value; + break; + case '>=': + isIneqCmp = true; + matches = fieldVal >= value; + break; + case '<': + isIneqCmp = true; + matches = fieldVal < value; + break; + case '<=': + isIneqCmp = true; + matches = fieldVal <= value; + break; + default: + matches = false; + break; + } + + // apply the not op, but null vals for inequalities should always stay non-matching + if( notExpr && ( fieldVal != null || !isIneqCmp ) ){ + matches = !matches; + } + + return matches; +}; + +export const boolCmp = (fieldVal, operator) => { + switch( operator ){ + case '?': + return fieldVal ? true : false; + case '!': + return fieldVal ? false : true; + case '^': + return fieldVal === undefined; + } +}; + +export const existCmp = (fieldVal) => fieldVal !== undefined; + +export const data = (ele, field) => ele.data(field); + +export const meta = (ele, field) => ele[field](); \ No newline at end of file diff --git a/src/selector/expressions.js b/src/selector/expressions.js index a1e78e2216..5db9706af6 100644 --- a/src/selector/expressions.js +++ b/src/selector/expressions.js @@ -1,9 +1,8 @@ -import state from './state'; import tokens from './tokens'; import * as util from '../util'; import newQuery from './new-query'; - -const { stateSelectorRegex } = state; +import Type from './type'; +import { stateSelectorRegex } from './state'; // when a token like a variable has escaped meta characters, we need to clean the backslashes out // so that values get compared properly in Selector.filter() @@ -14,23 +13,23 @@ const cleanMetaChars = function( str ){ }; const replaceLastQuery = ( selector, examiningQuery, replacementQuery ) => { - if( examiningQuery === selector[ selector.length - 1 ] ){ - selector[ selector.length - 1 ] = replacementQuery; - } + selector[ selector.length - 1 ] = replacementQuery; }; // NOTE: add new expression syntax here to have it recognised by the parser; // - a query contains all adjacent (i.e. no separator in between) expressions; -// - the current query is stored in selector[i] --- you can use the reference to `this` in the populate function; -// - you need to check the query objects in Selector.filter() for it actually filter properly, but that's pretty straight forward -// - when you add something here, also add to Selector.toString() +// - the current query is stored in selector[i] +// - you need to check the query objects in match() for it actually filter properly, but that's pretty straight forward let exprs = [ { - name: 'group', + name: 'group', // just used for identifying when debugging query: true, regex: '(' + tokens.group + ')', populate: function( selector, query, [ group ] ){ - query.group = group === '*' ? group : group + 's'; + query.checks.push({ + type: Type.GROUP, + value: group === '*' ? group : group + 's' + }); } }, @@ -39,7 +38,10 @@ let exprs = [ query: true, regex: stateSelectorRegex, populate: function( selector, query, [ state ] ){ - query.colonSelectors.push( state ); + query.checks.push({ + type: Type.STATE, + value: state + }); } }, @@ -48,7 +50,10 @@ let exprs = [ query: true, regex: '\\#(' + tokens.id + ')', populate: function( selector, query,[ id ] ){ - query.ids.push( cleanMetaChars( id ) ); + query.checks.push({ + type: Type.ID, + value: cleanMetaChars( id ) + }); } }, @@ -57,7 +62,10 @@ let exprs = [ query: true, regex: '\\.(' + tokens.className + ')', populate: function( selector, query, [ className ] ){ - query.classes.push( cleanMetaChars( className ) ); + query.checks.push({ + type: Type.CLASS, + value: cleanMetaChars( className ) + }); } }, @@ -66,7 +74,8 @@ let exprs = [ query: true, regex: '\\[\\s*(' + tokens.variable + ')\\s*\\]', populate: function( selector, query, [ variable ] ){ - query.data.push( { + query.checks.push( { + type: Type.DATA_EXIST, field: cleanMetaChars( variable ) } ); } @@ -85,7 +94,8 @@ let exprs = [ value = parseFloat( value ); } - query.data.push( { + query.checks.push( { + type: Type.DATA_COMPARE, field: cleanMetaChars( variable ), operator: comparatorOp, value: value @@ -98,7 +108,8 @@ let exprs = [ query: true, regex: '\\[\\s*(' + tokens.boolOp + ')\\s*(' + tokens.variable + ')\\s*\\]', populate: function( selector, query, [ boolOp, variable ] ){ - query.data.push( { + query.checks.push( { + type: Type.DATA_BOOL, field: cleanMetaChars( variable ), operator: boolOp } ); @@ -110,7 +121,8 @@ let exprs = [ query: true, regex: '\\[\\[\\s*(' + tokens.meta + ')\\s*(' + tokens.comparatorOp + ')\\s*(' + tokens.number + ')\\s*\\]\\]', populate: function( selector, query, [ meta, comparatorOp, number ] ){ - query.meta.push( { + query.checks.push( { + type: Type.META_COMPARE, field: cleanMetaChars( meta ), operator: comparatorOp, value: parseFloat( number ) @@ -122,12 +134,27 @@ let exprs = [ name: 'nextQuery', separator: true, regex: tokens.separator, - populate: function( selector ){ + populate: function( selector, query ){ + let currentSubject = selector.currentSubject; + let edgeCount = selector.edgeCount; + let compoundCount = selector.compoundCount; + let lastQ = selector[ selector.length - 1 ]; + + if( currentSubject != null ){ + lastQ.subject = currentSubject; + selector.currentSubject = null; + } + + lastQ.edgeCount = edgeCount; + lastQ.compoundCount = compoundCount; + + selector.edgeCount = 0; + selector.compoundCount = 0; + // go on to next query let nextQuery = selector[ selector.length++ ] = newQuery(); - selector.currentSubject = null; - return nextQuery; + return nextQuery; // this is the new query to be filled by the following exprs } }, @@ -136,20 +163,34 @@ let exprs = [ separator: true, regex: tokens.directedEdge, populate: function( selector, query ){ - let edgeQuery = newQuery(); - let source = query; - let target = newQuery(); + if( selector.currentSubject == null ){ // undirected edge + let edgeQuery = newQuery(); + let source = query; + let target = newQuery(); - edgeQuery.group = 'edges'; - edgeQuery.target = target; - edgeQuery.source = source; - edgeQuery.subject = selector.currentSubject; + edgeQuery.checks.push({ type: Type.DIRECTED_EDGE, source, target }); - // the query in the selector should be the edge rather than the source - replaceLastQuery( selector, query, edgeQuery ); + // the query in the selector should be the edge rather than the source + replaceLastQuery( selector, query, edgeQuery ); - // we're now populating the target query with expressions that follow - return target; + selector.edgeCount++; + + // we're now populating the target query with expressions that follow + return target; + } else { // source/target + let srcTgtQ = newQuery(); + let source = query; + let target = newQuery(); + + srcTgtQ.checks.push({ type: Type.NODE_SOURCE, source, target }); + + // the query in the selector should be the neighbourhood rather than the node + replaceLastQuery( selector, query, srcTgtQ ); + + selector.edgeCount++; + + return target; // now populating the target with the following expressions + } } }, @@ -158,19 +199,32 @@ let exprs = [ separator: true, regex: tokens.undirectedEdge, populate: function( selector, query ){ - let edgeQuery = newQuery(); - let source = query; - let target = newQuery(); + if( selector.currentSubject == null ){ // undirected edge + let edgeQuery = newQuery(); + let source = query; + let target = newQuery(); - edgeQuery.group = 'edges'; - edgeQuery.connectedNodes = [ source, target ]; - edgeQuery.subject = selector.currentSubject; + edgeQuery.checks.push({ type: Type.UNDIRECTED_EDGE, nodes: [ source, target ] }); - // the query in the selector should be the edge rather than the source - replaceLastQuery( selector, query, edgeQuery ); + // the query in the selector should be the edge rather than the source + replaceLastQuery( selector, query, edgeQuery ); - // we're now populating the target query with expressions that follow - return target; + selector.edgeCount++; + + // we're now populating the target query with expressions that follow + return target; + } else { // neighbourhood + let nhoodQ = newQuery(); + let node = query; + let neighbor = newQuery(); + + nhoodQ.checks.push({ type: Type.NODE_NEIGHBOR, node, neighbor }); + + // the query in the selector should be the neighbourhood rather than the node + replaceLastQuery( selector, query, nhoodQ ); + + return neighbor; // now populating the neighbor with following expressions + } } }, @@ -179,16 +233,65 @@ let exprs = [ separator: true, regex: tokens.child, populate: function( selector, query ){ - // this query is the parent of the following query - let childQuery = newQuery(); - childQuery.parent = query; - childQuery.subject = selector.currentSubject; + if( selector.currentSubject == null ){ // default: child query + let parentChildQuery = newQuery(); + let child = newQuery(); + let parent = selector[selector.length - 1]; + + parentChildQuery.checks.push({ type: Type.CHILD, parent, child }); + + // the query in the selector should be the '>' itself + replaceLastQuery( selector, query, parentChildQuery ); + + selector.compoundCount++; + + // we're now populating the child query with expressions that follow + return child; + } else if( selector.currentSubject === query ){ // compound split query + let compound = newQuery(); + let left = selector[ selector.length - 1 ]; + let right = newQuery(); + let subject = newQuery(); + let child = newQuery(); + let parent = newQuery(); + + // set up the root compound q + compound.checks.push({ type: Type.COMPOUND_SPLIT, left, right, subject }); + + // populate the subject and replace the q at the old spot (within left) with TRUE + subject.checks = query.checks; // take the checks from the left + query.checks = [ { type: Type.TRUE } ]; // checks under left refs the subject implicitly + + // set up the right q + parent.checks.push({ type: Type.TRUE }); // parent implicitly refs the subject + right.checks.push({ + type: Type.PARENT, // type is swapped on right side queries + parent, + child // empty for now + }); + + replaceLastQuery( selector, left, compound ); - // it's cheaper to compare children first and go up so replace the parent - replaceLastQuery( selector, query, childQuery ); + // update the ref since we moved things around for `query` + selector.currentSubject = subject; - // we're now populating the child query with expressions that follow - return childQuery; + selector.compoundCount++; + + return child; // now populating the right side's child + } else { // parent query + // info for parent query + let parent = newQuery(); + let child = newQuery(); + let pcQChecks = [ { type: Type.PARENT, parent, child } ]; + + // the parent-child query takes the place of the query previously being populated + parent.checks = query.checks; // the previous query contains the checks for the parent + query.checks = pcQChecks; // pc query takes over + + selector.compoundCount++; + + return child; // we're now populating the child + } } }, @@ -197,16 +300,65 @@ let exprs = [ separator: true, regex: tokens.descendant, populate: function( selector, query ){ - // this query is the ancestor of the following query - let descendantQuery = newQuery(); - descendantQuery.ancestor = query; - descendantQuery.subject = selector.currentSubject; + if( selector.currentSubject == null ){ // default: descendant query + let ancChQuery = newQuery(); + let descendant = newQuery(); + let ancestor = selector[selector.length - 1]; + + ancChQuery.checks.push({ type: Type.DESCENDANT, ancestor, descendant }); + + // the query in the selector should be the '>' itself + replaceLastQuery( selector, query, ancChQuery ); + + selector.compoundCount++; + + // we're now populating the descendant query with expressions that follow + return descendant; + } else if( selector.currentSubject === query ){ // compound split query + let compound = newQuery(); + let left = selector[ selector.length - 1 ]; + let right = newQuery(); + let subject = newQuery(); + let descendant = newQuery(); + let ancestor = newQuery(); + + // set up the root compound q + compound.checks.push({ type: Type.COMPOUND_SPLIT, left, right, subject }); + + // populate the subject and replace the q at the old spot (within left) with TRUE + subject.checks = query.checks; // take the checks from the left + query.checks = [ { type: Type.TRUE } ]; // checks under left refs the subject implicitly + + // set up the right q + ancestor.checks.push({ type: Type.TRUE }); // ancestor implicitly refs the subject + right.checks.push({ + type: Type.ANCESTOR, // type is swapped on right side queries + ancestor, + descendant // empty for now + }); + + replaceLastQuery( selector, left, compound ); - // it's cheaper to compare descendants first and go up so replace the ancestor - replaceLastQuery( selector, query, descendantQuery ); + // update the ref since we moved things around for `query` + selector.currentSubject = subject; - // we're now populating the descendant query with expressions that follow - return descendantQuery; + selector.compoundCount++; + + return descendant; // now populating the right side's descendant + } else { // ancestor query + // info for parent query + let ancestor = newQuery(); + let descendant = newQuery(); + let adQChecks = [ { type: Type.ANCESTOR, ancestor, descendant } ]; + + // the parent-child query takes the place of the query previously being populated + ancestor.checks = query.checks; // the previous query contains the checks for the parent + query.checks = adQChecks; // pc query takes over + + selector.compoundCount++; + + return descendant; // we're now populating the child + } } }, @@ -215,14 +367,34 @@ let exprs = [ modifier: true, regex: tokens.subject, populate: function( selector, query ){ - if( selector.currentSubject != null && query.subject != query ){ + if( selector.currentSubject != null && selector.currentSubject !== query ){ util.warn( 'Redefinition of subject in selector `' + selector.toString() + '`' ); return false; } selector.currentSubject = query; - query.subject = query; - selector[ selector.length - 1 ].subject = query; + + let topQ = selector[selector.length - 1]; + let topChk = topQ.checks[0]; + let topType = topChk == null ? null : topChk.type; + + if( topType === Type.DIRECTED_EDGE ){ + // directed edge with subject on the target + + // change to target node check + topChk.type = Type.NODE_TARGET; + + } else if( topType === Type.UNDIRECTED_EDGE ){ + // undirected edge with subject on the second node + + // change to neighbor check + topChk.type = Type.NODE_NEIGHBOR; + topChk.node = topChk.nodes[1]; // second node is subject + topChk.neighbor = topChk.nodes[0]; + + // clean up unused fields for new type + topChk.nodes = null; + } } } ]; diff --git a/src/selector/index.js b/src/selector/index.js index b2e9349729..d6376f8947 100644 --- a/src/selector/index.js +++ b/src/selector/index.js @@ -1,72 +1,57 @@ import * as is from '../is'; import * as util from '../util'; -import newQuery from './new-query'; import parse from './parse'; import matching from './matching'; +import Type from './type'; let Selector = function( selector ){ - let self = this; - - self._private = { - selectorText: selector, - invalid: true - }; + this.inputText = selector; + this.currentSubject = null; + this.compoundCount = 0; + this.edgeCount = 0; + this.length = 0; if( selector == null || ( is.string( selector ) && selector.match( /^\s*$/ ) ) ){ - - self.length = 0; - - } else if( selector === '*' || selector === 'edge' || selector === 'node' ){ - - // make single, group-only selectors cheap to make and cheap to filter - - self[0] = newQuery(); - self[0].group = selector === '*' ? selector : selector + 's'; - self[0].groupOnly = true; - self[0].length = 1; - self._private.invalid = false; - self.length = 1; + // leave empty } else if( is.elementOrCollection( selector ) ){ - let collection = selector.collection(); - - self[0] = newQuery(); - self[0].collection = collection; - self[0].length = 1; - self.length = 1; + this.addQuery({ + checks: [ { + type: Type.COLLECTION, + value: selector.collection() + } ] + }); } else if( is.fn( selector ) ){ - self[0] = newQuery(); - self[0].filter = selector; - self[0].length = 1; - self.length = 1; + this.addQuery({ + checks: [ { + type: Type.FILTER, + value: selector + } ] + }); } else if( is.string( selector ) ){ - if( !self.parse( selector ) ){ return; } + if( !this.parse( selector ) ){ + this.invalid = true; + } } else { util.error( 'A selector must be created from a string; found ', selector ); - return; } - - self._private.invalid = false; }; let selfn = Selector.prototype; -selfn.valid = function(){ - return !this._private.invalid; -}; - -selfn.invalid = function(){ - return this._private.invalid; -}; +[ + parse, + matching +].forEach( p => util.assign( selfn, p ) ); selfn.text = function(){ - return this._private.selectorText; + return this.inputText; }; selfn.size = function(){ @@ -78,126 +63,13 @@ selfn.eq = function( i ){ }; selfn.sameText = function( otherSel ){ - return this.text() === otherSel.text(); + return !this.invalid && !otherSel.invalid && this.text() === otherSel.text(); }; -selfn.toString = selfn.selector = function(){ - - if( this._private.toStringCache != null ){ - return this._private.toStringCache; - } - - let i; - let str = ''; - - 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 = function( val ){ - return ' ' + val + ' '; - }; - - let queryToString = function( query ){ - let str = ''; - let j, sel; - - if( query.subject === query ){ - str += '$'; - } - - let group = clean( query.group ); - str += group.substring( 0, group.length - 1 ); - - for( j = 0; j < query.data.length; j++ ){ - let data = query.data[ j ]; - - if( data.value ){ - str += '[' + data.field + space( clean( data.operator ) ) + cleanVal( data.value ) + ']'; - } else { - str += '[' + clean( data.operator ) + data.field + ']'; - } - } - - for( j = 0; j < query.meta.length; j++ ){ - let meta = query.meta[ j ]; - str += '[[' + meta.field + space( clean( meta.operator ) ) + cleanVal( meta.value ) + ']]'; - } - - for( j = 0; j < query.colonSelectors.length; j++ ){ - sel = query.colonSelectors[ i ]; - str += sel; - } - - for( j = 0; j < query.ids.length; j++ ){ - sel = '#' + query.ids[ i ]; - str += sel; - } - - for( j = 0; j < query.classes.length; j++ ){ - sel = '.' + query.classes[ j ]; - str += sel; - } - - if( query.source != null && query.target != null ){ - str = queryToString( query.source ) + ' -> ' + queryToString( query.target ); - } - - if( query.connectedNodes != null ){ - let n = query.connectedNodes; - - str = queryToString( n[0] ) + ' <-> ' + queryToString( n[1] ); - } - - if( query.parent != null ){ - str = queryToString( query.parent ) + ' > ' + str; - } - - if( query.ancestor != null ){ - str = queryToString( query.ancestor ) + ' ' + str; - } - - if( query.child != null ){ - str += ' > ' + queryToString( query.child ); - } - - if( query.descendant != null ){ - str += ' ' + queryToString( query.descendant ); - } - - return str; - }; - - for( i = 0; i < this.length; i++ ){ - let query = this[ i ]; - - str += queryToString( query ); - - if( this.length > 1 && i < this.length - 1 ){ - str += ', '; - } - } - - this._private.toStringCache = str; - - return str; +selfn.addQuery = function( q ){ + this[ this.length++ ] = q; }; -[ - parse, - matching -].forEach( p => util.assign( selfn, p ) ); +selfn.selector = selfn.toString; export default Selector; diff --git a/src/selector/matching.js b/src/selector/matching.js index 079475c7a5..56f4ffe0d2 100644 --- a/src/selector/matching.js +++ b/src/selector/matching.js @@ -1,271 +1,13 @@ -import state from './state'; -import * as is from '../is'; - -const { stateSelectorMatches } = state; - -// generic checking for data/metadata -let operandsMatch = function( query, params ){ - let allDataMatches = true; - for( let k = 0; k < query[ params.name ].length; k++ ){ - let data = query[ params.name ][ k ]; - let operator = data.operator; - let value = data.value; - let field = data.field; - let matches; - let fieldVal = params.fieldValue( field ); - - if( operator != null && value != null ){ - let fieldStr = !is.string( fieldVal ) && !is.number( fieldVal ) ? '' : '' + fieldVal; - let valStr = '' + value; - - let caseInsensitive = false; - if( operator.indexOf( '@' ) >= 0 ){ - fieldStr = fieldStr.toLowerCase(); - valStr = valStr.toLowerCase(); - - operator = operator.replace( '@', '' ); - caseInsensitive = true; - } - - let notExpr = false; - if( operator.indexOf( '!' ) >= 0 ){ - operator = operator.replace( '!', '' ); - notExpr = true; - } - - // if we're doing a case insensitive comparison, then we're using a STRING comparison - // even if we're comparing numbers - if( caseInsensitive ){ - value = valStr.toLowerCase(); - fieldVal = fieldStr.toLowerCase(); - } - - let isIneqCmp = false; - - switch( operator ){ - case '*=': - matches = fieldStr.indexOf( valStr ) >= 0; - break; - case '$=': - matches = fieldStr.indexOf( valStr, fieldStr.length - valStr.length ) >= 0; - break; - case '^=': - matches = fieldStr.indexOf( valStr ) === 0; - break; - case '=': - matches = fieldVal === value; - break; - case '>': - isIneqCmp = true; - matches = fieldVal > value; - break; - case '>=': - isIneqCmp = true; - matches = fieldVal >= value; - break; - case '<': - isIneqCmp = true; - matches = fieldVal < value; - break; - case '<=': - isIneqCmp = true; - matches = fieldVal <= value; - break; - default: - matches = false; - break; - } - - // apply the not op, but null vals for inequalities should always stay non-matching - if( notExpr && ( fieldVal != null || !isIneqCmp ) ){ - matches = !matches; - } - } else if( operator != null ){ - switch( operator ){ - case '?': - matches = fieldVal ? true : false; - break; - case '!': - matches = fieldVal ? false : true; - break; - case '^': - matches = fieldVal === undefined; - break; - } - } else { - matches = fieldVal !== undefined; - } - - if( !matches ){ - allDataMatches = false; - break; - } - } // for - - return allDataMatches; -}; // operandsMatch - -// check parent/child relations -let confirmRelations = function( query, isNecessary, eles ){ - if( query != null ){ - let matches = false; - - if( !isNecessary ){ return false; } - - eles = eles(); // save cycles if query == null - - // query must match for at least one element (may be recursive) - for( let i = 0; i < eles.length; i++ ){ - if( queryMatches( query, eles[ i ] ) ){ - matches = true; - break; - } - } - - return matches; - } else { - return true; - } -}; - -let queryMatches = function( query, ele ){ - // make single group-only selectors really cheap to check since they're the most common ones - if( query.groupOnly ){ - return query.group === '*' || query.group === ele.group(); - } - - // check group - if( query.group != null && query.group != '*' && query.group != ele.group() ){ - return false; - } - - let cy = ele.cy(); - let k; - - // check colon selectors - let allColonSelectorsMatch = true; - for( k = 0; k < query.colonSelectors.length; k++ ){ - let sel = query.colonSelectors[ k ]; - - allColonSelectorsMatch = stateSelectorMatches( sel, ele ); - - if( !allColonSelectorsMatch ) break; - } - if( !allColonSelectorsMatch ) return false; - - // check id - let allIdsMatch = true; - for( k = 0; k < query.ids.length; k++ ){ - let id = query.ids[ k ]; - let actualId = ele.id(); - - allIdsMatch = allIdsMatch && (id == actualId); - - if( !allIdsMatch ) break; - } - if( !allIdsMatch ) return false; - - // check classes - let allClassesMatch = true; - for( k = 0; k < query.classes.length; k++ ){ - let cls = query.classes[ k ]; - - allClassesMatch = allClassesMatch && ele.hasClass( cls ); - - if( !allClassesMatch ) break; - } - if( !allClassesMatch ) return false; - - // check data matches - let allDataMatches = operandsMatch( query, { - name: 'data', - fieldValue: function( field ){ - return ele.data( field ); - } - } ); - - if( !allDataMatches ){ - return false; - } - - // check metadata matches - let allMetaMatches = operandsMatch( query, { - name: 'meta', - fieldValue: function( field ){ - return ele[ field ](); - } - } ); - - if( !allMetaMatches ){ - return false; - } - - // check collection - if( query.collection != null ){ - let matchesAny = query.collection.hasElementWithId( ele.id() ); - - if( !matchesAny ){ - return false; - } - } - - // check filter function - if( query.filter != null && ele.collection().some( query.filter ) ){ - return false; - } - - let isCompound = cy.hasCompoundNodes(); - let getSource = () => ele.source(); - let getTarget = () => ele.target(); - - if( !confirmRelations( query.parent, isCompound, () => ele.parent() ) ){ return false; } - - if( !confirmRelations( query.ancestor, isCompound, () => ele.parents() ) ){ return false; } - - if( !confirmRelations( query.child, isCompound, () => ele.children() ) ){ return false; } - - if( !confirmRelations( query.descendant, isCompound, () => ele.descendants() ) ){ return false; } - - if( !confirmRelations( query.source, true, getSource ) ){ return false; } - - if( !confirmRelations( query.target, true, getTarget ) ){ return false; } - - if( query.connectedNodes ){ - let q0 = query.connectedNodes[0]; - let q1 = query.connectedNodes[1]; - - if( - confirmRelations( q0, true, getSource ) && - confirmRelations( q1, true, getTarget ) - ){ - // match - } else if( - confirmRelations( q0, true, getTarget ) && - confirmRelations( q1, true, getSource ) - ){ - // match - } else { - return false; - } - } - - // we've reached the end, so we've matched everything for this query - return true; -}; // queryMatches +import { matches as queryMatches } from './query-type-match'; +import Type from './type'; // filter an existing collection let filter = function( collection ){ let self = this; - let cy = collection.cy(); - - // don't bother trying if it's invalid - if( self.invalid() ){ - return cy.collection(); - } // for 1 id #foo queries, just get the element - if( self.length === 1 && self[0].length === 1 && self[0].ids.length === 1 ){ - return collection.getElementById( self[0].ids[0] ).collection(); + if( self.length === 1 && self[0].checks.length === 1 && self[0].checks[0].type === Type.ID ){ + return collection.getElementById( self[0].checks[0].value ).collection(); } let selectorFunction = function( element ){ @@ -284,20 +26,13 @@ let filter = function( collection ){ selectorFunction = function(){ return true; }; } - let filteredCollection = collection.filter( selectorFunction ); - - return filteredCollection; + return collection.filter( selectorFunction ); }; // filter // does selector match a single element? let matches = function( ele ){ let self = this; - // don't bother trying if it's invalid - if( self.invalid() ){ - return false; - } - for( let j = 0; j < self.length; j++ ){ let query = self[ j ]; @@ -307,6 +42,6 @@ let matches = function( ele ){ } return false; -}; // filter +}; // matches export default { matches, filter }; diff --git a/src/selector/new-query.js b/src/selector/new-query.js index 9ba8065066..1d1f469598 100644 --- a/src/selector/new-query.js +++ b/src/selector/new-query.js @@ -1,26 +1,12 @@ -// storage for parsed queries +/** + * Make a new query object + * + * @prop type {Type} The type enum (int) of the query + * @prop checks List of checks to make against an ele to test for a match + */ let newQuery = function(){ return { - classes: [], - colonSelectors: [], - data: [], - group: null, - ids: [], - meta: [], - - // fake selectors - collection: null, // a collection to match against - filter: null, // filter function - - // these are defined in the upward direction rather than down (e.g. child) - // because we need to go up in Selector.filter() - parent: null, // parent query obj - ancestor: null, // ancestor query obj - subject: null, // defines subject in compound query (subject query obj; points to self if subject) - - // use these only when subject has been defined - child: null, - descendant: null + checks: [] }; }; diff --git a/src/selector/parse.js b/src/selector/parse.js index 020156abd7..7eaa42919f 100644 --- a/src/selector/parse.js +++ b/src/selector/parse.js @@ -1,9 +1,15 @@ -import * as util from '../util'; +import { warn } from '../util'; +import * as is from '../is'; import exprs from './expressions'; import newQuery from './new-query'; - -// of all the expressions, find the first match in the remaining text -let consumeExpr = function( remaining ){ +import Type from './type'; + +/** + * Of all the expressions, find the first match in the remaining text. + * @param {string} remaining The remaining text to parse + * @returns The matched expression and the newly remaining text `{ expr, match, name, remaining }` + */ +const consumeExpr = ( remaining ) => { let expr; let match; let name; @@ -35,8 +41,12 @@ let consumeExpr = function( remaining ){ }; -// consume all leading whitespace -let consumeWhitespace = function( remaining ){ +/** + * Consume all the leading whitespace + * @param {string} remaining The text to consume + * @returns The text with the leading whitespace removed + */ +const consumeWhitespace = ( remaining ) => { let match = remaining.match( /^\s+/ ); if( match ){ @@ -47,10 +57,15 @@ let consumeWhitespace = function( remaining ){ return remaining; }; -let parse = function( selector ){ +/** + * Parse the string and store the parsed representation in the Selector. + * @param {string} selector The selector string + * @returns `true` if the selector was successfully parsed, `false` otherwise + */ +const parse = function( selector ){ let self = this; - let remaining = self._private.selectorText = selector; + let remaining = self.inputText = selector; let currentQuery = self[0] = newQuery(); self.length = 1; @@ -58,16 +73,16 @@ let parse = function( selector ){ remaining = consumeWhitespace( remaining ); // get rid of leading whitespace for( ;; ){ - let check = consumeExpr( remaining ); + let exprInfo = consumeExpr( remaining ); - if( check.expr == null ){ - util.warn( 'The selector `' + selector + '`is invalid' ); + if( exprInfo.expr == null ){ + warn( 'The selector `' + selector + '`is invalid' ); return false; } else { - let args = check.match.slice( 1 ); + let args = exprInfo.match.slice( 1 ); // let the token populate the selector object in currentQuery - let ret = check.expr.populate( self, currentQuery, args ); + let ret = exprInfo.expr.populate( self, currentQuery, args ); if( ret === false ){ return false; // exit if population failed @@ -76,7 +91,7 @@ let parse = function( selector ){ } } - remaining = check.remaining; + remaining = exprInfo.remaining; // we're done when there's nothing left to parse if( remaining.match( /^\s*$/ ) ){ @@ -84,45 +99,155 @@ let parse = function( selector ){ } } - // adjust references for subject - for( let j = 0; j < self.length; j++ ){ - let query = self[ j ]; + let lastQ = self[self.length - 1]; + + if( self.currentSubject != null ){ + lastQ.subject = self.currentSubject; + } - if( query.subject != null ){ - // go up the tree until we reach the subject - for( ;; ){ - if( query.subject === query ){ break; } // done if subject is self + lastQ.edgeCount = self.edgeCount; + lastQ.compoundCount = self.compoundCount; - if( query.parent != null ){ // swap parent/child reference - let parent = query.parent; - let child = query; + for( let i = 0; i < self.length; i++ ){ + let q = self[i]; - child.parent = null; - parent.child = child; + // in future, this could potentially be allowed if there were operator precedence and detection of invalid combinations + 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; + } - query = parent; // go up the tree - } else if( query.ancestor != null ){ // swap ancestor/descendant - let ancestor = query.ancestor; - let descendant = query; + // in future, this could potentially be allowed with an explicit subject selector + if( q.edgeCount > 1 ){ + warn( 'The selector `' + selector + '`is invalid because it uses multiple edge selectors' ); + return false; + } + } - descendant.ancestor = null; - ancestor.descendant = descendant; + return true; // success +}; - query = ancestor; // go up the tree - } else if( query.source || query.target || query.connectedNodes ){ - util.warn( 'The selector `' + self.text() + '` can not contain a subject selector that applies to the source or target of an edge selector' ); - return false; - } else { - util.warn( 'When adjusting references for the selector `' + self.text() + '`, neither parent nor ancestor was found' ); - return false; - } - } // for +/** + * Get the selector represented as a string. This value uses default formatting, + * so things like spacing may differ from the input text passed to the constructor. + * @returns {string} The selector string + */ +export const toString = function(){ + if( this.toStringCache != null ){ + return this.toStringCache; + } - self[ j ] = query.subject; // subject should be the root query - } // if - } // for + let clean = function( obj ){ + if( obj == null ){ + return ''; + } else { + return obj; + } + }; - return true; // success + 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 }; +export default { parse, toString }; diff --git a/src/selector/query-type-match.js b/src/selector/query-type-match.js new file mode 100644 index 0000000000..30d4e7367a --- /dev/null +++ b/src/selector/query-type-match.js @@ -0,0 +1,122 @@ +import Type from './type'; +import { stateSelectorMatches } from './state'; +import { valCmp, boolCmp, existCmp, meta, data } from './data'; + +/** A lookup of `match(check, ele)` functions by `Type` int */ +export const match = []; + +/** + * Returns whether the query matches for the element + * @param query The `{ type, value, ... }` query object + * @param ele The element to compare against +*/ +export const matches = (query, ele) => { + return query.checks.every( chk => match[chk.type](chk, ele) ); +}; + +match[Type.GROUP] = (check, ele) => { + let group = check.value; + + return group === '*' || group === ele.group(); +}; + +match[Type.STATE] = (check, ele) => { + let stateSelector = check.value; + + return stateSelectorMatches( stateSelector, ele ); +}; + +match[Type.ID] = (check, ele) => { + let id = check.value; + + return ele.id() === id; +}; + +match[Type.CLASS] = (check, ele) => { + let cls = check.value; + + return ele.hasClass(cls); +}; + +match[Type.META_COMPARE] = (check, ele) => { + let { field, operator, value } = check; + + return valCmp( meta(ele, field), operator, value ); +}; + +match[Type.DATA_COMPARE] = (check, ele) => { + let { field, operator, value } = check; + + return valCmp( data(ele, field), operator, value ); +}; + +match[Type.DATA_BOOL] = (check, ele) => { + let { field, operator } = check; + + return boolCmp( data(ele, field), operator ); +}; + +match[Type.DATA_EXIST] = (check, ele) => { + let { field, operator } = check; + + return existCmp( data(ele, field), operator ); +}; + +match[Type.UNDIRECTED_EDGE] = (check, ele) => { + let qA = check.nodes[0]; + let qB = check.nodes[1]; + let src = ele.source(); + let tgt = ele.target(); + + return ( matches(qA, src) && matches(qB, tgt) ) || ( matches(qB, src) && matches(qA, tgt) ); +}; + +match[Type.NODE_NEIGHBOR] = (check, ele) => { + return matches(check.node, ele) && ele.neighborhood().some( n => n.isNode() && matches(check.neighbor, n) ); +}; + +match[Type.DIRECTED_EDGE] = (check, ele) => { + return matches(check.source, ele.source()) && matches(check.target, ele.target()); +}; + +match[Type.NODE_SOURCE] = (check, ele) => { + return matches(check.source, ele) && ele.outgoers().some( n => n.isNode() && matches(check.target, n) ); +}; + +match[Type.NODE_TARGET] = (check, ele) => { + return matches(check.target, ele) && ele.incomers().some( n => n.isNode() && matches(check.source, n) ); +}; + +match[Type.CHILD] = (check, ele) => { + return matches(check.child, ele) && matches(check.parent, ele.parent()); +}; + +match[Type.PARENT] = (check, ele) => { + return matches(check.parent, ele) && ele.children().some( c => matches(check.child, c) ); +}; + +match[Type.DESCENDANT] = (check, ele) => { + return matches(check.descendant, ele) && ele.ancestors().some( a => matches(check.ancestor, a) ); +}; + +match[Type.ANCESTOR] = (check, ele) => { + return matches(check.ancestor, ele) && ele.descendants().some( d => matches(check.descendant, d) ); +}; + +match[Type.COMPOUND_SPLIT] = (check, ele) => { + return matches(check.subject, ele) && matches(check.left, ele) && matches(check.right, ele); +}; + +match[Type.TRUE] = () => true; + +match[Type.COLLECTION] = (check, ele) => { + let collection = check.value; + + return collection.has(ele); +}; + +match[Type.FILTER] = (check, ele) => { + let filter = check.value; + + return filter(ele); +}; \ No newline at end of file diff --git a/src/selector/state.js b/src/selector/state.js index 841f7bdabc..8e1d641014 100644 --- a/src/selector/state.js +++ b/src/selector/state.js @@ -1,6 +1,6 @@ import * as util from '../util'; -let stateSelectors = [ +export const stateSelectors = [ { selector: ':selected', matches: function( ele ){ return ele.selected(); } @@ -117,23 +117,21 @@ let stateSelectors = [ return util.sort.descending( a.selector, b.selector ); }); -let stateSelectorMatches = function( sel, ele ){ - let lookup = stateSelectorMatches.lookup = stateSelectorMatches.lookup || (function(){ - let selToFn = {}; - let s; +let lookup = (function(){ + let selToFn = {}; + let s; - for( let i = 0; i < stateSelectors.length; i++ ){ - s = stateSelectors[i]; + for( let i = 0; i < stateSelectors.length; i++ ){ + s = stateSelectors[i]; - selToFn[ s.selector ] = s.matches; - } + selToFn[ s.selector ] = s.matches; + } - return selToFn; - })(); + return selToFn; +})(); +export const stateSelectorMatches = function( sel, ele ){ return lookup[ sel ]( ele ); }; -let stateSelectorRegex = '(' + stateSelectors.map(function( s ){ return s.selector; }).join('|') + ')'; - -export default { stateSelectors, stateSelectorMatches, stateSelectorRegex }; +export const stateSelectorRegex = '(' + stateSelectors.map(s => s.selector).join('|') + ')'; diff --git a/src/selector/type.js b/src/selector/type.js new file mode 100644 index 0000000000..944a892a8b --- /dev/null +++ b/src/selector/type.js @@ -0,0 +1,70 @@ +/** + * A check type enum-like object. Uses integer values for fast match() lookup. + * The ordering does not matter as long as the ints are unique. + */ +const Type = { + /** E.g. node */ + GROUP: 0, + + /** A collection of elements */ + COLLECTION: 1, + + /** A filter(ele) function */ + FILTER: 2, + + /** E.g. [foo > 1] */ + DATA_COMPARE: 3, + + /** E.g. [foo] */ + DATA_EXIST: 4, + + /** E.g. [?foo] */ + DATA_BOOL: 5, + + /** E.g. [[degree > 2]] */ + META_COMPARE: 6, + + /** E.g. :selected */ + STATE: 7, + + /** E.g. #foo */ + ID: 8, + + /** E.g. .foo */ + CLASS: 9, + + /** E.g. #foo <-> #bar */ + UNDIRECTED_EDGE: 10, + + /** E.g. #foo -> #bar */ + DIRECTED_EDGE: 11, + + /** E.g. $#foo -> #bar */ + NODE_SOURCE: 12, + + /** E.g. #foo -> $#bar */ + NODE_TARGET: 13, + + /** E.g. $#foo <-> #bar */ + NODE_NEIGHBOR: 14, + + /** E.g. #foo > #bar */ + CHILD: 15, + + /** E.g. #foo #bar */ + DESCENDANT: 16, + + /** E.g. $#foo > #bar */ + PARENT: 17, + + /** E.g. $#foo #bar */ + ANCESTOR: 18, + + /** E.g. #foo > $bar > #baz */ + COMPOUND_SPLIT: 19, + + /** Always matches, useful placeholder for subject in `COMPOUND_SPLIT` */ + TRUE: 20 +}; + +export default Type; \ No newline at end of file diff --git a/test/modules/emitter.js b/test/modules/emitter.js index be20bbcee8..f0de3d95b8 100644 --- a/test/modules/emitter.js +++ b/test/modules/emitter.js @@ -1,5 +1,5 @@ var expect = require('chai').expect; -var Emitter = require('../../src/emitter'); +var Emitter = require('../../src/emitter').default; describe('Emitter', function(){ var em; diff --git a/test/selectors.js b/test/selectors.js index 387effa701..77e3ab5bbc 100644 --- a/test/selectors.js +++ b/test/selectors.js @@ -4,9 +4,6 @@ var cytoscape = require('../src/test.js', cytoscape); describe('Selectors', function(){ var cy; - var n1, n2, nparent, n1n2, nparentLoop; - var eles; - beforeEach(function(){ cy = cytoscape({ styleEnabled: true, @@ -15,7 +12,8 @@ describe('Selectors', function(){ nodes: [ { data: { id: 'n1', foo: 'one', weight: 1, 'weird.name': 1, 'weird.name2': 'weird.val', emptystr: '' }, classes: 'cls1 cls2' }, { data: { id: 'n2', foo: 'two', parent: 'nparent', weight: 2, 'weird.name3': '"blah"^blah#blah' }, classes: 'cls1' }, - { data: { id: 'nparent', weight: 3 }, classes: 'cls2' } + { data: { id: 'nparent', parent: 'nparent2', weight: 3 }, classes: 'cls2' }, + { data: { id: 'nparent2' } } ], edges: [ @@ -25,25 +23,13 @@ describe('Selectors', function(){ } }); - n1 = cy.getElementById('n1'); - n2 = cy.getElementById('n2'); - nparent = cy.getElementById('nparent'); - n1n2 = cy.getElementById('n1n2'); - nparentLoop = cy.getElementById('nparentLoop'); - - n1.select(); - n2.unselectify(); - nparent.lock(); - n1n2.hide(); - n1n2.css('opacity', 0); - - eles = { - n1: n1, - n2: n2, - n1n2: n1n2, - nparent: nparent, - nparentLoop: nparentLoop - }; + cy.getElementById('n1').select(); + cy.getElementById('n2').unselectify(); + cy.getElementById('nparent').lock(); + cy.getElementById('n1n2').style({ + display: 'none', + opacity: 0 + }); }); afterEach(function(){ @@ -57,23 +43,21 @@ describe('Selectors', function(){ return col.map(function( ele ){ return '#' + ele.id(); }).sort().join(', '); - } + }; it(selector, function(){ - var col = cy.collection(); + var eles = []; for( var i = 1; i < args.length; i++ ){ - var ele = eles[ args[i] ]; - - col = col.add(ele); + eles.push( cy.getElementById(args[i]) ); } - expect( getIds( cy.$(selector) ) ).to.equal( getIds( col ) ); + expect( getIds( cy.$(selector) ) ).to.equal( getIds( eles ) ); }); }; // general - itSelects('node', 'n1', 'n2', 'nparent'); + itSelects('node', 'n1', 'n2', 'nparent', 'nparent2'); itSelects('edge', 'n1n2', 'nparentLoop'); itSelects('#n1', 'n1'); itSelects('#n1, #n2', 'n1', 'n2'); @@ -83,10 +67,10 @@ describe('Selectors', function(){ itSelects('[weight]', 'n1', 'n2', 'nparent', 'n1n2'); itSelects('[?foo]', 'n1', 'n2'); itSelects('[?foo]', 'n1', 'n2'); - itSelects('[!foo]', 'n1n2', 'nparentLoop', 'nparent'); - itSelects('[^foo]', 'nparent', 'nparentLoop'); + itSelects('[!foo]', 'n1n2', 'nparentLoop', 'nparent', 'nparent2'); + itSelects('[^foo]', 'nparent', 'nparentLoop', 'nparent2'); itSelects('[foo = "one"]', 'n1'); - itSelects('[foo != "one"]', 'n2', 'nparent', 'n1n2', 'nparentLoop'); + itSelects('[foo != "one"]', 'n2', 'nparent', 'n1n2', 'nparent2', 'nparentLoop'); itSelects('[foo > "one"]', 'n2'); itSelects('[foo < "two"]', 'n1'); itSelects('[foo <= "two"]', 'n1', 'n2'); @@ -97,14 +81,14 @@ describe('Selectors', function(){ itSelects('[foo $= "e"]', 'n1'); itSelects('[foo @= "ONE"]', 'n1'); itSelects('[weight = 2]', 'n2'); - itSelects('[weight != 2]', 'n1', 'nparent', 'n1n2', 'nparentLoop'); + itSelects('[weight != 2]', 'n1', 'nparent', 'nparent2', 'n1n2', 'nparentLoop'); itSelects('[weight > 2]', 'nparent'); itSelects('[weight >= 2]', 'nparent', 'n2'); itSelects('[weight < 2]', 'n1', 'n1n2'); itSelects('[weight <= 2]', 'n1', 'n2', 'n1n2'); itSelects('[weight !< 2]', 'n2', 'nparent'); itSelects('[emptystr = ""]', 'n1'); - itSelects('[emptystr != ""]', 'n2', 'nparent', 'n1n2', 'nparentLoop'); + itSelects('[emptystr != ""]', 'n2', 'nparent', 'nparent2', 'n1n2', 'nparentLoop'); // metadata itSelects('[[degree = 1]]', 'n1', 'n2'); @@ -113,28 +97,37 @@ describe('Selectors', function(){ // selection itSelects(':selected', 'n1'); - itSelects(':unselected', 'n2', 'n1n2', 'nparent', 'nparentLoop'); - itSelects(':selectable', 'n1', 'nparent', 'n1n2', 'nparentLoop'); + itSelects(':unselected', 'n2', 'n1n2', 'nparent', 'nparent2', 'nparentLoop'); + itSelects(':selectable', 'n1', 'nparent', 'nparent2', 'n1n2', 'nparentLoop'); itSelects(':unselectable', 'n2'); // locking itSelects(':locked', 'nparent'); - itSelects(':unlocked', 'n1', 'n2', 'n1n2', 'nparentLoop'); + itSelects(':unlocked', 'n1', 'n2', 'nparent2', 'n1n2', 'nparentLoop'); // visible - itSelects(':visible', 'n1', 'n2', 'nparent', 'nparentLoop'); + itSelects(':visible', 'n1', 'n2', 'nparent', 'nparent2', 'nparentLoop'); itSelects(':hidden', 'n1n2'); itSelects(':transparent', 'n1n2'); // compound - itSelects(':parent', 'nparent'); + itSelects(':parent', 'nparent', 'nparent2'); itSelects(':childless', 'n1', 'n2'); - itSelects(':child', 'n2'); - itSelects(':nonorphan', 'n2'); - itSelects(':orphan', 'n1', 'nparent'); - itSelects('#nparent node', 'n2'); + itSelects(':child', 'n2', 'nparent'); + itSelects(':nonorphan', 'n2', 'nparent'); + itSelects(':orphan', 'n1', 'nparent2'); itSelects('#nparent > node', 'n2'); - itSelects('$node > node', 'nparent'); + itSelects('#nparent node', 'n2'); + itSelects('$node > node', 'nparent', 'nparent2'); + itSelects('$node node', 'nparent', 'nparent2'); + itSelects('node > $node > node', 'nparent'); + itSelects('node $node node', 'nparent'); + itSelects('$node > node > node', 'nparent2'); + itSelects('$node node node', 'nparent2'); + itSelects('node > node > $node', 'n2'); + itSelects('node node $node', 'n2'); + itSelects('node > node > node', 'n2'); + itSelects('node node node', 'n2'); // edges itSelects(':loop', 'nparentLoop'); @@ -147,6 +140,10 @@ describe('Selectors', function(){ itSelects('node <-> #n1', 'n1n2'); itSelects('node <-> [foo = "one"]', 'n1n2'); itSelects('node <-> [weight = 1]', 'n1n2'); + itSelects('#n2 <-> $node', 'n1'); + itSelects('$node <-> #n1', 'n2'); + itSelects('$node -> #n2', 'n1'); + itSelects('#n1 -> $node', 'n2'); // metachars itSelects('[weird\\.name = 1]', 'n1');