moy-router
Version:
Give a solution for moy-dom router management.
600 lines (514 loc) • 17.9 kB
JavaScript
/**
* moy-router v1.0.6
* (jp) 2018 murakami
* @license MIT
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('moy-fp'), require('moy-dom')) :
typeof define === 'function' && define.amd ? define(['exports', 'moy-fp', 'moy-dom'], factory) :
(factory((global.MoyRouter = {}),global.M,global.moyDom));
}(this, (function (exports,moyFp,moyDom) { 'use strict';
/**
* Expose `pathToRegexp`.
*/
var pathToRegexp_1 = pathToRegexp;
var parse_1 = parse;
var compile_1 = compile;
var tokensToFunction_1 = tokensToFunction;
var tokensToRegExp_1 = tokensToRegExp;
/**
* Default configs.
*/
var DEFAULT_DELIMITER = '/';
var DEFAULT_DELIMITERS = './';
/**
* The main path matching regexp utility.
*
* @type {RegExp}
*/
var PATH_REGEXP = new RegExp([
// Match escaped characters that would otherwise appear in future matches.
// This allows the user to escape special characters that won't transform.
'(\\\\.)',
// Match Express-style parameters and un-named parameters with a prefix
// and optional suffixes. Matches appear as:
//
// ":test(\\d+)?" => ["test", "\d+", undefined, "?"]
// "(\\d+)" => [undefined, undefined, "\d+", undefined]
'(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?'
].join('|'), 'g');
/**
* Parse a string for the raw tokens.
*
* @param {string} str
* @param {Object=} options
* @return {!Array}
*/
function parse (str, options) {
var tokens = [];
var key = 0;
var index = 0;
var path = '';
var defaultDelimiter = (options && options.delimiter) || DEFAULT_DELIMITER;
var delimiters = (options && options.delimiters) || DEFAULT_DELIMITERS;
var pathEscaped = false;
var res;
while ((res = PATH_REGEXP.exec(str)) !== null) {
var m = res[0];
var escaped = res[1];
var offset = res.index;
path += str.slice(index, offset);
index = offset + m.length;
// Ignore already escaped sequences.
if (escaped) {
path += escaped[1];
pathEscaped = true;
continue
}
var prev = '';
var next = str[index];
var name = res[2];
var capture = res[3];
var group = res[4];
var modifier = res[5];
if (!pathEscaped && path.length) {
var k = path.length - 1;
if (delimiters.indexOf(path[k]) > -1) {
prev = path[k];
path = path.slice(0, k);
}
}
// Push the current path onto the tokens.
if (path) {
tokens.push(path);
path = '';
pathEscaped = false;
}
var partial = prev !== '' && next !== undefined && next !== prev;
var repeat = modifier === '+' || modifier === '*';
var optional = modifier === '?' || modifier === '*';
var delimiter = prev || defaultDelimiter;
var pattern = capture || group;
tokens.push({
name: name || key++,
prefix: prev,
delimiter: delimiter,
optional: optional,
repeat: repeat,
partial: partial,
pattern: pattern ? escapeGroup(pattern) : '[^' + escapeString(delimiter) + ']+?'
});
}
// Push any remaining characters.
if (path || index < str.length) {
tokens.push(path + str.substr(index));
}
return tokens
}
/**
* Compile a string to a template function for the path.
*
* @param {string} str
* @param {Object=} options
* @return {!function(Object=, Object=)}
*/
function compile (str, options) {
return tokensToFunction(parse(str, options))
}
/**
* Expose a method for transforming tokens into the path function.
*/
function tokensToFunction (tokens) {
// Compile all the tokens into regexps.
var matches = new Array(tokens.length);
// Compile all the patterns before compilation.
for (var i = 0; i < tokens.length; i++) {
if (typeof tokens[i] === 'object') {
matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$');
}
}
return function (data, options) {
var path = '';
var encode = (options && options.encode) || encodeURIComponent;
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 segment;
if (Array.isArray(value)) {
if (!token.repeat) {
throw new TypeError('Expected "' + token.name + '" to not repeat, but got array')
}
if (value.length === 0) {
if (token.optional) continue
throw new TypeError('Expected "' + token.name + '" to not be empty')
}
for (var j = 0; j < value.length; j++) {
segment = encode(value[j], token);
if (!matches[i].test(segment)) {
throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '"')
}
path += (j === 0 ? token.prefix : token.delimiter) + segment;
}
continue
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
segment = encode(String(value), token);
if (!matches[i].test(segment)) {
throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but got "' + segment + '"')
}
path += token.prefix + segment;
continue
}
if (token.optional) {
// Prepend partial segment prefixes.
if (token.partial) path += token.prefix;
continue
}
throw new TypeError('Expected "' + token.name + '" to be ' + (token.repeat ? 'an array' : 'a string'))
}
return path
}
}
/**
* Escape a regular expression string.
*
* @param {string} str
* @return {string}
*/
function escapeString (str) {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1')
}
/**
* Escape the capturing group by escaping special characters and meaning.
*
* @param {string} group
* @return {string}
*/
function escapeGroup (group) {
return group.replace(/([=!:$/()])/g, '\\$1')
}
/**
* Get the flags for a regexp from the options.
*
* @param {Object} options
* @return {string}
*/
function flags (options) {
return options && options.sensitive ? '' : 'i'
}
/**
* Pull out keys from a regexp.
*
* @param {!RegExp} path
* @param {Array=} keys
* @return {!RegExp}
*/
function regexpToRegexp (path, keys) {
if (!keys) return path
// Use a negative lookahead to match only capturing groups.
var groups = path.source.match(/\((?!\?)/g);
if (groups) {
for (var i = 0; i < groups.length; i++) {
keys.push({
name: i,
prefix: null,
delimiter: null,
optional: false,
repeat: false,
partial: false,
pattern: null
});
}
}
return path
}
/**
* Transform an array into a regexp.
*
* @param {!Array} path
* @param {Array=} keys
* @param {Object=} options
* @return {!RegExp}
*/
function arrayToRegexp (path, keys, options) {
var parts = [];
for (var i = 0; i < path.length; i++) {
parts.push(pathToRegexp(path[i], keys, options).source);
}
return new RegExp('(?:' + parts.join('|') + ')', flags(options))
}
/**
* Create a path regexp from string input.
*
* @param {string} path
* @param {Array=} keys
* @param {Object=} options
* @return {!RegExp}
*/
function stringToRegexp (path, keys, options) {
return tokensToRegExp(parse(path, options), keys, options)
}
/**
* Expose a function for taking tokens and returning a RegExp.
*
* @param {!Array} tokens
* @param {Array=} keys
* @param {Object=} options
* @return {!RegExp}
*/
function tokensToRegExp (tokens, keys, options) {
options = options || {};
var strict = options.strict;
var start = options.start !== false;
var end = options.end !== false;
var delimiter = escapeString(options.delimiter || DEFAULT_DELIMITER);
var delimiters = options.delimiters || DEFAULT_DELIMITERS;
var endsWith = [].concat(options.endsWith || []).map(escapeString).concat('$').join('|');
var route = start ? '^' : '';
var isEndDelimited = tokens.length === 0;
// Iterate over the tokens and create our regexp string.
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i];
if (typeof token === 'string') {
route += escapeString(token);
isEndDelimited = i === tokens.length - 1 && delimiters.indexOf(token[token.length - 1]) > -1;
} else {
var capture = token.repeat
? '(?:' + token.pattern + ')(?:' + escapeString(token.delimiter) + '(?:' + token.pattern + '))*'
: token.pattern;
if (keys) keys.push(token);
if (token.optional) {
if (token.partial) {
route += escapeString(token.prefix) + '(' + capture + ')?';
} else {
route += '(?:' + escapeString(token.prefix) + '(' + capture + '))?';
}
} else {
route += escapeString(token.prefix) + '(' + capture + ')';
}
}
}
if (end) {
if (!strict) route += '(?:' + delimiter + ')?';
route += endsWith === '$' ? '$' : '(?=' + endsWith + ')';
} else {
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 }]`.
*
* @param {(string|RegExp|Array)} path
* @param {Array=} keys
* @param {Object=} options
* @return {!RegExp}
*/
function pathToRegexp (path, keys, options) {
if (path instanceof RegExp) {
return regexpToRegexp(path, keys)
}
if (Array.isArray(path)) {
return arrayToRegexp(/** @type {!Array} */ (path), keys, options)
}
return stringToRegexp(/** @type {string} */ (path), keys, options)
}
pathToRegexp_1.parse = parse_1;
pathToRegexp_1.compile = compile_1;
pathToRegexp_1.tokensToFunction = tokensToFunction_1;
pathToRegexp_1.tokensToRegExp = tokensToRegExp_1;
var slicedToArray = function () {
function sliceIterator(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"]) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
return function (arr, i) {
if (Array.isArray(arr)) {
return arr;
} else if (Symbol.iterator in Object(arr)) {
return sliceIterator(arr, i);
} else {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
};
}();
var stringifyQuery = moyFp.compose(moyFp.prepend('?'), moyFp.join('&'), moyFp.map(moyFp.compose(moyFp.join('='), moyFp.map(encodeURIComponent))), moyFp.entries);
var parseQuery = moyFp.compose(moyFp.reduce(function (query, params) {
var _compose = moyFp.compose(moyFp.map(decodeURIComponent), moyFp.split('='))(params),
_compose2 = slicedToArray(_compose, 2),
key = _compose2[0],
value = _compose2[1];
query[key] = value;
return query;
}, {}), moyFp.split('&'));
var pushQueryAndHash = moyFp.ifElse(function (queryAndHash, route) {
var _queryAndHash$split = queryAndHash.split('#'),
_queryAndHash$split2 = slicedToArray(_queryAndHash$split, 2),
query = _queryAndHash$split2[0],
hash = _queryAndHash$split2[1];
route.query = query ? parseQuery(query) : undefined;
route.hash = hash;
})(function (queryAndHash, route) {
return route.query = parseQuery(queryAndHash);
})(moyFp.includes('#'));
var handleStringPathname = function handleStringPathname(pathname) {
var route = {};
route.pathname = pathname;
var queryAndHash = moyFp.compose(moyFp.prop(1), moyFp.split('?'))(pathname);
!queryAndHash && pathname.includes('#') && (queryAndHash = moyFp.compose(moyFp.prepend('#'), moyFp.prop(1), moyFp.split('#'))(pathname));
queryAndHash && pushQueryAndHash(queryAndHash, route);
return route;
};
var handleObjectPathname = function handleObjectPathname(route) {
if (route.params) {
route.pathname = pathToRegexp_1.compile(route.pathname)(route.params);
}
if (route.query) {
route.pathname += stringifyQuery(route.query);
}
if (route.hash) {
route.pathname += moyFp.concat(route.hash)('#');
}
return route;
};
var parseRoute = moyFp.ifElse(handleStringPathname)(handleObjectPathname)(moyFp.type('String'));
var windowMonad = moyFp.Maybe.of(window);
var createHistory = (function (current) {
return moyFp.and(windowMonad.map(function (window) {
return window.addEventListener('popstate', moyFp.compose(moyFp.assoc('route', moyFp.__, current), moyFp.path(['state', 'route'])));
}))({
back: function back() {
windowMonad.map(function (window) {
return window.history.back();
});
},
go: function go(index) {
windowMonad.map(function (window) {
return window.history.go(index);
});
},
forward: function forward() {
windowMonad.map(function (window) {
return window.history.forward();
});
},
replace: function replace(routeConfig) {
var route = parseRoute(routeConfig);
windowMonad.map(function (window) {
return moyFp.or(window.history.replaceState(route, '', route.pathname))(moyFp.assoc('route', route, current));
});
},
push: function push(routeConfig) {
var route = parseRoute(routeConfig);
windowMonad.map(function (window) {
return moyFp.or(window.history.replaceState(route, '', route.pathname))(moyFp.assoc('route', route, current));
});
}
});
});
var RouterView = moyFp.curry(function (current, routes) {
return function (props) {
var currentPathname = current.route.pathname.split('?').map(moyFp.split('#'))[0][0];
var maybeRouteNode = moyFp.compose(moyFp.map(function (route) {
return route.component(Object.assign({}, props, {
route: {
pathname: current.route.pathname,
params: current.route.params,
query: current.route.query,
hash: current.route.hash,
state: current.route.state
}
}));
}), moyFp.map(moyFp.tap(moyFp.ifElse(function (currentRoute) {
var matched = currentPathname.match(currentRoute.pathnameRegExp);
current.route.params = {};
currentRoute.paramKeys.forEach(function (key, index) {
current.route.params[key.name] = decodeURIComponent(matched[index + 1]);
});
})(moyFp.always(undefined))(moyFp.compose(moyFp.and(!current.route.params), moyFp.length, moyFp.prop('paramKeys'))))), moyFp.Maybe.of)(routes.find(function (route) {
return route.pathnameRegExp.test(currentPathname);
}));
return maybeRouteNode.isJust ? maybeRouteNode.join() : '';
};
});
var RouterLink = moyFp.curry(function (push, _ref) {
var to = _ref.to,
innerNode = _ref.innerNode;
return moyDom.Element.of('a', {
href: moyFp.type('String', to) ? to : to.pathname,
onclick: function onclick(e) {
e.preventDefault();
push(to);
}
}, innerNode);
});
var getLocation = (function () {
return moyFp.Maybe.of(window).map(function (window) {
return window.location.pathname + window.location.search + window.location.hash;
});
});
var createRouter = (function (routes, subscription) {
var routesRegExpPathname = moyFp.map(moyFp.compose(moyFp.dissoc('pathname'), function (route) {
return moyFp.assoc('pathnameRegExp')(pathToRegexp_1(moyFp.prop('pathname', route), moyFp.prop('paramKeys', route), {
sensitive: true
}))(route);
}, moyFp.tap(function (route) {
return route.paramKeys = [];
})))(routes);
var currentRoute = getLocation().chain(parseRoute);
var current = new Proxy({
route: currentRoute
}, {
set: function set(target, prop, value) {
Reflect.set(target, prop, value);
prop === 'route' && subscription();
return true;
}
});
var _createHistory = createHistory(current),
back = _createHistory.back,
go = _createHistory.go,
forward = _createHistory.forward,
replace = _createHistory.replace,
push = _createHistory.push;
var router = {
back: back,
go: go,
forward: forward,
replace: replace,
push: push,
RouterView: RouterView(current, routesRegExpPathname),
RouterLink: RouterLink(push)
};
return router;
});
exports.createRouter = createRouter;
Object.defineProperty(exports, '__esModule', { value: true });
})));