// ========================================================================== // Project: SC
.Statechart - A Statechart Framework for SproutCore
// Copyright: ©2010, 2011 Michael Cohen, and contributors. // Portions @2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ==========================================================================
/*globals SC
*/
/** @class
The `SC.StatePathMatcher` is used to match a given state path match expression against state paths. A state path is a basic dot-notion consisting of one or more state names joined using '.'. Ex: 'foo', 'foo.bar'. The state path match expression language provides a way of expressing a state path. The expression is matched against a state path from the end of the state path to the beginning of the state path. A match is true if the expression has been satisfied by the given path. Syntax: expression -> <this> <subpath> | <path> path -> <part> <subpath> subpath -> '.' <part> <subpath> | empty this -> 'this' part -> <name> | <expansion> expansion -> <name> '~' <name> name -> [a-z_][\w]* Expression examples: foo foo.bar foo.bar.mah foo~mah this.foo this.foo.bar this.foo~mah foo.bar~mah foo~bar.mah @extends SC.Object @author Michael Cohen
*/ SC
.StatePathMatcher = SC
.Object.extend(
/** @scope SC.StatePathMatcher.prototype */{ /** The state that is used to represent 'this' for the matcher's given expression. @field {SC.State} @see #expression */ state: null, /** The expression used by this matcher to match against given state paths @field {String} */ expression: null, /** A parsed set of tokens from the matcher's given expression @field {Array} @see #expression */ tokens: null, init: function() { sc_super(); this._parseExpression(); }, /** @private Will parse the matcher's given expession by creating tokens and chaining them together. Note: Because the DSL for state path expressions is tiny, a simple hand-crafted parser is being used. However, if the DSL becomes any more complex, then it will probably be necessary to refactor the logic in order follow a more conventional type of parser. @see #expression */ _parseExpression: function() { var parts = this.expression ? this.expression.split('.') : [], len = parts.length, i = 0, part, chain = null, token, tokens = []; for (; i < len; i += 1) { part = parts[i]; if (part.indexOf('~') >= 0) { part = part.split('~'); if (part.length > 2) { throw new Error("Invalid use of '~' at part %@".fmt(i)); } token = SC.StatePathMatcher._ExpandToken.create({ start: part[0], end: part[1] }); } else if (part === 'this') { if (tokens.length > 0) { throw new Error("Invalid use of 'this' at part %@".fmt(i)); } token = SC.StatePathMatcher._ThisToken.create(); } else { token = SC.StatePathMatcher._BasicToken.create({ value: part }); } token.owner = this; tokens.push(token); } this.set('tokens', tokens); var stack = SC.clone(tokens); this._chain = chain = stack.pop(); while (token = stack.pop()) { chain.nextToken = token; chain = token; } }, /** Returns the last part of the expression. So if the expression is 'foo.bar' or 'foo~bar' then 'bar' is returned in both cases. If the expression is 'this' then 'this is returned. */ lastPart: function() { var tokens = this.get('tokens'), len = tokens ? tokens.length : 0, token = len > 0 ? tokens[len -1] : null; return token.get('lastPart'); }.property('tokens').cacheable(), /** Will make a state path against this matcher's expression. The path provided must follow a basic dot-notation path containing one or dots '.'. Ex: 'foo', 'foo.bar' @param path {String} a dot-notation path @return {Boolean} true if there is a match, otherwise false */ match: function(path) { this._stack = path.split('.'); if (SC.empty(path) || SC.typeOf(path) !== SC.T_STRING) return NO; return this._chain.match(); }, /** @private */ _pop: function() { this._lastPopped = this._stack.pop(); return this._lastPopped; }
});
/** @private @class
Base class used to represent a token the expression
*/ SC
.StatePathMatcher._Token = SC
.Object.extend({
/** The type of this token */ type: null, /** The state path matcher that owns this token */ owner: null, /** The next token in the matching chain */ nextToken: null, /** The last part the token represents, which is either a valid state name or representation of a state */ lastPart: null, /** Used to match against what is currently on the owner's current path stack */ match: function() { return NO; }
});
/** @private @class
Represents a basic name of a state in the expression. Ex 'foo'. A match is true if the matcher's current path stack is popped and the result matches this token's value.
*/ SC
.StatePathMatcher._BasicToken = SC
.StatePathMatcher._Token.extend({
type: 'basic', value: null, lastPart: function() { return this.value; }.property('value').cacheable(), match: function() { var part = this.owner._pop(), token = this.nextToken; if (this.value !== part) return NO; return token ? token.match() : YES; }
});
/** @private @class
Represents an expanding path based on the use of the '<start>~<end>' syntax. <start> represents the start and <end> represents the end. A match is true if the matcher's current path stack is first popped to match <end> and eventually is popped to match <start>. If neither <end> nor <start> are satified then false is retuend.
*/ SC
.StatePathMatcher._ExpandToken = SC
.StatePathMatcher._Token.extend({
type: 'expand', start: null, end: null, lastPart: function() { return this.end; }.property('end').cacheable(), match: function() { var start = this.start, end = this.end, part, token = this.nextToken; part = this.owner._pop(); if (part !== end) return NO; while (part = this.owner._pop()) { if (part === start) { return token ? token.match() : YES; } } return NO; }
});
/** @private @class
Represents a this token, which is used to represent the owner's `state` property. A match is true if the last path part popped from the owner's current path stack is an immediate substate of the state this token represents.
*/ SC
.StatePathMatcher._ThisToken = SC
.StatePathMatcher._Token.extend({
type: 'this', lastPart: 'this', match: function() { var state = this.owner.state, substates = state.get('substates'), len = substates.length, i = 0, part; part = this.owner._lastPopped; if (!part || this.owner._stack.length !== 0) return NO; for (; i < len; i += 1) { if (substates[i].get('name') === part) return YES; } return NO; }
});