'use strict' const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require('./lib/utils') const { SCHEMES, getSchemeHandler } = require('./lib/schemes') /** * @template {import('./types/index').URIComponent|string} T * @param {T} uri * @param {import('./types/index').Options} [options] * @returns {T} */ function normalize (uri, options) { if (typeof uri === 'string') { uri = /** @type {T} */ (serialize(parse(uri, options), options)) } else if (typeof uri === 'object') { uri = /** @type {T} */ (parse(serialize(uri, options), options)) } return uri } /** * @param {string} baseURI * @param {string} relativeURI * @param {import('./types/index').Options} [options] * @returns {string} */ function resolve (baseURI, relativeURI, options) { const schemelessOptions = options ? Object.assign({ scheme: 'null' }, options) : { scheme: 'null' } const resolved = resolveComponent(parse(baseURI, schemelessOptions), parse(relativeURI, schemelessOptions), schemelessOptions, true) schemelessOptions.skipEscape = true return serialize(resolved, schemelessOptions) } /** * @param {import ('./types/index').URIComponent} base * @param {import ('./types/index').URIComponent} relative * @param {import('./types/index').Options} [options] * @param {boolean} [skipNormalization=false] * @returns {import ('./types/index').URIComponent} */ function resolveComponent (base, relative, options, skipNormalization) { /** @type {import('./types/index').URIComponent} */ const target = {} if (!skipNormalization) { base = parse(serialize(base, options), options) // normalize base component relative = parse(serialize(relative, options), options) // normalize relative component } options = options || {} if (!options.tolerant && relative.scheme) { target.scheme = relative.scheme // target.authority = relative.authority; target.userinfo = relative.userinfo target.host = relative.host target.port = relative.port target.path = removeDotSegments(relative.path || '') target.query = relative.query } else { if (relative.userinfo !== undefined || relative.host !== undefined || relative.port !== undefined) { // target.authority = relative.authority; target.userinfo = relative.userinfo target.host = relative.host target.port = relative.port target.path = removeDotSegments(relative.path || '') target.query = relative.query } else { if (!relative.path) { target.path = base.path if (relative.query !== undefined) { target.query = relative.query } else { target.query = base.query } } else { if (relative.path[0] === '/') { target.path = removeDotSegments(relative.path) } else { if ((base.userinfo !== undefined || base.host !== undefined || base.port !== undefined) && !base.path) { target.path = '/' + relative.path } else if (!base.path) { target.path = relative.path } else { target.path = base.path.slice(0, base.path.lastIndexOf('/') + 1) + relative.path } target.path = removeDotSegments(target.path) } target.query = relative.query } // target.authority = base.authority; target.userinfo = base.userinfo target.host = base.host target.port = base.port } target.scheme = base.scheme } target.fragment = relative.fragment return target } /** * @param {import ('./types/index').URIComponent|string} uriA * @param {import ('./types/index').URIComponent|string} uriB * @param {import ('./types/index').Options} options * @returns {boolean} */ function equal (uriA, uriB, options) { if (typeof uriA === 'string') { uriA = unescape(uriA) uriA = serialize(normalizeComponentEncoding(parse(uriA, options), true), { ...options, skipEscape: true }) } else if (typeof uriA === 'object') { uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true }) } if (typeof uriB === 'string') { uriB = unescape(uriB) uriB = serialize(normalizeComponentEncoding(parse(uriB, options), true), { ...options, skipEscape: true }) } else if (typeof uriB === 'object') { uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true }) } return uriA.toLowerCase() === uriB.toLowerCase() } /** * @param {Readonly} cmpts * @param {import('./types/index').Options} [opts] * @returns {string} */ function serialize (cmpts, opts) { const component = { host: cmpts.host, scheme: cmpts.scheme, userinfo: cmpts.userinfo, port: cmpts.port, path: cmpts.path, query: cmpts.query, nid: cmpts.nid, nss: cmpts.nss, uuid: cmpts.uuid, fragment: cmpts.fragment, reference: cmpts.reference, resourceName: cmpts.resourceName, secure: cmpts.secure, error: '' } const options = Object.assign({}, opts) const uriTokens = [] // find scheme handler const schemeHandler = getSchemeHandler(options.scheme || component.scheme) // perform scheme specific serialization if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options) if (component.path !== undefined) { if (!options.skipEscape) { component.path = escape(component.path) if (component.scheme !== undefined) { component.path = component.path.split('%3A').join(':') } } else { component.path = unescape(component.path) } } if (options.reference !== 'suffix' && component.scheme) { uriTokens.push(component.scheme, ':') } const authority = recomposeAuthority(component) if (authority !== undefined) { if (options.reference !== 'suffix') { uriTokens.push('//') } uriTokens.push(authority) if (component.path && component.path[0] !== '/') { uriTokens.push('/') } } if (component.path !== undefined) { let s = component.path if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) { s = removeDotSegments(s) } if ( authority === undefined && s[0] === '/' && s[1] === '/' ) { // don't allow the path to start with "//" s = '/%2F' + s.slice(2) } uriTokens.push(s) } if (component.query !== undefined) { uriTokens.push('?', component.query) } if (component.fragment !== undefined) { uriTokens.push('#', component.fragment) } return uriTokens.join('') } const URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u /** * @param {string} uri * @param {import('./types/index').Options} [opts] * @returns */ function parse (uri, opts) { const options = Object.assign({}, opts) /** @type {import('./types/index').URIComponent} */ const parsed = { scheme: undefined, userinfo: undefined, host: '', port: undefined, path: '', query: undefined, fragment: undefined } let isIP = false if (options.reference === 'suffix') { if (options.scheme) { uri = options.scheme + ':' + uri } else { uri = '//' + uri } } const matches = uri.match(URI_PARSE) if (matches) { // store each component parsed.scheme = matches[1] parsed.userinfo = matches[3] parsed.host = matches[4] parsed.port = parseInt(matches[5], 10) parsed.path = matches[6] || '' parsed.query = matches[7] parsed.fragment = matches[8] // fix port number if (isNaN(parsed.port)) { parsed.port = matches[5] } if (parsed.host) { const ipv4result = isIPv4(parsed.host) if (ipv4result === false) { const ipv6result = normalizeIPv6(parsed.host) parsed.host = ipv6result.host.toLowerCase() isIP = ipv6result.isIPV6 } else { isIP = true } } if (parsed.scheme === undefined && parsed.userinfo === undefined && parsed.host === undefined && parsed.port === undefined && parsed.query === undefined && !parsed.path) { parsed.reference = 'same-document' } else if (parsed.scheme === undefined) { parsed.reference = 'relative' } else if (parsed.fragment === undefined) { parsed.reference = 'absolute' } else { parsed.reference = 'uri' } // check for reference errors if (options.reference && options.reference !== 'suffix' && options.reference !== parsed.reference) { parsed.error = parsed.error || 'URI is not a ' + options.reference + ' reference.' } // find scheme handler const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme) // check if scheme can't handle IRIs if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) { // if host component is a domain name if (parsed.host && (options.domainHost || (schemeHandler && schemeHandler.domainHost)) && isIP === false && nonSimpleDomain(parsed.host)) { // convert Unicode IDN -> ASCII IDN try { parsed.host = URL.domainToASCII(parsed.host.toLowerCase()) } catch (e) { parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e } } // convert IRI -> URI } if (!schemeHandler || (schemeHandler && !schemeHandler.skipNormalize)) { if (uri.indexOf('%') !== -1) { if (parsed.scheme !== undefined) { parsed.scheme = unescape(parsed.scheme) } if (parsed.host !== undefined) { parsed.host = unescape(parsed.host) } } if (parsed.path) { parsed.path = escape(unescape(parsed.path)) } if (parsed.fragment) { parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment)) } } // perform scheme specific parsing if (schemeHandler && schemeHandler.parse) { schemeHandler.parse(parsed, options) } } else { parsed.error = parsed.error || 'URI can not be parsed.' } return parsed } const fastUri = { SCHEMES, normalize, resolve, resolveComponent, equal, serialize, parse } module.exports = fastUri module.exports.default = fastUri module.exports.fastUri = fastUri