(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.rawth = {})); }(this, (function (exports) { 'use strict'; /** * Tokenize input string. */ function lexer(str) { var tokens = []; var i = 0; while (i < str.length) { var char = str[i]; if (char === "*" || char === "+" || char === "?") { tokens.push({ type: "MODIFIER", index: i, value: str[i++] }); continue; } if (char === "\\") { tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] }); continue; } if (char === "{") { tokens.push({ type: "OPEN", index: i, value: str[i++] }); continue; } if (char === "}") { tokens.push({ type: "CLOSE", index: i, value: str[i++] }); continue; } if (char === ":") { var name = ""; var j = i + 1; while (j < str.length) { var code = str.charCodeAt(j); if ( // `0-9` (code >= 48 && code <= 57) || // `A-Z` (code >= 65 && code <= 90) || // `a-z` (code >= 97 && code <= 122) || // `_` code === 95) { name += str[j++]; continue; } break; } if (!name) throw new TypeError("Missing parameter name at " + i); tokens.push({ type: "NAME", index: i, value: name }); i = j; continue; } if (char === "(") { var count = 1; var pattern = ""; var j = i + 1; if (str[j] === "?") { throw new TypeError("Pattern cannot start with \"?\" at " + j); } while (j < str.length) { if (str[j] === "\\") { pattern += str[j++] + str[j++]; continue; } if (str[j] === ")") { count--; if (count === 0) { j++; break; } } else if (str[j] === "(") { count++; if (str[j + 1] !== "?") { throw new TypeError("Capturing groups are not allowed at " + j); } } pattern += str[j++]; } if (count) throw new TypeError("Unbalanced pattern at " + i); if (!pattern) throw new TypeError("Missing pattern at " + i); tokens.push({ type: "PATTERN", index: i, value: pattern }); i = j; continue; } tokens.push({ type: "CHAR", index: i, value: str[i++] }); } tokens.push({ type: "END", index: i, value: "" }); return tokens; } /** * Parse a string for the raw tokens. */ function parse(str, options) { if (options === void 0) { options = {}; } var tokens = lexer(str); var _a = options.prefixes, prefixes = _a === void 0 ? "./" : _a; var defaultPattern = "[^" + escapeString(options.delimiter || "/#?") + "]+?"; var result = []; var key = 0; var i = 0; var path = ""; var tryConsume = function (type) { if (i < tokens.length && tokens[i].type === type) return tokens[i++].value; }; var mustConsume = function (type) { var value = tryConsume(type); if (value !== undefined) return value; var _a = tokens[i], nextType = _a.type, index = _a.index; throw new TypeError("Unexpected " + nextType + " at " + index + ", expected " + type); }; var consumeText = function () { var result = ""; var value; // tslint:disable-next-line while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) { result += value; } return result; }; while (i < tokens.length) { var char = tryConsume("CHAR"); var name = tryConsume("NAME"); var pattern = tryConsume("PATTERN"); if (name || pattern) { var prefix = char || ""; if (prefixes.indexOf(prefix) === -1) { path += prefix; prefix = ""; } if (path) { result.push(path); path = ""; } result.push({ name: name || key++, prefix: prefix, suffix: "", pattern: pattern || defaultPattern, modifier: tryConsume("MODIFIER") || "" }); continue; } var value = char || tryConsume("ESCAPED_CHAR"); if (value) { path += value; continue; } if (path) { result.push(path); path = ""; } var open = tryConsume("OPEN"); if (open) { var prefix = consumeText(); var name_1 = tryConsume("NAME") || ""; var pattern_1 = tryConsume("PATTERN") || ""; var suffix = consumeText(); mustConsume("CLOSE"); result.push({ name: name_1 || (pattern_1 ? key++ : ""), pattern: name_1 && !pattern_1 ? defaultPattern : pattern_1, prefix: prefix, suffix: suffix, modifier: tryConsume("MODIFIER") || "" }); continue; } mustConsume("END"); } return result; } /** * Compile a string to a template function for the path. */ function compile(str, options) { return tokensToFunction(parse(str, options), options); } /** * Expose a method for transforming tokens into the path function. */ function tokensToFunction(tokens, options) { if (options === void 0) { options = {}; } var reFlags = flags(options); var _a = options.encode, encode = _a === void 0 ? function (x) { return x; } : _a, _b = options.validate, validate = _b === void 0 ? true : _b; // Compile all the tokens into regexps. var matches = tokens.map(function (token) { if (typeof token === "object") { return new RegExp("^(?:" + token.pattern + ")$", reFlags); } }); return function (data) { var path = ""; for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; if (typeof token === "string") { path += token; continue; } var value = data ? data[token.name] : undefined; var optional = token.modifier === "?" || token.modifier === "*"; var repeat = token.modifier === "*" || token.modifier === "+"; if (Array.isArray(value)) { if (!repeat) { throw new TypeError("Expected \"" + token.name + "\" to not repeat, but got an array"); } if (value.length === 0) { if (optional) continue; throw new TypeError("Expected \"" + token.name + "\" to not be empty"); } for (var j = 0; j < value.length; j++) { var segment = encode(value[j], token); if (validate && !matches[i].test(segment)) { throw new TypeError("Expected all \"" + token.name + "\" to match \"" + token.pattern + "\", but got \"" + segment + "\""); } path += token.prefix + segment + token.suffix; } continue; } if (typeof value === "string" || typeof value === "number") { var segment = encode(String(value), token); if (validate && !matches[i].test(segment)) { throw new TypeError("Expected \"" + token.name + "\" to match \"" + token.pattern + "\", but got \"" + segment + "\""); } path += token.prefix + segment + token.suffix; continue; } if (optional) continue; var typeOfMessage = repeat ? "an array" : "a string"; throw new TypeError("Expected \"" + token.name + "\" to be " + typeOfMessage); } return path; }; } /** * Escape a regular expression string. */ function escapeString(str) { return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1"); } /** * Get the flags for a regexp from the options. */ function flags(options) { return options && options.sensitive ? "" : "i"; } /** * Pull out keys from a regexp. */ function regexpToRegexp(path, keys) { if (!keys) return path; var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g; var index = 0; var execResult = groupsRegex.exec(path.source); while (execResult) { keys.push({ // Use parenthesized substring match if available, index otherwise name: execResult[1] || index++, prefix: "", suffix: "", modifier: "", pattern: "" }); execResult = groupsRegex.exec(path.source); } return path; } /** * Transform an array into a regexp. */ function arrayToRegexp(paths, keys, options) { var parts = paths.map(function (path) { return pathToRegexp(path, keys, options).source; }); return new RegExp("(?:" + parts.join("|") + ")", flags(options)); } /** * Create a path regexp from string input. */ function stringToRegexp(path, keys, options) { return tokensToRegexp(parse(path, options), keys, options); } /** * Expose a function for taking tokens and returning a RegExp. */ function tokensToRegexp(tokens, keys, options) { if (options === void 0) { options = {}; } var _a = options.strict, strict = _a === void 0 ? false : _a, _b = options.start, start = _b === void 0 ? true : _b, _c = options.end, end = _c === void 0 ? true : _c, _d = options.encode, encode = _d === void 0 ? function (x) { return x; } : _d; var endsWith = "[" + escapeString(options.endsWith || "") + "]|$"; var delimiter = "[" + escapeString(options.delimiter || "/#?") + "]"; var route = start ? "^" : ""; // Iterate over the tokens and create our regexp string. for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { var token = tokens_1[_i]; if (typeof token === "string") { route += escapeString(encode(token)); } else { var prefix = escapeString(encode(token.prefix)); var suffix = escapeString(encode(token.suffix)); if (token.pattern) { if (keys) keys.push(token); if (prefix || suffix) { if (token.modifier === "+" || token.modifier === "*") { var mod = token.modifier === "*" ? "?" : ""; route += "(?:" + prefix + "((?:" + token.pattern + ")(?:" + suffix + prefix + "(?:" + token.pattern + "))*)" + suffix + ")" + mod; } else { route += "(?:" + prefix + "(" + token.pattern + ")" + suffix + ")" + token.modifier; } } else { route += "(" + token.pattern + ")" + token.modifier; } } else { route += "(?:" + prefix + suffix + ")" + token.modifier; } } } if (end) { if (!strict) route += delimiter + "?"; route += !options.endsWith ? "$" : "(?=" + endsWith + ")"; } else { var endToken = tokens[tokens.length - 1]; var isEndDelimited = typeof endToken === "string" ? delimiter.indexOf(endToken[endToken.length - 1]) > -1 : // tslint:disable-next-line endToken === undefined; if (!strict) { route += "(?:" + delimiter + "(?=" + endsWith + "))?"; } if (!isEndDelimited) { route += "(?=" + delimiter + "|" + endsWith + ")"; } } return new RegExp(route, flags(options)); } /** * Normalize the given path string, returning a regular expression. * * An empty array can be passed in for the keys, which will hold the * placeholder key descriptions. For example, using `/user/:id`, `keys` will * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. */ function pathToRegexp(path, keys, options) { if (path instanceof RegExp) return regexpToRegexp(path, keys); if (Array.isArray(path)) return arrayToRegexp(path, keys, options); return stringToRegexp(path, keys, options); } /** * Cancel token * @private * @type { Symbol } */ const CANCEL = Symbol(); /** * Helper that can be returned by ruit function to cancel the tasks chain * @returns { Symbol } internal private constant * @example * * ruit( * 100, * num => Math.random() * num * num => num > 50 ? ruit.cancel() : num * num => num - 2 * ).then(result => { * console.log(result) // here we will get only number lower than 50 * }) * */ ruit.cancel = () => CANCEL; /** * The same as ruit() but with the arguments inverted from right to left * @param { * } tasks - list of tasks to process sequentially * @returns { Promise } a promise containing the result of the whole chain * @example * * const curry = f => a => b => f(a, b) * const add = (a, b) => a + b * * const addOne = curry(add)(1) * * const squareAsync = (num) => { * return new Promise(r => { * setTimeout(r, 500, num * 2) * }) * } * * // a -> a + a -> a * 2 * // basically from right to left: 1 => 1 + 1 => 2 * 2 * ruit.compose(squareAsync, addOne, 1).then(result => console.log(result)) // 4 */ ruit.compose = (...tasks) => ruit(...tasks.reverse()); /** * Serialize a list of sync and async tasks from left to right * @param { * } tasks - list of tasks to process sequentially * @returns { Promise } a promise containing the result of the whole chain * @example * * const curry = f => a => b => f(a, b) * const add = (a, b) => a + b * * const addOne = curry(add)(1) * * const squareAsync = (num) => { * return new Promise(r => { * setTimeout(r, 500, num * 2) * }) * } * * // a -> a + a -> a * 2 * // basically from left to right: 1 => 1 + 1 => 2 * 2 * ruit(1, addOne, squareAsync).then(result => console.log(result)) // 4 */ function ruit(...tasks) { return new Promise((resolve, reject) => { return (function run(queue, result) { if (!queue.length) return resolve(result) const [task, ...rest] = queue; const value = typeof task === 'function' ? task(result) : task; const done = v => run(rest, v); // check against nil values if (value != null) { if (value === CANCEL) return if (value.then) return value.then(done, reject) } return Promise.resolve(done(value)) })(tasks) }) } // Store the erre the API methods to handle the plugins installation const API_METHODS = new Set(); const UNSUBSCRIBE_SYMBOL = Symbol(); const UNSUBSCRIBE_METHOD = 'off'; const CANCEL_METHOD = 'cancel'; /** * Factory function to create the stream generator * @private * @param {Set} modifiers - stream input modifiers * @returns {Generator} the stream generator */ function createStream(modifiers) { const stream = (function *stream() { while (true) { // get the initial stream value const input = yield; // run the input sequence yield ruit(input, ...modifiers); } })(); // start the stream stream.next(); return stream } /** * Dispatch a value to several listeners * @private * @param {Set} callbacks - callbacks collection * @param {*} value - anything * @returns {Set} the callbacks received */ function dispatch(callbacks, value) { callbacks.forEach(f => { // unsubscribe the callback if erre.unsubscribe() will be returned if (f(value) === UNSUBSCRIBE_SYMBOL) callbacks.delete(f); }); return callbacks } /** * Throw a panic error * @param {string} message - error message * @returns {Error} an error object */ function panic$1(message) { throw new Error(message) } /** * Install an erre plugin adding it to the API * @param {string} name - plugin name * @param {Function} fn - new erre API method * @returns {Function} return the erre function */ erre.install = function(name, fn) { if (!name || typeof name !== 'string') panic$1('Please provide a name (as string) for your erre plugin'); if (!fn || typeof fn !== 'function') panic$1('Please provide a function for your erre plugin'); if (API_METHODS.has(name)) { panic$1(`The ${name} is already part of the erre API, please provide a different name`); } else { erre[name] = fn; API_METHODS.add(name); } return erre }; // alias for ruit canel to stop a stream chain erre.install(CANCEL_METHOD, ruit.cancel); // unsubscribe helper erre.install(UNSUBSCRIBE_METHOD, () => UNSUBSCRIBE_SYMBOL); /** * Stream constuction function * @param {...Function} fns - stream modifiers * @returns {Object} erre instance */ function erre(...fns) { const [success, error, end, modifiers] = [new Set(), new Set(), new Set(), new Set(fns)], generator = createStream(modifiers), stream = Object.create(generator), addToCollection = (collection) => (fn) => collection.add(fn) && stream, deleteFromCollection = (collection) => (fn) => collection.delete(fn) ? stream : panic$1('Couldn\'t remove handler passed by reference'); return Object.assign(stream, { on: Object.freeze({ value: addToCollection(success), error: addToCollection(error), end: addToCollection(end) }), off: Object.freeze({ value: deleteFromCollection(success), error: deleteFromCollection(error), end: deleteFromCollection(end) }), connect: addToCollection(modifiers), push(input) { const { value, done } = stream.next(input); // dispatch the stream events if (!done) { value.then( res => dispatch(success, res), err => dispatch(error, err) ); } return stream }, end() { // kill the stream generator.return(); // dispatch the end event dispatch(end) // clean up all the collections ;[success, error, end, modifiers].forEach(el => el.clear()); return stream }, fork() { return erre(...modifiers) }, next(input) { // get the input and run eventually the promise const result = generator.next(input); // pause to the next iteration generator.next(); return result } }) } // check whether the window object is defined const isNode = typeof process !== 'undefined'; const isString = str => typeof str === 'string'; // the url parsing function depends on the platform, on node we rely on the 'url' module /* istanbul ignore next */ const parseURL = (...args) => isNode ? require('url').parse(...args) : new URL(...args); /** * Replace the base path from a path * @param {string} path - router path string * @returns {string} path cleaned up without the base */ const replaceBase = path => path.replace(defaults.base, ''); /** * Try to match the current path or skip it * @param {RegEx} pathRegExp - target path transformed by pathToRegexp * @returns {string|Symbol} if the path match we return it otherwise we cancel the stream */ const matchOrSkip = pathRegExp => path => match(path, pathRegExp) ? path : erre.cancel(); /** * Combine 2 streams connecting the events of dispatcherStream to the receiverStream * @param {Stream} dispatcherStream - main stream dispatching events * @param {Stream} receiverStream - sub stream receiving events from the dispatcher * @returns {Stream} receiverStream */ const joinStreams = (dispatcherStream, receiverStream) => { dispatcherStream.on.value(receiverStream.push); receiverStream.on.end(() => { dispatcherStream.off.value(receiverStream.push); }); return receiverStream }; /** * Error handling function * @param {Error} error - error to catch * @returns {void} */ const panic = error => { if (defaults.silentErrors) return throw new Error(error) }; // make sure that the router will always receive strings params const filterStrings = str => isString(str) ? str : erre.cancel(); // create the streaming router const router = erre(filterStrings).on.error(panic); // cast the values of this stream always to string /* @type {object} general configuration object */ const defaults = { // custom option base: '', silentErrors: false, // pathToRegexp options sensitive: false, strict: false, end: true, start: true, delimiter: '/#?', encode: undefined, endsWith: undefined, prefixes: './' }; /** * Merge the user options with the defaults * @param {Object} options - custom user options * @returns {Object} options object merged with defaults */ const mergeOptions = options => ({...defaults, ...options}); /* {@link https://github.com/pillarjs/path-to-regexp#usage} */ const toRegexp = (path, keys, options) => pathToRegexp(path, keys, mergeOptions(options)); /** * Convert a router entry to a real path computing the url parameters * @param {string} path - router path string * @param {Object} params - named matched parameters * @param {Object} options - pathToRegexp options object * @returns {string} computed url string */ const toPath = (path, params, options) => compile(path, mergeOptions(options))(params); /** * Parse a string path generating an object containing * @param {string} path - target path * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp * @param {Object} options - object containing the base path * @returns {URL} url object enhanced with the `match` attribute */ const toURL = (path, pathRegExp, options = {}) => { const {base} = mergeOptions(options); const [, ...params] = pathRegExp.exec(path); const url = parseURL(path, base); // extend the url object adding the matched params url.params = params.reduce((acc, param, index) => { const key = options.keys && options.keys[index]; if (key) acc[key.name] = param ? decodeURIComponent(param) : param; return acc }, {}); return url }; /** * Return true if a path will be matched * @param {string} path - target path * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp * @returns {boolean} true if the path matches the regexp */ const match = (path, pathRegExp) => pathRegExp.test(path); /** * Factory function to create an sequence of functions to pass to erre.js * This function will be used in the erre stream * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp * @param {Object} options - pathToRegexp options object * @returns {Array} a functions array that will be used as stream pipe for erre.js */ const createURLStreamPipe = (pathRegExp, options) => [ decodeURI, replaceBase, matchOrSkip(pathRegExp), path => toURL(path, pathRegExp, options) ]; /** * Create a fork of the main router stream * @param {string} path - route to match * @param {Object} options - pathToRegexp options object * @returns {Stream} new route stream */ function createRoute(path, options) { const keys = []; const pathRegExp = pathToRegexp(path, keys, options); const URLStream = erre(...createURLStreamPipe(pathRegExp, { ...options, keys })); return joinStreams(router, URLStream).on.error(panic) } exports.createURLStreamPipe = createURLStreamPipe; exports.default = createRoute; exports.defaults = defaults; exports.filterStrings = filterStrings; exports.match = match; exports.mergeOptions = mergeOptions; exports.router = router; exports.toPath = toPath; exports.toRegexp = toRegexp; exports.toURL = toURL; Object.defineProperty(exports, '__esModule', { value: true }); })));