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