UNPKG

kompendium

Version:

Documentation generator for Stencil components

573 lines (566 loc) 18.9 kB
'use strict'; /** * 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;