blob: b8e2481877af3d00bbf508b5d687ec239d4c993f [file] [log] [blame]
// This is a helper script designed to parse the computed syntax of the custom filter.
// Note that it is generic enough so that it can be used for other properties in the future.
function Token(type)
{
this.type = type;
}
// Checks if the type of the token is in the list of arguments passed to the function.
// It also accepts arrays and other type checking functions.
Token.prototype.isA = function()
{
for (var i = 0; i < arguments.length; ++i) {
var type = arguments[i];
if ((typeof type == "object" && type.length && this.isA.apply(this, type))
|| (typeof type == "function" && type(this))
|| type == this.type)
return true;
}
return false;
}
// Creates a new token object and copies all the properties in "value" to the new token.
function createToken(type, value) {
var token = new Token(type);
if (value) {
for (var i in value)
if (value.hasOwnProperty(i))
token[i] = value[i];
}
return token;
}
// Tokenizes the string into Tokens of types [urls, floats, integers, keywords, ",", "(", ")" and "."].
function tokenizeString(s)
{
var tokenizer = new RegExp([
"url\\(\\s*(.*?)\\s*\\)", // url() - 1
"((?:[\\+-]?)\\d*\\.\\d+)", // floats - 2
"((?:[\\+-]?)\\d+)", // integers - 3
"([\\w-][\\w\\d-]*)", // keywords - 4
"([,\\.\\(\\)])" // punctuation - 5
].join("|"), "g");
var match, tokens = [];
while (match = tokenizer.exec(s)) {
if (match[1] !== undefined)
tokens.push(createToken("url", {value: match[1]}));
else if (match[2] !== undefined)
tokens.push(createToken("float", {value: parseFloat(match[2])}));
else if (match[3] !== undefined)
tokens.push(createToken("integer", {value: parseInt(match[3])}));
else if (match[4] !== undefined)
tokens.push(createToken("keyword", {value: match[4]}));
else if (match[5] !== undefined)
tokens.push(createToken(match[5]));
}
return tokens;
}
// Checks if the token is a number. Can be used in combination with the "Token.isA" method.
function number(token)
{
return token.type == "float" || token.type == "integer";
}
// Helper class to iterate on the token stream. It will add an "end"
// token at the end to make it easier to use the "ahead" function
// without checking if it's the last token in the stream.
function TokenStream(tokens) {
this.tokens = tokens;
this.tokens.push(new Token("end"));
this.tokenIndex = 0;
}
TokenStream.prototype.current = function()
{
return this.tokens[this.tokenIndex];
}
TokenStream.prototype.ahead = function()
{
return this.tokens[this.tokenIndex + 1];
}
// Skips the current token only if it matches the "typeMatcher". Otherwise it throws an error.
TokenStream.prototype.skip = function(typeMatcher)
{
if (!typeMatcher || this.current().isA(typeMatcher)) {
var token = this.current();
++this.tokenIndex;
return token;
}
throw new Error("Cannot use " + JSON.stringify(typeMatcher) + " to skip over " + JSON.stringify(this.ahead()));
}
// Skips the current token, only if it matches the "typeMatcher". Makes it easy to skip over comma tokens.
TokenStream.prototype.skipIfNeeded = function(typeMatcher)
{
if (this.ahead() && this.ahead().isA(typeMatcher))
++this.tokenIndex;
}
// Creates a new "function" node. It expects that the current token is the function name.
function parseFunction(m)
{
var functionObject = {
type: "function",
name: m.skip().value
};
m.skip("(");
functionObject.arguments = parseList(m, [")", "end"]);
m.skip(")");
return functionObject;
}
// Creates a new "parameter" node. It expects that the current token is the parameter name.
// It consumes everything before the following comma.
function parseParameter(m)
{
var functionObject = {
type: "parameter",
name: m.skip().value
};
functionObject.value = parseList(m, [",", "end"]);
m.skipIfNeeded(",");
return functionObject;
}
// Consumes a list of tokens before reaching the "endToken" and returns an array with all the parsed items.
// Makes the following assumptions:
// - if a keyword is followed by "(" then it is a start of function
// - if a keyword is not followed by "," it is a parameter
// Keywords that do not match either of the previous rules and tokens like number and url are just cloned.
function parseList(m, endToken)
{
var result = [], token;
while ((token = m.current()) && !token.isA(endToken)) {
if (token.isA("keyword")) {
if (m.ahead().isA("("))
result.push(parseFunction(m));
else if (m.ahead().isA(",", endToken)) {
result.push({
type: "keyword",
value: m.skip().value
});
} else
result.push(parseParameter(m));
} else if (token.isA(number)) {
result.push({
type: "number",
value: m.skip().value
});
} else if (token.isA("url")) {
result.push({
type: "url",
value: m.skip().value
});
} else if (token.isA(","))
m.skip();
else
throw "Unexpected token " + JSON.stringify(token) + " in a list.";
}
return result;
}
function tokensToValues(tokens)
{
var m = new TokenStream(tokens);
return parseList(m, "end");
}
// Extracts a parameters array from the parameters string of the custom filter function.
function parseCustomFilterParameters(s)
{
return tokensToValues(tokenizeString(s));
}
// Need to remove the base URL to avoid having local paths in the expected results.
function removeBaseURL(src) {
var urlRegexp = /url\(([^\)]*)\)/g;
return src.replace(urlRegexp, function(match, url) {
return "url(" + url.substr(url.lastIndexOf("/") + 1) + ")";
});
}
// Parses the parameters of the custom filter function and returns it using the following order:
// - If parameters have different types (ie. url, number, named parameter), the alphabetical order of the "type" is used.
// - If parameters are of type "parameter" the alphabetical order of the parameter names is used.
// - If parameters are of other types, then the value is used to order them alphabetical.
// The order is important to make it easy to compare two custom filters, that have exactly the same parameters,
// but with potentially different "named parameter" values.
function getCustomFilterParameters(s)
{
return parseCustomFilterParameters(removeBaseURL(s)).sort(function(a, b) {
if (a.type != b.type)
return a.type.localeCompare(b.type);
if (a.type == "parameter")
return a.name.localeCompare(b.name);
return a.value.toString().localeCompare(b.value.toString());
});
}