const path = require('bare-path') const binding = require('./binding') const errors = require('./lib/errors') const URLSearchParams = require('./lib/url-search-params') const kind = Symbol.for('bare.url.kind') const isWindows = Bare.platform === 'win32' module.exports = exports = class URL { static get [kind]() { return 0 // Compatibility version } constructor(input, base, opts = {}) { if (arguments.length === 0) throw errors.INVALID_URL() input = String(input) if (base !== undefined) base = String(base) this._components = new Uint32Array(8) this._parse(input, base, opts.throw !== false) if (this._href) this._params = new URLSearchParams(this.search, this) } get [kind]() { return URL[kind] } // https://url.spec.whatwg.org/#dom-url-href get href() { return this._href } set href(value) { this._update(value) this._params._parse(this.search) } // https://url.spec.whatwg.org/#dom-url-protocol get protocol() { return this._slice(0, this._components[0]) + ':' } set protocol(value) { this._update( this._replace(value.replace(/:+$/, ''), 0, this._components[0]) ) } // https://url.spec.whatwg.org/#dom-url-username get username() { return this._slice(this._components[0] + 3 /* :// */, this._components[1]) } set username(value) { if (cannotHaveCredentialsOrPort(this)) { return } if (this.username === '') value += '@' this._update( this._replace( value, this._components[0] + 3 /* :// */, this._components[1] ) ) } // https://url.spec.whatwg.org/#dom-url-password get password() { return this._href.slice( this._components[1] + 1 /* : */, this._components[2] - 1 /* @ */ ) } set password(value) { if (cannotHaveCredentialsOrPort(this)) { return } let start = this._components[1] + 1 /* : */ let end = this._components[2] - 1 /* @ */ if (this.password === '') { value = ':' + value start-- } if (this.username === '') { value += '@' end++ } this._update(this._replace(value, start, end)) } // https://url.spec.whatwg.org/#dom-url-host get host() { return this._slice(this._components[2], this._components[5]) } set host(value) { if (hasOpaquePath(this)) { return } this._update( this._replace( value, this._components[2], this._components[value.includes(':') ? 5 : 3] ) ) } // https://url.spec.whatwg.org/#dom-url-hostname get hostname() { return this._slice(this._components[2], this._components[3]) } set hostname(value) { if (hasOpaquePath(this)) { return } this._update(this._replace(value, this._components[2], this._components[3])) } // https://url.spec.whatwg.org/#dom-url-port get port() { return this._slice(this._components[3] + 1 /* : */, this._components[5]) } set port(value) { if (cannotHaveCredentialsOrPort(this)) { return } let start = this._components[3] + 1 /* : */ if (this.port === '') { value = ':' + value start-- } this._update(this._replace(value, start, this._components[5])) } // https://url.spec.whatwg.org/#dom-url-pathname get pathname() { return this._slice(this._components[5], this._components[6] - 1 /* ? */) } set pathname(value) { if (hasOpaquePath(this)) { return } if (value[0] !== '/' && value[0] !== '\\') { value = '/' + value } this._update( this._replace(value, this._components[5], this._components[6] - 1 /* ? */) ) } // https://url.spec.whatwg.org/#dom-url-search get search() { return this._slice( this._components[6] - 1 /* ? */, this._components[7] - 1 /* # */ ) } set search(value) { if (value && value[0] !== '?') value = '?' + value this._update( this._replace( value, this._components[6] - 1 /* ? */, this._components[7] - 1 /* # */ ) ) this._params._parse(this.search) } // https://url.spec.whatwg.org/#dom-url-searchparams get searchParams() { return this._params } // https://url.spec.whatwg.org/#dom-url-hash get hash() { return this._slice(this._components[7] - 1 /* # */) } set hash(value) { if (value && value[0] !== '#') value = '#' + value this._update(this._replace(value, this._components[7] - 1 /* # */)) } toString() { return this._href } toJSON() { return this._href } [Symbol.for('bare.inspect')]() { return { __proto__: { constructor: URL }, href: this.href, protocol: this.protocol, username: this.username, password: this.password, host: this.host, hostname: this.hostname, port: this.port, pathname: this.pathname, search: this.search, searchParams: this.searchParams, hash: this.hash } } _slice(start, end = this._href.length) { return this._href.slice(start, end) } _replace(replacement, start, end = this._href.length) { return this._slice(0, start) + replacement + this._slice(end) } _parse(input, base, shouldThrow) { try { this._href = binding.parse( String(input), base ? String(base) : null, this._components, shouldThrow ) } catch (err) { if (err instanceof TypeError) throw err throw errors.INVALID_URL() } } _update(input) { try { this._parse(input, null, true) } catch (err) { if (err instanceof TypeError) throw err } } } // https://url.spec.whatwg.org/#url-opaque-path function hasOpaquePath(url) { return url.pathname[0] !== '/' } // https://url.spec.whatwg.org/#cannot-have-a-username-password-port function cannotHaveCredentialsOrPort(url) { return url.hostname === '' || url.protocol === 'file:' } const URL = exports exports.URL = URL exports.URLSearchParams = URLSearchParams exports.errors = errors exports.isURL = function isURL(value) { if (value instanceof URL) return true return ( typeof value === 'object' && value !== null && value[kind] === URL[kind] ) } // https://url.spec.whatwg.org/#dom-url-parse exports.parse = function parse(input, base) { const url = new URL(input, base, { throw: false }) return url._href ? url : null } // https://url.spec.whatwg.org/#dom-url-canparse exports.canParse = function canParse(input, base) { return binding.canParse(String(input), base ? String(base) : null) } exports.fileURLToPath = function fileURLToPath(url) { if (typeof url === 'string') { url = new URL(url) } if (url.protocol !== 'file:') { throw errors.INVALID_URL_SCHEME('The URL must use the file: protocol') } if (isWindows) { if (/%2f|%5c/i.test(url.pathname)) { throw errors.INVALID_FILE_URL_PATH( 'The file: URL path must not include encoded \\ or / characters' ) } } else { if (url.hostname) { throw errors.INVALID_FILE_URL_HOST( "The file: URL host must be 'localhost' or empty" ) } if (/%2f/i.test(url.pathname)) { throw errors.INVALID_FILE_URL_PATH( 'The file: URL path must not include encoded / characters' ) } } const pathname = path.normalize(decodeURIComponent(url.pathname)) if (isWindows) { if (url.hostname) return '\\\\' + url.hostname + pathname const letter = pathname.charCodeAt(1) | 0x20 if ( letter < 0x61 /* a */ || letter > 0x7a /* z */ || pathname.charCodeAt(2) !== 0x3a /* : */ ) { throw errors.INVALID_FILE_URL_PATH('The file: URL path must be absolute') } return pathname.slice(1) } return pathname } exports.pathToFileURL = function pathToFileURL(pathname) { let resolved = path.resolve(pathname) if (pathname[pathname.length - 1] === '/') { resolved += '/' } else if (isWindows && pathname[pathname.length - 1] === '\\') { resolved += '\\' } resolved = resolved .replaceAll('%', '%25') // Must be first .replaceAll('#', '%23') .replaceAll('?', '%3f') .replaceAll('\n', '%0a') .replaceAll('\r', '%0d') .replaceAll('\t', '%09') if (!isWindows) { resolved = resolved.replaceAll('\\', '%5c') } return new URL('file:' + resolved) } exports.format = function format(parts) { const { protocol, auth, host, hostname, port, pathname, search, query, hash, slashes } = parts let result = '' if (typeof protocol === 'string') { result += protocol if (protocol[protocol.length - 1] !== ':') { result += ':' } if (slashes === true || /https?|ftp|gopher|file/.test(protocol)) { result += '//' } } if (typeof auth === 'string') { if (host || hostname) result += auth + '@' } if (typeof host === 'string') result += host else { result += hostname if (port) result += ':' + port } if (typeof pathname === 'string' && pathname !== '') { if (pathname[0] !== '/') result += '/' result += pathname } if (typeof search === 'string') { if (search[0] !== '?') result += '?' result += search } else if (typeof query === 'object' && query !== null) { result += '?' + new URLSearchParams(query) } if (typeof hash === 'string') { if (hash[0] !== '#') result += '#' result += hash } return result }