UNPKG

6.42 kBJavaScriptView Raw
1import { warn } from '../util';
2import * as is from '../is';
3import exprs from './expressions';
4import newQuery from './new-query';
5import Type from './type';
6
7/**
8 * Of all the expressions, find the first match in the remaining text.
9 * @param {string} remaining The remaining text to parse
10 * @returns The matched expression and the newly remaining text `{ expr, match, name, remaining }`
11 */
12const consumeExpr = ( remaining ) => {
13 let expr;
14 let match;
15 let name;
16
17 for( let j = 0; j < exprs.length; j++ ){
18 let e = exprs[ j ];
19 let n = e.name;
20
21 let m = remaining.match( e.regexObj );
22
23 if( m != null ){
24 match = m;
25 expr = e;
26 name = n;
27
28 let consumed = m[0];
29 remaining = remaining.substring( consumed.length );
30
31 break; // we've consumed one expr, so we can return now
32 }
33 }
34
35 return {
36 expr: expr,
37 match: match,
38 name: name,
39 remaining: remaining
40 };
41};
42
43
44/**
45 * Consume all the leading whitespace
46 * @param {string} remaining The text to consume
47 * @returns The text with the leading whitespace removed
48 */
49const consumeWhitespace = ( remaining ) => {
50 let match = remaining.match( /^\s+/ );
51
52 if( match ){
53 let consumed = match[0];
54 remaining = remaining.substring( consumed.length );
55 }
56
57 return remaining;
58};
59
60/**
61 * Parse the string and store the parsed representation in the Selector.
62 * @param {string} selector The selector string
63 * @returns `true` if the selector was successfully parsed, `false` otherwise
64 */
65const parse = function( selector ){
66 let self = this;
67
68 let remaining = self.inputText = selector;
69
70 let currentQuery = self[0] = newQuery();
71 self.length = 1;
72
73 remaining = consumeWhitespace( remaining ); // get rid of leading whitespace
74
75 for( ;; ){
76 let exprInfo = consumeExpr( remaining );
77
78 if( exprInfo.expr == null ){
79 warn( 'The selector `' + selector + '`is invalid' );
80 return false;
81 } else {
82 let args = exprInfo.match.slice( 1 );
83
84 // let the token populate the selector object in currentQuery
85 let ret = exprInfo.expr.populate( self, currentQuery, args );
86
87 if( ret === false ){
88 return false; // exit if population failed
89 } else if( ret != null ){
90 currentQuery = ret; // change the current query to be filled if the expr specifies
91 }
92 }
93
94 remaining = exprInfo.remaining;
95
96 // we're done when there's nothing left to parse
97 if( remaining.match( /^\s*$/ ) ){
98 break;
99 }
100 }
101
102 let lastQ = self[self.length - 1];
103
104 if( self.currentSubject != null ){
105 lastQ.subject = self.currentSubject;
106 }
107
108 lastQ.edgeCount = self.edgeCount;
109 lastQ.compoundCount = self.compoundCount;
110
111 for( let i = 0; i < self.length; i++ ){
112 let q = self[i];
113
114 // in future, this could potentially be allowed if there were operator precedence and detection of invalid combinations
115 if( q.compoundCount > 0 && q.edgeCount > 0 ){
116 warn( 'The selector `' + selector + '` is invalid because it uses both a compound selector and an edge selector' );
117 return false;
118 }
119
120 if( q.edgeCount > 1 ){
121 warn( 'The selector `' + selector + '` is invalid because it uses multiple edge selectors' );
122 return false;
123 } else if( q.edgeCount === 1 ){
124 warn( 'The selector `' + selector + '` is deprecated. Edge selectors do not take effect on changes to source and target nodes after an edge is added, for performance reasons. Use a class or data selector on edges instead, updating the class or data of an edge when your app detects a change in source or target nodes.' );
125 }
126 }
127
128 return true; // success
129};
130
131/**
132 * Get the selector represented as a string. This value uses default formatting,
133 * so things like spacing may differ from the input text passed to the constructor.
134 * @returns {string} The selector string
135 */
136export const toString = function(){
137 if( this.toStringCache != null ){
138 return this.toStringCache;
139 }
140
141 let clean = function( obj ){
142 if( obj == null ){
143 return '';
144 } else {
145 return obj;
146 }
147 };
148
149 let cleanVal = function( val ){
150 if( is.string( val ) ){
151 return '"' + val + '"';
152 } else {
153 return clean( val );
154 }
155 };
156
157 let space = ( val ) => {
158 return ' ' + val + ' ';
159 };
160
161 let checkToString = ( check, subject ) => {
162 let { type, value } = check;
163
164 switch( type ){
165 case Type.GROUP: {
166 let group = clean( value );
167
168 return group.substring( 0, group.length - 1 );
169 }
170
171 case Type.DATA_COMPARE: {
172 let { field, operator } = check;
173
174 return '[' + field + space( clean( operator ) ) + cleanVal( value ) + ']';
175 }
176
177 case Type.DATA_BOOL: {
178 let { operator, field } = check;
179
180 return '[' + clean( operator ) + field + ']';
181 }
182
183 case Type.DATA_EXIST: {
184 let { field } = check;
185
186 return '[' + field + ']';
187 }
188
189 case Type.META_COMPARE: {
190 let { operator, field } = check;
191
192 return '[[' + field + space( clean( operator ) ) + cleanVal( value ) + ']]';
193 }
194
195 case Type.STATE: {
196 return value;
197 }
198
199 case Type.ID: {
200 return '#' + value;
201 }
202
203 case Type.CLASS: {
204 return '.' + value;
205 }
206
207 case Type.PARENT:
208 case Type.CHILD: {
209 return queryToString(check.parent, subject) + space('>') + queryToString(check.child, subject);
210 }
211
212 case Type.ANCESTOR:
213 case Type.DESCENDANT: {
214 return queryToString(check.ancestor, subject) + ' ' + queryToString(check.descendant, subject);
215 }
216
217 case Type.COMPOUND_SPLIT: {
218 let lhs = queryToString(check.left, subject);
219 let sub = queryToString(check.subject, subject);
220 let rhs = queryToString(check.right, subject);
221
222 return lhs + (lhs.length > 0 ? ' ' : '') + sub + rhs;
223 }
224
225 case Type.TRUE: {
226 return '';
227 }
228 }
229 };
230
231 let queryToString = ( query, subject ) => {
232 return query.checks.reduce((str, chk, i) => {
233 return str + (subject === query && i === 0 ? '$' : '') + checkToString(chk, subject);
234 }, '');
235 };
236
237 let str = '';
238
239 for( let i = 0; i < this.length; i++ ){
240 let query = this[ i ];
241
242 str += queryToString( query, query.subject );
243
244 if( this.length > 1 && i < this.length - 1 ){
245 str += ', ';
246 }
247 }
248
249 this.toStringCache = str;
250
251 return str;
252};
253
254export default { parse, toString };