| 'use strict' |
| |
| const { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, 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} */ (normalizeString(uri, 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) { |
| const normalizedA = normalizeComparableURI(uriA, options) |
| const normalizedB = normalizeComparableURI(uriB, options) |
| |
| return normalizedA !== undefined && normalizedB !== undefined && normalizedA.toLowerCase() === normalizedB.toLowerCase() |
| } |
| |
| /** |
| * @param {Readonly<import('./types/index').URIComponent>} 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 = escapePreservingEscapes(component.path) |
| |
| if (component.scheme !== undefined) { |
| component.path = component.path.split('%3A').join(':') |
| } |
| } else { |
| component.path = normalizePercentEncoding(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 {import('./types/index').URIComponent} parsed |
| * @param {RegExpMatchArray} matches |
| * @returns {string|undefined} |
| */ |
| function getParseError (parsed, matches) { |
| if (matches[2] !== undefined && parsed.path && parsed.path[0] !== '/') { |
| return 'URI path must start with "/" when authority is present.' |
| } |
| |
| if (typeof parsed.port === 'number' && (parsed.port < 0 || parsed.port > 65535)) { |
| return 'URI port is malformed.' |
| } |
| |
| return undefined |
| } |
| |
| /** |
| * @param {string} uri |
| * @param {import('./types/index').Options} [opts] |
| * @returns {{ parsed: import('./types/index').URIComponent, malformedAuthorityOrPort: boolean }} |
| */ |
| function parseWithStatus (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 malformedAuthorityOrPort = false |
| |
| 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] |
| } |
| |
| const parseError = getParseError(parsed, matches) |
| if (parseError !== undefined) { |
| parsed.error = parsed.error || parseError |
| malformedAuthorityOrPort = true |
| } |
| |
| 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 = reescapeHostDelimiters(unescape(parsed.host), isIP) |
| } |
| } |
| if (parsed.path) { |
| parsed.path = normalizePathEncoding(parsed.path) |
| } |
| if (parsed.fragment) { |
| try { |
| parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment)) |
| } catch { |
| parsed.error = parsed.error || 'URI malformed' |
| } |
| } |
| } |
| |
| // perform scheme specific parsing |
| if (schemeHandler && schemeHandler.parse) { |
| schemeHandler.parse(parsed, options) |
| } |
| } else { |
| parsed.error = parsed.error || 'URI can not be parsed.' |
| } |
| return { parsed, malformedAuthorityOrPort } |
| } |
| |
| /** |
| * @param {string} uri |
| * @param {import('./types/index').Options} [opts] |
| * @returns |
| */ |
| function parse (uri, opts) { |
| return parseWithStatus(uri, opts).parsed |
| } |
| |
| /** |
| * @param {string} uri |
| * @param {import('./types/index').Options} [opts] |
| * @returns {string} |
| */ |
| function normalizeString (uri, opts) { |
| return normalizeStringWithStatus(uri, opts).normalized |
| } |
| |
| /** |
| * @param {string} uri |
| * @param {import('./types/index').Options} [opts] |
| * @returns {{ normalized: string, malformedAuthorityOrPort: boolean }} |
| */ |
| function normalizeStringWithStatus (uri, opts) { |
| const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts) |
| return { |
| normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts), |
| malformedAuthorityOrPort |
| } |
| } |
| |
| /** |
| * @param {import ('./types/index').URIComponent|string} uri |
| * @param {import('./types/index').Options} [opts] |
| * @returns {string|undefined} |
| */ |
| function normalizeComparableURI (uri, opts) { |
| if (typeof uri === 'string') { |
| const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts) |
| return malformedAuthorityOrPort ? undefined : normalized |
| } |
| |
| if (typeof uri === 'object') { |
| return serialize(uri, opts) |
| } |
| } |
| |
| const fastUri = { |
| SCHEMES, |
| normalize, |
| resolve, |
| resolveComponent, |
| equal, |
| serialize, |
| parse |
| } |
| |
| module.exports = fastUri |
| module.exports.default = fastUri |
| module.exports.fastUri = fastUri |