blossom
Version:
Modern, Cross-Platform Application Framework
312 lines (226 loc) • 7.6 kB
JavaScript
// ==========================================================================
// 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() {
arguments.callee.base.apply(this, arguments);
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 "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 "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 false;
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 false; }
});
/** @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 false;
return token ? token.match() : true;
}
});
/** @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 false;
while (part = this.owner._pop()) {
if (part === start) {
return token ? token.match() : true;
}
}
return false;
}
});
/** @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 false;
for (; i < len; i += 1) {
if (substates[i].get('name') === part) return true;
}
return false;
}
});