Skip to content

Instantly share code, notes, and snippets.

@eligrey
Last active April 19, 2024 06:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eligrey/282cb2bef74d81e73bee7e836c82d0a5 to your computer and use it in GitHub Desktop.
Save eligrey/282cb2bef74d81e73bee7e836c82d0a5 to your computer and use it in GitHub Desktop.
Fast URL.parse() polyfill
/**
* Fast URL.parse() polyfill
*
* Copyright (c) 2024 Transcend Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export default URL.parse ||= (() => {
const call = Function.prototype.call.bind(Function.prototype.call);
const { document, URLPattern, HTMLInputElement, ValidityState } = self;
const testURLPattern = URLPattern?.prototype?.test;
const anyValidURLPattern = URLPattern && new URLPattern();
const HTML_NS = 'http://www.w3.org/1999/xhtml';
const validateURL =
URLPattern &&
((input: unknown): boolean =>
input !== null &&
call(testURLPattern, anyValidURLPattern, `${input}`, 'http://-'));
/**
* Parse URL input into a URL instance and throws an error if the
* input is invalid.
*
* @param input - Stringifiable URL input (e.g. a string or URL instance)
* @param context - Context URL
* @returns Parsed URL
*/
const parseURL = (input: string | URL, context?: string | URL): URL =>
new URL(input, context);
/**
* Parses and validates any URL string. Returns null if the URL is invalid.
*
* @param input - Stringifiable URL input (e.g. a string or URL instance)
* @param context - Context URL
* @returns Parsed URL or null
*/
const parseAndValidateURL = (
input: string | URL,
context?: string | URL,
): URL | null => {
try {
return parseURL(input, context);
} catch (ex) {
return null;
}
};
/**
* Parses stringifiable URL input into a URL instance and
* returns null if the input is invalid.
*
* Implemented with three tiers:
* 1. fastest: URLPattern validation + new URL()
* 2. fast for absolute URLs: DOM validation + new URL() for absolute URLs / try...catch new URL() for relative URLs
* 3. slowest: try...catch new URL()
*
* @param input - Stringifiable URL input (e.g. a string or URL instance)
* @param context - Context URL
* @returns Parsed URL or null
*/
const parsePotentialURL: (
input: string | URL,
context?: string | URL,
) => URL | null = validateURL
? // URLPattern validator can be used instead of try...catch
(input, context) => (validateURL(input) ? parseURL(input, context) : null)
: document
? // if DOM is available, use input[type=url] validation for absolute URLs
(() => {
const validator = document.createElementNS(
HTML_NS,
'input',
) as HTMLInputElement;
validator.type = 'url';
const validityState = validator.validity;
const setValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
)!.set;
const getValidity = Object.getOwnPropertyDescriptor(
ValidityState.prototype,
'valid',
)!.get;
return (input, context) => {
// DOM can only validate absolute urls, so we still have to fallback
// to parseAndValidateURL for everything that doesn't pass this check
call(setValue, validator, input);
return input && call(getValidity, validityState)
? parseURL(input, context)
: parseAndValidateURL(input, context);
};
})()
: parseAndValidateURL;
return parsePotentialURL;
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment