kompendium
Version:
Documentation generator for Stencil components
573 lines (566 loc) • 18.9 kB
JavaScript
;
/**
* TS adaption of https://github.com/pillarjs/path-to-regexp/blob/master/index.js
*/
/**
* Default configs.
*/
const DEFAULT_DELIMITER = '/';
const DEFAULT_DELIMITERS = './';
/**
* The main path matching regexp utility.
*/
const 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, "?"]
// "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined]
'(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?'
].join('|'), 'g');
/**
* Parse a string for the raw tokens.
*/
const 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;
};
/**
* Escape a regular expression string.
*/
const escapeString = (str) => {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, '\\$1');
};
/**
* Escape the capturing group by escaping special characters and meaning.
*/
const escapeGroup = (group) => {
return group.replace(/([=!:$/()])/g, '\\$1');
};
/**
* Get the flags for a regexp from the options.
*/
const flags = (options) => {
return options && options.sensitive ? '' : 'i';
};
/**
* Pull out keys from a regexp.
*/
const 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.
*/
const 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.
*/
const stringToRegexp = (path, keys, options) => {
return tokensToRegExp(parse(path, options), keys, options);
};
/**
* Expose a function for taking tokens and returning a RegExp.
*/
const tokensToRegExp = (tokens, keys, options) => {
options = options || {};
var strict = options.strict;
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 = '';
var isEndDelimited = false;
// 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 prefix = escapeString(token.prefix || '');
var capture = token.repeat
? '(?:' + token.pattern + ')(?:' + prefix + '(?:' + token.pattern + '))*'
: token.pattern;
if (keys)
keys.push(token);
if (token.optional) {
if (token.partial) {
route += prefix + '(' + capture + ')?';
}
else {
route += '(?:' + prefix + '(' + capture + '))?';
}
}
else {
route += 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 }]`.
*/
const 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);
};
const hasBasename = (path, prefix) => {
return (new RegExp('^' + prefix + '(\\/|\\?|#|$)', 'i')).test(path);
};
const stripBasename = (path, prefix) => {
return hasBasename(path, prefix) ? path.substr(prefix.length) : path;
};
const stripTrailingSlash = (path) => {
return path.charAt(path.length - 1) === '/' ? path.slice(0, -1) : path;
};
const addLeadingSlash = (path) => {
return path.charAt(0) === '/' ? path : '/' + path;
};
const stripLeadingSlash = (path) => {
return path.charAt(0) === '/' ? path.substr(1) : path;
};
const parsePath = (path) => {
let pathname = path || '/';
let search = '';
let hash = '';
const hashIndex = pathname.indexOf('#');
if (hashIndex !== -1) {
hash = pathname.substr(hashIndex);
pathname = pathname.substr(0, hashIndex);
}
const searchIndex = pathname.indexOf('?');
if (searchIndex !== -1) {
search = pathname.substr(searchIndex);
pathname = pathname.substr(0, searchIndex);
}
return {
pathname,
search: search === '?' ? '' : search,
hash: hash === '#' ? '' : hash,
query: {},
key: ''
};
};
const createPath = (location) => {
const { pathname, search, hash } = location;
let path = pathname || '/';
if (search && search !== '?') {
path += (search.charAt(0) === '?' ? search : `?${search}`);
}
if (hash && hash !== '#') {
path += (hash.charAt(0) === '#' ? hash : `#${hash}`);
}
return path;
};
const parseQueryString = (query) => {
if (!query) {
return {};
}
return (/^[?#]/.test(query) ? query.slice(1) : query)
.split('&')
.reduce((params, param) => {
let [key, value] = param.split('=');
params[key] = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : '';
return params;
}, {});
};
const isAbsolute = (pathname) => {
return pathname.charAt(0) === '/';
};
const createKey = (keyLength) => {
return Math.random().toString(36).substr(2, keyLength);
};
// About 1.5x faster than the two-arg version of Array#splice()
const spliceOne = (list, index) => {
for (let i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) {
list[i] = list[k];
}
list.pop();
};
// This implementation is based heavily on node's url.parse
const resolvePathname = (to, from = '') => {
let fromParts = from && from.split('/') || [];
let hasTrailingSlash;
let up = 0;
const toParts = to && to.split('/') || [];
const isToAbs = to && isAbsolute(to);
const isFromAbs = from && isAbsolute(from);
const mustEndAbs = isToAbs || isFromAbs;
if (to && isAbsolute(to)) {
// to is absolute
fromParts = toParts;
}
else if (toParts.length) {
// to is relative, drop the filename
fromParts.pop();
fromParts = fromParts.concat(toParts);
}
if (!fromParts.length) {
return '/';
}
if (fromParts.length) {
const last = fromParts[fromParts.length - 1];
hasTrailingSlash = (last === '.' || last === '..' || last === '');
}
else {
hasTrailingSlash = false;
}
for (let i = fromParts.length; i >= 0; i--) {
const part = fromParts[i];
if (part === '.') {
spliceOne(fromParts, i);
}
else if (part === '..') {
spliceOne(fromParts, i);
up++;
}
else if (up) {
spliceOne(fromParts, i);
up--;
}
}
if (!mustEndAbs) {
for (; up--; up) {
fromParts.unshift('..');
}
}
if (mustEndAbs && fromParts[0] !== '' && (!fromParts[0] || !isAbsolute(fromParts[0]))) {
fromParts.unshift('');
}
let result = fromParts.join('/');
if (hasTrailingSlash && result.substr(-1) !== '/') {
result += '/';
}
return result;
};
const valueEqual = (a, b) => {
if (a === b) {
return true;
}
if (a == null || b == null) {
return false;
}
if (Array.isArray(a)) {
return Array.isArray(b) && a.length === b.length && a.every((item, index) => {
return valueEqual(item, b[index]);
});
}
const aType = typeof a;
const bType = typeof b;
if (aType !== bType) {
return false;
}
if (aType === 'object') {
const aValue = a.valueOf();
const bValue = b.valueOf();
if (aValue !== a || bValue !== b) {
return valueEqual(aValue, bValue);
}
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length) {
return false;
}
return aKeys.every((key) => {
return valueEqual(a[key], b[key]);
});
}
return false;
};
const locationsAreEqual = (a, b) => {
return a.pathname === b.pathname &&
a.search === b.search &&
a.hash === b.hash &&
a.key === b.key &&
valueEqual(a.state, b.state);
};
const createLocation = (path, state, key, currentLocation) => {
let location;
if (typeof path === 'string') {
// Two-arg form: push(path, state)
location = parsePath(path);
if (state !== undefined) {
location.state = state;
}
}
else {
// One-arg form: push(location)
location = Object.assign({ pathname: '' }, path);
if (location.search && location.search.charAt(0) !== '?') {
location.search = '?' + location.search;
}
if (location.hash && location.hash.charAt(0) !== '#') {
location.hash = '#' + location.hash;
}
if (state !== undefined && location.state === undefined) {
location.state = state;
}
}
try {
location.pathname = decodeURI(location.pathname);
}
catch (e) {
if (e instanceof URIError) {
throw new URIError('Pathname "' + location.pathname + '" could not be decoded. ' +
'This is likely caused by an invalid percent-encoding.');
}
else {
throw e;
}
}
location.key = key;
if (currentLocation) {
// Resolve incomplete/relative pathname relative to current location.
if (!location.pathname) {
location.pathname = currentLocation.pathname;
}
else if (location.pathname.charAt(0) !== '/') {
location.pathname = resolvePathname(location.pathname, currentLocation.pathname);
}
}
else {
// When there is no prior location and pathname is empty, set it to /
if (!location.pathname) {
location.pathname = '/';
}
}
location.query = parseQueryString(location.search || '');
return location;
};
let cacheCount = 0;
const patternCache = {};
const cacheLimit = 10000;
// Memoized function for creating the path match regex
const compilePath = (pattern, options) => {
const cacheKey = `${options.end}${options.strict}`;
const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
const cachePattern = JSON.stringify(pattern);
if (cache[cachePattern]) {
return cache[cachePattern];
}
const keys = [];
const re = pathToRegexp(pattern, keys, options);
const compiledPattern = { re, keys };
if (cacheCount < cacheLimit) {
cache[cachePattern] = compiledPattern;
cacheCount += 1;
}
return compiledPattern;
};
/**
* Public API for matching a URL pathname to a path pattern.
*/
const matchPath = (pathname, options = {}) => {
if (typeof options === 'string') {
options = { path: options };
}
const { path = '/', exact = false, strict = false } = options;
const { re, keys } = compilePath(path, { end: exact, strict });
const match = re.exec(pathname);
if (!match) {
return null;
}
const [url, ...values] = match;
const isExact = pathname === url;
if (exact && !isExact) {
return null;
}
return {
path,
url: path === '/' && url === '' ? '/' : url,
isExact,
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
};
const matchesAreEqual = (a, b) => {
if (a == null && b == null) {
return true;
}
if (b == null) {
return false;
}
return a && b &&
a.path === b.path &&
a.url === b.url &&
valueEqual(a.params, b.params);
};
const getConfirmation = (win, message, callback) => (callback(win.confirm(message)));
const isModifiedEvent = (ev) => (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey);
/**
* Returns true if the HTML5 history API is supported. Taken from Modernizr.
*
* https://github.com/Modernizr/Modernizr/blob/master/LICENSE
* https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
* changed to avoid false negatives for Windows Phones: https://github.com/reactjs/react-router/issues/586
*/
const supportsHistory = (win) => {
const ua = win.navigator.userAgent;
if ((ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1) {
return false;
}
return win.history && 'pushState' in win.history;
};
/**
* Returns true if browser fires popstate on hash change.
* IE10 and IE11 do not.
*/
const supportsPopStateOnHashChange = (nav) => (nav.userAgent.indexOf('Trident') === -1);
/**
* Returns false if using go(n) with hash history causes a full page reload.
*/
const supportsGoWithoutReloadUsingHash = (nav) => (nav.userAgent.indexOf('Firefox') === -1);
const isExtraneousPopstateEvent = (nav, event) => (event.state === undefined &&
nav.userAgent.indexOf('CriOS') === -1);
const storageAvailable = (win, type) => {
const storage = win[type];
const x = '__storage_test__';
try {
storage.setItem(x, x);
storage.removeItem(x);
return true;
}
catch (e) {
return e instanceof DOMException && (
// everything except Firefox
e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === 'QuotaExceededError' ||
// Firefox
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
// acknowledge QuotaExceededError only if there's something already stored
storage.length !== 0;
}
};
exports.addLeadingSlash = addLeadingSlash;
exports.createKey = createKey;
exports.createLocation = createLocation;
exports.createPath = createPath;
exports.getConfirmation = getConfirmation;
exports.hasBasename = hasBasename;
exports.isExtraneousPopstateEvent = isExtraneousPopstateEvent;
exports.isModifiedEvent = isModifiedEvent;
exports.locationsAreEqual = locationsAreEqual;
exports.matchPath = matchPath;
exports.matchesAreEqual = matchesAreEqual;
exports.storageAvailable = storageAvailable;
exports.stripBasename = stripBasename;
exports.stripLeadingSlash = stripLeadingSlash;
exports.stripTrailingSlash = stripTrailingSlash;
exports.supportsGoWithoutReloadUsingHash = supportsGoWithoutReloadUsingHash;
exports.supportsHistory = supportsHistory;
exports.supportsPopStateOnHashChange = supportsPopStateOnHashChange;