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');