UNPKG

diffusion

Version:

Diffusion JavaScript client

254 lines (214 loc) 7.32 kB
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);