2011-07-29 18:48:43 +00:00
|
|
|
// vim: et:ts=4:sw=4:sts=4:ft=javascript
|
2011-04-22 22:03:10 +00:00
|
|
|
|
2011-09-20 21:50:00 +00:00
|
|
|
/*@
|
|
|
|
Ox.URL <f> URL controller
|
|
|
|
(options) -> <o> URL controller
|
|
|
|
options <o> Options object
|
|
|
|
findKeys <[o]> Find keys {id: "", type: ""}
|
|
|
|
type can be "string" or "number"
|
|
|
|
getItemId <f> Tests if a string matches an item
|
|
|
|
(string, callback) -> <u> undefined
|
|
|
|
string <s> The string to be tested
|
|
|
|
callback <f> callback function
|
|
|
|
id <s> Matching item id, or empty
|
|
|
|
getSpanId <f> Tests if a string matches a span
|
|
|
|
(string, callback) -> <u> undefined
|
|
|
|
string <s> The string to be tested
|
|
|
|
callback <f> callback function
|
|
|
|
id <s> Matching span id, or empty
|
|
|
|
pages <[s]> List of pages
|
|
|
|
sortKeys <[o]> Sort keys {id: "", operator: ""}
|
|
|
|
operator is the default operator ("+" or "-")
|
|
|
|
types <[s]> List of types
|
|
|
|
views <o> List of views {type: {'list': [...], 'item': [...]}}
|
|
|
|
@*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
example.com[/page]
|
|
|
|
or
|
|
|
|
example.com[/type][/item][/view][/span][/sort][/find]
|
|
|
|
|
|
|
|
page Special page, like "about" or "contact"
|
|
|
|
type Section a.k.a. item type, like "movies", "edits", "texts" etc.
|
|
|
|
item Item id or title, like in '/movies/0060304', '/movies/inception' or
|
|
|
|
'texts/ABC'. Testing this is asynchonous.
|
|
|
|
view List or item view, like "clips" or "map". Both list and item views are
|
|
|
|
per type.
|
|
|
|
span Position or selection in a view, either one or two coordinates or one
|
|
|
|
id, like in "video/01:00", "video/-01:00", "video/01:00,-01:00",
|
|
|
|
"video/annotationABC", "video/subtitles:23", "text/chapter42",
|
|
|
|
"map/0,0", "map/-45,-90,45,90", "map/Barcelona", "image/100,100" etc.
|
|
|
|
Testing id is asynchronous.
|
|
|
|
sort Sort, like "title" or "-director" or "country,year,-language,+runtime"
|
|
|
|
find Query, like a=x or a=x&b=y or a=x&(b=y|c=z). A query object has the form
|
|
|
|
{conditions: [], operator: ''} (logical operator), and a condition
|
|
|
|
object has the form {key: '', value: '' or ['', ''], operator: ''}
|
|
|
|
(comparison operator) or {conditions: [], operator: ''} (logical
|
|
|
|
operator). Condition strings can be more than just "k=v", see below.
|
|
|
|
|
|
|
|
String Key Value Operator
|
|
|
|
v * v = any text or string contains or any number is
|
|
|
|
!v * v != no text or string contains and no number is
|
|
|
|
k=v k v = contains (text or string), is (number)
|
|
|
|
k!=v k v != does not contain (text or string), is not (number)
|
|
|
|
k==v k v == is (string)
|
|
|
|
k!==v k v !== is not (string)
|
|
|
|
k=v* k v ^ starts with (string)
|
|
|
|
k!=v* k v !^ does not start with (string)
|
|
|
|
k=*v k v $ ends with (string)
|
|
|
|
k!=*v k v !$ does not end with (string)
|
|
|
|
k<v k v < is less than (number)
|
|
|
|
k!<v k v !< is not less than (number)
|
|
|
|
k>v k v > is more than (number)
|
|
|
|
k!>v k v !> is not more than (number)
|
|
|
|
k=v:w k [v,w] = is between (number), is (string)
|
|
|
|
k!=v:w k [v,w] != is not between (number), is not (string)
|
|
|
|
|
|
|
|
All parts of the URL can be omitted, as long as the order is preserved.
|
|
|
|
|
|
|
|
example.com/foo
|
|
|
|
If "foo" is not a type, item (of the default type), view (of the default
|
|
|
|
type), span id (of the default type's default view) or sort key (of the
|
|
|
|
default type's default view), then this means find *=foo
|
|
|
|
example.com/title, or example.com/+title or example.com/-title
|
|
|
|
If this neither matches a type or default type item, then this will be sort
|
|
|
|
example.com/clip/+duration/title=foo
|
|
|
|
If "clip" is a default type list view, this will show all clips of items
|
|
|
|
that match title=foo, sorted by item duration in ascending order
|
|
|
|
example.com/clip/+clip.duration/subtitles=foo
|
|
|
|
If "clip" is a default type list view and "subtitles" is an annotation type,
|
|
|
|
this will show all clips that match subtitles=foo, sorted by clip duration
|
|
|
|
in ascending order. (In pan.do/ra's clip view, annotation=foo is always per
|
|
|
|
clip. There is no way to show all clips of all items where any clip matches
|
|
|
|
subtitles=foo, this doesn't seem to be needed.)
|
|
|
|
example.com/map/Paris/duration/title!=london
|
|
|
|
Ff "map" is a default type list view and "Paris" is a place id, this will
|
|
|
|
zoom the map to Paris, show all places of items that match title!=london,
|
|
|
|
and when a place is selected sort matching clips by item duration in
|
|
|
|
default order.
|
|
|
|
example.com/calendar/1900,2000/clip.duration/event=hiroshima
|
|
|
|
If "calendar" is a default type list view, this will zoom the calendar to
|
|
|
|
the 20th century, show all events of all items that match event=hiroshima,
|
|
|
|
and when an event is selected sort matching clips by clip duration in
|
|
|
|
default order. (In pan.do/ra's map and calendar view, annotation=foo is
|
|
|
|
always per item. There is no way to show all events of all clips that match
|
|
|
|
event=hiroshima, this doesn't seem to be needed.)
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
Ox.URL = function(options) {
|
|
|
|
|
|
|
|
var self = {}, that = {};
|
|
|
|
|
|
|
|
self.options = Ox.extend({
|
|
|
|
findKeys: [],
|
|
|
|
getItemId: null,
|
|
|
|
getSpanId: null,
|
|
|
|
pages: [],
|
|
|
|
sortKeys: [],
|
|
|
|
types: [],
|
|
|
|
views: {}
|
|
|
|
}, options);
|
|
|
|
|
|
|
|
self.sortKeyIds = self.options.sortKeys.map(function(sortKey) {
|
|
|
|
return sortKey.id;
|
|
|
|
});
|
|
|
|
|
|
|
|
function constructCondition(condition) {
|
|
|
|
var key = condition.key == '*' ? '' : condition.key,
|
|
|
|
operator = condition.operator,
|
|
|
|
value = (
|
|
|
|
Ox.isArray(condition.value) ? condition.value : [condition.value]
|
|
|
|
).map(encodeValue).join(':');
|
|
|
|
if (!key) {
|
|
|
|
operator = operator.replace('=', '');
|
|
|
|
} else if (operator.indexOf('^') > -1) {
|
|
|
|
operator = operator.replace('^', '=');
|
|
|
|
value += '*';
|
|
|
|
} else if (operator.indexOf('$') > -1) {
|
|
|
|
operator = operator.replace('$', '=');
|
|
|
|
value = '*' + value;
|
|
|
|
}
|
|
|
|
return [key, operator, value].join('');
|
|
|
|
}
|
|
|
|
|
|
|
|
function constructDuration(duration) {
|
|
|
|
return Ox.formatDuration(duration, 3).replace(/\.000$/, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
function constructFind(find) {
|
|
|
|
return find.conditions.map(function(condition) {
|
|
|
|
var ret;
|
|
|
|
if (condition.conditions) {
|
|
|
|
ret = '(' + constructFind(condition) + ')';
|
|
|
|
} else {
|
|
|
|
ret = constructCondition(condition);
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}).join(find.operator);
|
|
|
|
}
|
|
|
|
|
|
|
|
function constructSort(sort) {
|
|
|
|
return sort.map(function(sort) {
|
|
|
|
return (
|
|
|
|
sort.operator == Ox.getObjectById(self.options.sortKeys, sort.key).operator
|
|
|
|
? '' : sort.operator
|
|
|
|
) + sort.key;
|
|
|
|
}).join(',')
|
|
|
|
}
|
|
|
|
|
|
|
|
function constructSpan(span) {
|
|
|
|
return span.map(function(point) {
|
|
|
|
return /^[0-9-\.:]+$/.test(str) ? constuctDuration(point) : point;
|
|
|
|
}).join(',');
|
|
|
|
}
|
|
|
|
|
|
|
|
function constructURL(state) {
|
|
|
|
var parts = [];
|
|
|
|
if (self.options.types.indexOf(state.type) > 0) {
|
|
|
|
parts.push(state.type);
|
|
|
|
}
|
|
|
|
if (state.item) {
|
|
|
|
parts.push(item);
|
|
|
|
}
|
|
|
|
if (self.options.views[state.type][
|
|
|
|
state.item ? 'item' : 'list'
|
|
|
|
].indexOf(state.view) > 0) {
|
|
|
|
parts.push(state.view);
|
|
|
|
}
|
|
|
|
if (state.span) {
|
|
|
|
parts.push(constructSpan(state.span));
|
|
|
|
}
|
|
|
|
if (state.sort.length) {
|
|
|
|
parts.push(constructSort(state.sort));
|
|
|
|
}
|
|
|
|
if (state.find) {
|
|
|
|
parts.push(constructFind(state.find));
|
|
|
|
}
|
|
|
|
return parts.join('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
function decodeValue(str) {
|
|
|
|
return decodeURIComponent(str);
|
|
|
|
}
|
|
|
|
|
|
|
|
function encodeValue(str) {
|
|
|
|
var chars = '/&|()=*:';
|
|
|
|
ret = '';
|
|
|
|
str.split('').forEach(function(char) {
|
|
|
|
var index = chars.indexOf(char);
|
|
|
|
ret += index > -1
|
|
|
|
? '%' + char.charCodeAt(0).toString(16).toUpperCase()
|
|
|
|
: char;
|
|
|
|
});
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseCondition(str) {
|
|
|
|
var condition,
|
|
|
|
operators = ['!==', '==', '!=', '=', '!<', '<', '!>', '>'],
|
|
|
|
split;
|
|
|
|
Ox.forEach(operators, function(operator) {
|
|
|
|
if (str.indexOf(operator) > - 1) {
|
|
|
|
split = str.split(operator);
|
|
|
|
condition = {
|
|
|
|
key: split.shift(),
|
|
|
|
value: split.join(operator),
|
|
|
|
operator: operator
|
|
|
|
};
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
if (!condition.operator) {
|
|
|
|
condition = {key: '*', value: str, operator: '='};
|
|
|
|
}
|
|
|
|
if (['=', '!='].indexOf(condition.operator) > -1) {
|
|
|
|
if (condition.value[0] == '*') {
|
|
|
|
condition.value = condition.value.substr(1);
|
|
|
|
condition.operator = condition.operator.replace('=', '$')
|
|
|
|
} else if (condition.value[condition.value.length - 1] == '*') {
|
|
|
|
condition.value = condition.value.substr(0, condition.value.length - 1);
|
|
|
|
condition.operator = condition.operator.replace('=', '^')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (condition.value.indexOf(':') > -1) {
|
|
|
|
condition.value = condition.value.split(':')
|
|
|
|
}
|
|
|
|
return condition;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseDuration(str) {
|
|
|
|
var parts = str.split(':').reverse();
|
|
|
|
while (parts.length > 3) {
|
|
|
|
parts.pop();
|
|
|
|
}
|
|
|
|
return parts.reduce(function(prev, curr, i) {
|
|
|
|
return prev + (parseFloat(curr) || 0) * Math.pow(60, i);
|
|
|
|
}, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseFind(str) {
|
|
|
|
var conditions, counter = 0,
|
|
|
|
find = {conditions: [], operator: '&'},
|
|
|
|
subconditions = [];
|
|
|
|
if (str.length) {
|
|
|
|
// replace subconditions with placeholder,
|
|
|
|
// so we can later split by main operator
|
|
|
|
Ox.forEach(str, function(c, i) {
|
|
|
|
if (c == ')') {
|
|
|
|
counter--;
|
|
|
|
}
|
|
|
|
if (counter >= 1) {
|
|
|
|
subconditions[subconditions.length - 1] += c;
|
|
|
|
}
|
|
|
|
if (c == '(') {
|
|
|
|
(++counter == 1) && subconditions.push('');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
subconditions.forEach(function(subcondition, i) {
|
|
|
|
str = str.replace(subcondition, i);
|
|
|
|
});
|
|
|
|
find.operator = str.indexOf('|') > -1 ? '|' : '&'
|
|
|
|
find.conditions = str.split(find.operator).map(function(condition, i) {
|
|
|
|
var ret;
|
|
|
|
if (condition[0] == '(') {
|
|
|
|
// re-insert subcondition
|
|
|
|
ret = parseFind(subconditions[parseInt(Ox.sub(condition, 1, -1))]);
|
|
|
|
} else {
|
|
|
|
ret = parseCondition(condition);
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return find;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseSort(str) {
|
|
|
|
return str.split(',').map(function(str) {
|
|
|
|
var hasOperator = /^[\+-]/.test(str);
|
|
|
|
return {
|
|
|
|
key: hasOperator ? str.substr(1) : str,
|
|
|
|
operator: hasOperator ? str[0] : Ox.getObjectById(self.options.sortKeys, str).operator
|
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseSpan(str, callback) {
|
|
|
|
return str.split(',').map(parseDuration);
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseURL(str, callback) {
|
|
|
|
str = str || document.location.pathname
|
|
|
|
+ document.location.search + document.location.hash;
|
|
|
|
var parts = str.substr(1).split('/'),
|
|
|
|
state = {};
|
|
|
|
if (parts.length == 0) {
|
|
|
|
state.page = '';
|
|
|
|
} else if (self.options.pages.indexOf(parts[0]) > -1) {
|
|
|
|
state.page = parts[0];
|
|
|
|
} else {
|
|
|
|
if (self.options.types.indexOf(parts[0]) > -1) {
|
|
|
|
state.type = parts[0];
|
|
|
|
parts.shift();
|
|
|
|
}
|
|
|
|
if (parts.length) {
|
|
|
|
if (self.options.views.list.indexOf(parts[0]) > -1) {
|
|
|
|
state.item = '';
|
|
|
|
state.view = parts[0];
|
|
|
|
parts.shift();
|
|
|
|
parseBeyondItem(false);
|
|
|
|
} else {
|
|
|
|
self.options.getItemId(parts[0], function(itemId) {
|
|
|
|
if (itemId) {
|
|
|
|
state.item = itemId;
|
|
|
|
parts.shift();
|
|
|
|
}
|
|
|
|
parseBeyondItem(!!itemId);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
callback(state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function parseBeyondItem(itemId) {
|
|
|
|
if (parts.length && itemId && self.options.views.item.indexOf(parts[0]) > -1) {
|
|
|
|
state.view = parts[0];
|
|
|
|
parts.shift();
|
|
|
|
}
|
|
|
|
if (parts.length) {
|
|
|
|
if (parts.split(',').every(function(str) {
|
|
|
|
return /^[0-9-\.:]+$/.test(str);
|
|
|
|
})) {
|
|
|
|
state.span = parseSpan(parts[0]);
|
|
|
|
parts.shift()
|
|
|
|
parseBeyondSpan();
|
|
|
|
} else {
|
|
|
|
self.options.getSpanId(parts[0], function(spanId) {
|
|
|
|
if (spanId) {
|
|
|
|
state.span = spanId;
|
|
|
|
parts.shift();
|
|
|
|
}
|
|
|
|
parseBeyondSpan();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
callback(state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function parseBeyondSpan() {
|
|
|
|
if (parts.length && parts[0].split(',').every(function(str) {
|
|
|
|
return self.sortKeyIds.indexOf(str.replace(/^[\+-]/, '')) > -1;
|
|
|
|
})) {
|
|
|
|
state.sort = parseSort(parts[0]);
|
|
|
|
parts.shift();
|
|
|
|
}
|
|
|
|
if (parts.length) {
|
|
|
|
state.find = parseFind(parts.join('/'));
|
|
|
|
}
|
|
|
|
callback(state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
that._constructURL = function(state) {
|
|
|
|
return constructURL(state);
|
|
|
|
};
|
|
|
|
|
|
|
|
that.parse = function(str, callback) {
|
|
|
|
parseURL(str, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
that.pop = function() {
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
that.push = function(url) {
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
that.replace = function(url) {
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
that.update = function(state) {
|
|
|
|
// pushes a new URL, constructed from state
|
|
|
|
// state can have type, item, view, span, sort, find
|
|
|
|
};
|
|
|
|
|
|
|
|
return that;
|
|
|
|
|
|
|
|
};
|