1 // ==========================================================================
  2 // Project:   SC.Statechart - A Statechart Framework for SproutCore
  3 // Copyright: ©2010, 2011 Michael Cohen, and contributors.
  4 //            Portions @2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /*globals SC */
  9 
 10 /** @class
 11 
 12   The `SC.StatePathMatcher` is used to match a given state path match expression 
 13   against state paths. A state path is a basic dot-notion consisting of
 14   one or more state names joined using '.'. Ex: 'foo', 'foo.bar'. 
 15   
 16   The state path match expression language provides a way of expressing a state path.
 17   The expression is matched against a state path from the end of the state path
 18   to the beginning of the state path. A match is true if the expression has been
 19   satisfied by the given path. 
 20   
 21   Syntax:
 22   
 23     expression -> <this> <subpath> | <path>
 24     
 25     path -> <part> <subpath>
 26     
 27     subpath -> '.' <part> <subpath> | empty
 28   
 29     this -> 'this'
 30     
 31     part -> <name> | <expansion>
 32     
 33     expansion -> <name> '~' <name>
 34     
 35     name -> [a-z_][\w]*
 36     
 37   Expression examples:
 38   
 39     foo
 40     
 41     foo.bar
 42     
 43     foo.bar.mah
 44     
 45     foo~mah
 46     
 47     this.foo
 48     
 49     this.foo.bar
 50     
 51     this.foo~mah
 52     
 53     foo.bar~mah
 54     
 55     foo~bar.mah
 56 
 57   @extends SC.Object
 58   @author Michael Cohen
 59 */
 60 SC.StatePathMatcher = SC.Object.extend(
 61   /** @scope SC.StatePathMatcher.prototype */{
 62     
 63   /**
 64     The state that is used to represent 'this' for the
 65     matcher's given expression.
 66     
 67     @field {SC.State}
 68     @see #expression
 69   */
 70   state: null,
 71     
 72   /**
 73     The expression used by this matcher to match against
 74     given state paths
 75     
 76     @field {String}
 77   */
 78   expression: null,
 79   
 80   /**
 81     A parsed set of tokens from the matcher's given expression
 82     
 83     @field {Array}
 84     @see #expression
 85   */
 86   tokens: null,
 87   
 88   init: function() {
 89     sc_super();
 90     this._parseExpression();
 91   },
 92   
 93   /** @private 
 94   
 95     Will parse the matcher's given expession by creating tokens and chaining them
 96     together.
 97     
 98     Note: Because the DSL for state path expressions is tiny, a simple hand-crafted 
 99     parser is being used. However, if the DSL becomes any more complex, then it will 
100     probably be necessary to refactor the logic in order follow a more conventional 
101     type of parser.
102     
103     @see #expression
104   */
105   _parseExpression: function() {
106     var parts = this.expression ? this.expression.split('.') : [],
107         len = parts.length, i = 0, part,
108         chain = null, token, tokens = [];
109       
110     for (; i < len; i += 1) {
111       part = parts[i];      
112       
113       if (part.indexOf('~') >= 0) {
114         part = part.split('~');
115         if (part.length > 2) {
116           throw new Error("Invalid use of '~' at part %@".fmt(i));
117         }
118         token = SC.StatePathMatcher._ExpandToken.create({
119           start: part[0], end: part[1]
120         });
121       } 
122       
123       else if (part === 'this') {
124         if (tokens.length > 0) {
125           throw new Error("Invalid use of 'this' at part %@".fmt(i));
126         }
127         token = SC.StatePathMatcher._ThisToken.create();
128       }
129       
130       else {
131         token = SC.StatePathMatcher._BasicToken.create({
132           value: part
133         });
134       }
135       
136       token.owner = this;
137       tokens.push(token);
138     }
139 
140     this.set('tokens', tokens);
141 
142     var stack = SC.clone(tokens);
143     this._chain = chain = stack.pop();
144     while (token = stack.pop()) {
145       chain.nextToken = token;
146       chain = token;
147     }
148   },
149   
150   /**
151     Returns the last part of the expression. So if the
152     expression is 'foo.bar' or 'foo~bar' then 'bar' is returned
153     in both cases. If the expression is 'this' then 'this is
154     returned. 
155   */
156   lastPart: function() {
157     var tokens = this.get('tokens'),
158         len = tokens ? tokens.length : 0,
159         token = len > 0 ? tokens[len -1] : null;
160     return token.get('lastPart');
161   }.property('tokens').cacheable(),
162     
163   /**
164     Will make a state path against this matcher's expression. 
165     
166     The path provided must follow a basic dot-notation path containing
167     one or dots '.'. Ex: 'foo', 'foo.bar'
168     
169     @param path {String} a dot-notation path
170     @return {Boolean} true if there is a match, otherwise false
171   */
172   match: function(path) {
173     this._stack = path.split('.');
174     if (SC.empty(path) || SC.typeOf(path) !== SC.T_STRING) return NO;
175     return this._chain.match();
176   },
177   
178   /** @private */
179   _pop: function() {
180     this._lastPopped = this._stack.pop();
181     return this._lastPopped;
182   }
183 
184 });
185 
186 /** @private @class
187 
188   Base class used to represent a token the expression
189 */
190 SC.StatePathMatcher._Token = SC.Object.extend({
191   
192   /** The type of this token */
193   type: null,
194   
195   /** The state path matcher that owns this token */
196   owner: null,
197   
198   /** The next token in the matching chain */
199   nextToken: null,
200   
201   /** 
202     The last part the token represents, which is either a valid state
203     name or representation of a state
204   */
205   lastPart: null,
206   
207   /** 
208     Used to match against what is currently on the owner's
209     current path stack
210   */
211   match: function() { return NO; }
212   
213 });
214 
215 /** @private @class
216 
217   Represents a basic name of a state in the expression. Ex 'foo'. 
218   
219   A match is true if the matcher's current path stack is popped and the
220   result matches this token's value.
221 */
222 SC.StatePathMatcher._BasicToken = SC.StatePathMatcher._Token.extend({
223     
224   type: 'basic',
225     
226   value: null,
227    
228   lastPart: function() {
229     return this.value; 
230   }.property('value').cacheable(),
231     
232   match: function() {
233     var part = this.owner._pop(),
234         token = this.nextToken;
235     if (this.value !== part) return NO;
236     return token ? token.match() : YES;
237   }
238     
239 });
240   
241 /** @private @class
242 
243   Represents an expanding path based on the use of the '<start>~<end>' syntax.
244   <start> represents the start and <end> represents the end. 
245   
246   A match is true if the matcher's current path stack is first popped to match 
247   <end> and eventually is popped to match <start>. If neither <end> nor <start>
248   are satified then false is retuend.
249 */
250 SC.StatePathMatcher._ExpandToken = SC.StatePathMatcher._Token.extend({
251     
252   type: 'expand',
253     
254   start: null,
255     
256   end: null,
257   
258   lastPart: function() {
259     return this.end; 
260   }.property('end').cacheable(),
261 
262   match: function() {
263     var start = this.start,
264         end = this.end, part,
265         token = this.nextToken;
266           
267     part = this.owner._pop();
268     if (part !== end) return NO;
269       
270     while (part = this.owner._pop()) {
271       if (part === start) {
272         return token ? token.match() : YES;
273       }
274     }
275       
276     return NO;
277   }
278     
279 });
280 
281 /** @private @class
282   
283   Represents a this token, which is used to represent the owner's
284   `state` property.
285   
286   A match is true if the last path part popped from the owner's
287   current path stack is an immediate substate of the state this
288   token represents.
289 */
290 SC.StatePathMatcher._ThisToken = SC.StatePathMatcher._Token.extend({
291     
292   type: 'this',
293   
294   lastPart: 'this',
295   
296   match: function() {
297     var state = this.owner.state,
298         substates = state.get('substates'),
299         len = substates.length, i = 0, part;
300         
301     part = this.owner._lastPopped;
302 
303     if (!part || this.owner._stack.length !== 0) return NO;
304     
305     for (; i < len; i += 1) {
306       if (substates[i].get('name') === part) return YES;
307     }
308     
309     return NO;
310   }
311   
312 });