(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 ".concat(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 ".concat(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 ".concat(j));
                        }
                    }
                    pattern += str[j++];
                }
                if (count)
                    throw new TypeError("Unbalanced pattern at ".concat(i));
                if (!pattern)
                    throw new TypeError("Missing pattern at ".concat(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 = "[^".concat(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 ".concat(nextType, " at ").concat(index, ", expected ").concat(type));
        };
        var consumeText = function () {
            var result = "";
            var value;
            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("^(?:".concat(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 \"".concat(token.name, "\" to not repeat, but got an array"));
                    }
                    if (value.length === 0) {
                        if (optional)
                            continue;
                        throw new TypeError("Expected \"".concat(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 \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(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 \"".concat(token.name, "\" to match \"").concat(token.pattern, "\", but got \"").concat(segment, "\""));
                    }
                    path += token.prefix + segment + token.suffix;
                    continue;
                }
                if (optional)
                    continue;
                var typeOfMessage = repeat ? "an array" : "a string";
                throw new TypeError("Expected \"".concat(token.name, "\" to be ").concat(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("(?:".concat(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, _e = options.delimiter, delimiter = _e === void 0 ? "/#?" : _e, _f = options.endsWith, endsWith = _f === void 0 ? "" : _f;
        var endsWithRe = "[".concat(escapeString(endsWith), "]|$");
        var delimiterRe = "[".concat(escapeString(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 += "(?:".concat(prefix, "((?:").concat(token.pattern, ")(?:").concat(suffix).concat(prefix, "(?:").concat(token.pattern, "))*)").concat(suffix, ")").concat(mod);
                        }
                        else {
                            route += "(?:".concat(prefix, "(").concat(token.pattern, ")").concat(suffix, ")").concat(token.modifier);
                        }
                    }
                    else {
                        if (token.modifier === "+" || token.modifier === "*") {
                            route += "((?:".concat(token.pattern, ")").concat(token.modifier, ")");
                        }
                        else {
                            route += "(".concat(token.pattern, ")").concat(token.modifier);
                        }
                    }
                }
                else {
                    route += "(?:".concat(prefix).concat(suffix, ")").concat(token.modifier);
                }
            }
        }
        if (end) {
            if (!strict)
                route += "".concat(delimiterRe, "?");
            route += !options.endsWith ? "$" : "(?=".concat(endsWithRe, ")");
        }
        else {
            var endToken = tokens[tokens.length - 1];
            var isEndDelimited = typeof endToken === "string"
                ? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1
                : endToken === undefined;
            if (!strict) {
                route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?");
            }
            if (!isEndDelimited) {
                route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")");
            }
        }
        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
        }
      })
    }

    const isString = str => typeof str === 'string';
    const parseURL = (...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   {RegExp} 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}
     */
    /* c8 ignore start */
    const panic = error => {
      if (defaults.silentErrors) return

      throw new Error(error)
    };
    /* c8 ignore stop */

    // 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

    /**
     * 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});

    /* @type {object} general configuration object */
    const defaults = {
      base: 'https://localhost',
      silentErrors: false,
      // pathToRegexp options
      sensitive: false,
      strict: false,
      end: true,
      start: true,
      delimiter: '/#?',
      encode: undefined,
      endsWith: undefined,
      prefixes: './'
    };

    /**
     * Configure the router options overriding the defaults
     * @param {Object} options - custom user options to override
     * @returns {Object} new defaults
     */
    const configure = (options) => {
      Object.entries(options).forEach(([key, value]) => {
        if (Object.hasOwn(defaults, key)) defaults[key] = value;
      });

      return defaults
    };



    /* {@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.configure = configure;
    exports.createURLStreamPipe = createURLStreamPipe;
    exports.default = createRoute;
    exports.defaults = defaults;
    exports.match = match;
    exports.router = router;
    exports.toPath = toPath;
    exports.toRegexp = toRegexp;
    exports.toURL = toURL;

    Object.defineProperty(exports, '__esModule', { value: true });

}));
