diffusion
Version:
Diffusion JavaScript client
254 lines (214 loc) • 7.32 kB
JavaScript
var split = require('util/string').split;
var cache = require('util/memoize');
var arr = require('util/array');
// API Topic Selector
var TopicSelector = require('../../selectors/topic-selector');
// Internal topic classes
var utils = require('topics/topic-path-utils');
var PathSelector = require('topics/path-selector');
var SplitPathSelector = require('topics/split-path-selector');
var FullPathSelector = require('topics/full-path-selector');
var SelectorSet = require('topics/selector-set');
// Shorthand aliases
var T = TopicSelector.Type,
P = TopicSelector.Prefix;
var DQ = utils.DescendantQualifier;
/**
* Parse a selector expression into a specific TopicSelector implementation.
* @param {String} expression - The selector's string expression
* @returns {TopicSelector} A new topic selector
*/
function parse(expression) {
if (!expression) {
throw new Error("Empty topic selector expression");
}
if (arguments.length > 1) {
expression = arr.argumentsToArray(arguments);
}
if (expression instanceof Array) {
var actual = [];
expression.forEach(function (s) {
if (s instanceof TopicSelector) {
actual.push(s);
} else {
actual.push(parse(s));
}
});
return new SelectorSet(actual);
}
if (typeof expression === "string") {
var components = getComponents(expression);
if (isTopicPath(components)) {
return new PathSelector(components);
}
switch (components.type) {
case T.SPLIT_PATH_PATTERN :
return new SplitPathSelector(components);
case T.FULL_PATH_PATTERN :
return new FullPathSelector(components);
case T.SELECTOR_SET :
var parts = split(components.remainder, SelectorSet.DELIMITER);
var selectors = [];
parts.forEach(function (e) {
selectors.push(parse(e));
});
return new SelectorSet(selectors);
}
}
if (expression instanceof TopicSelector) {
return expression;
}
throw new Error("Topic selector expression must be a string or array");
}
/**
* Extract a component group from a single expression
* @param {String} expression - The selector expression
* @returns {Object} an object containing parsed properties
*/
function getComponents(expression) {
var type = getType(expression);
// Treat expressions as Path selectors by default
if (type === null) {
expression = P.PATH + expression;
type = T.PATH;
}
var remainder = expression.substring(1);
var qualifier, prefix, base;
if (type === T.PATH) {
base = remainder;
qualifier = DQ.MATCH;
} else if (remainder[remainder.length - 1] === '/') {
if (remainder[remainder.length - 2] === '/') {
qualifier = DQ.MATCH_AND_DESCENDANTS;
base = utils.canonicalise(remainder.substring(0, remainder.length - 2));
} else {
qualifier = DQ.DESCENDANTS_OF_MATCH;
base = utils.canonicalise(remainder.substring(0, remainder.length - 1));
}
} else {
base = utils.canonicalise(remainder);
qualifier = DQ.MATCH;
}
if (type === T.PATH) {
prefix = utils.canonicalise(remainder);
} else {
prefix = extractPrefixFromRegex(base);
}
return {
type: type,
base: base,
prefix: prefix,
remainder: remainder,
qualifier: qualifier,
expression: type + remainder
};
}
/**
* Determine if a component group can be treated as a topic path.
*
* @param {Object} c - Component group
* @returns {Boolean} If the components represent a topic path selector
*/
function isTopicPath(c) {
if (c.type === T.PATH) {
return true;
}
if (c.type === T.SELECTOR_SET || c.prefix === "") {
return false;
}
return c.prefix === c.base;
}
/**
* Determine if a character is alpha-numeric.
*
* @param {String} c - The character to check
* @returns {Boolean} If the character is alpha-numeric
function isAlphaNumeric(c) {
return !isNaN(parseInt(c, 10)) || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
*/
/**
* Determine the Topic Selector type of a given expression.
*
* @param {String} expression - The selector expression
* @returns {TopicSelector.Type|null} The topic selector type, or null
*/
function getType(expression) {
switch (expression[0]) {
case P.PATH:
return T.PATH;
case P.SPLIT_PATH_PATTERN :
return T.SPLIT_PATH_PATTERN;
case P.FULL_PATH_PATTERN :
return T.FULL_PATH_PATTERN;
case P.SELECTOR_SET :
return T.SELECTOR_SET;
case '$':
case '%':
case '&':
case '<':
// Reserved character
throw new Error("Invalid expression type: " + expression);
default:
// Default is to treat null as a path
return null;
}
}
// Invalid characters for a topic path to contain
var metaChars = ['*', '.', '+', '?', '^', '$', '{', '}', '(', ')', '[', ']', '\\', '|'];
// Validate characters in a normal parsing mode
var normal = function (c) {
return metaChars.indexOf(c) === -1;
};
// Validated characters within a quoted block (\Q \E)
var quoted = function () {
return true;
};
/**
* Extract the largest possible topic path prefix from a string that may
* contain a regular expression.
*
* @param {String} remainder - The remainder (i.e expression minus prefix)
* @returns {String} The maximum topic path that could be extracted
*/
function extractPrefixFromRegex(remainder) {
var buffer = [], composite = [];
var validator = normal;
for (var i = 0, l = remainder.length; i < l; ++i) {
var char = remainder[i];
// Identify a metachar and transition to an appropriate parse mode
// We can then safely skip over the '\' char & following metachar
if (char === '\\' && i < l - 1) {
var next = remainder[i + 1];
if (validator === normal && next === 'Q') {
validator = quoted;
} else if (validator === quoted && next === 'E') {
validator = normal;
// An escaped alphabetic character can be assumed to be a meta construct
} else if ((next < 'a' || next > 'z') && (next < 'A' || next > 'Z')) {
buffer.push(next);
} else {
// An escaped metachar indicates the end of the run
buffer.length = 0;
break;
}
i++;
} else if (validator(char)) {
if (char === utils.PATH_SEPARATOR) {
composite.push(buffer.join(''));
buffer.length = 0;
}
buffer.push(char);
} else {
buffer.length = 0;
break;
}
}
composite.push(buffer.join(''));
return utils.canonicalise(composite.join(''));
}
function memoizeMatcher(args) {
return JSON.stringify(args);
}
// Wrap the parse function to cache results
module.exports = cache(parse, {}, memoizeMatcher);