From b59ef1e0838c2cbb426ceaf3c129a5fd4a438c42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Gomes?= Date: Mon, 2 Dec 2019 11:19:25 +0000 Subject: [PATCH] Split @wordpress/url into modules. (#18689) --- packages/url/src/add-query-args.js | 44 ++ packages/url/src/filter-url-for-display.js | 23 + packages/url/src/get-authority.js | 19 + packages/url/src/get-fragment.js | 19 + packages/url/src/get-path.js | 19 + packages/url/src/get-protocol.js | 19 + packages/url/src/get-query-arg.js | 32 ++ packages/url/src/get-query-string.js | 19 + packages/url/src/has-query-arg.js | 21 + packages/url/src/index.js | 454 +----------------- packages/url/src/is-email.js | 17 + packages/url/src/is-url.js | 17 + packages/url/src/is-valid-authority.js | 19 + packages/url/src/is-valid-fragment.js | 19 + packages/url/src/is-valid-path.js | 19 + packages/url/src/is-valid-protocol.js | 19 + packages/url/src/is-valid-query-string.js | 19 + packages/url/src/prepend-http.js | 31 ++ packages/url/src/remove-query-args.js | 27 ++ packages/url/src/safe-decode-uri-component.js | 15 + packages/url/src/safe-decode-uri.js | 20 + 21 files changed, 457 insertions(+), 434 deletions(-) create mode 100644 packages/url/src/add-query-args.js create mode 100644 packages/url/src/filter-url-for-display.js create mode 100644 packages/url/src/get-authority.js create mode 100644 packages/url/src/get-fragment.js create mode 100644 packages/url/src/get-path.js create mode 100644 packages/url/src/get-protocol.js create mode 100644 packages/url/src/get-query-arg.js create mode 100644 packages/url/src/get-query-string.js create mode 100644 packages/url/src/has-query-arg.js create mode 100644 packages/url/src/is-email.js create mode 100644 packages/url/src/is-url.js create mode 100644 packages/url/src/is-valid-authority.js create mode 100644 packages/url/src/is-valid-fragment.js create mode 100644 packages/url/src/is-valid-path.js create mode 100644 packages/url/src/is-valid-protocol.js create mode 100644 packages/url/src/is-valid-query-string.js create mode 100644 packages/url/src/prepend-http.js create mode 100644 packages/url/src/remove-query-args.js create mode 100644 packages/url/src/safe-decode-uri-component.js create mode 100644 packages/url/src/safe-decode-uri.js diff --git a/packages/url/src/add-query-args.js b/packages/url/src/add-query-args.js new file mode 100644 index 00000000000000..9a1add3af409be --- /dev/null +++ b/packages/url/src/add-query-args.js @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import { parse, stringify } from 'qs'; + +/** + * Appends arguments as querystring to the provided URL. If the URL already + * includes query arguments, the arguments are merged with (and take precedent + * over) the existing set. + * + * @param {string} [url=''] URL to which arguments should be appended. If omitted, + * only the resulting querystring is returned. + * @param {Object} args Query arguments to apply to URL. + * + * @example + * ```js + * const newURL = addQueryArgs( 'https://google.com', { q: 'test' } ); // https://google.com/?q=test + * ``` + * + * @return {string} URL with arguments applied. + */ +export function addQueryArgs( url = '', args ) { + // If no arguments are to be appended, return original URL. + if ( ! args || ! Object.keys( args ).length ) { + return url; + } + + let baseUrl = url; + + // Determine whether URL already had query arguments. + const queryStringIndex = url.indexOf( '?' ); + if ( queryStringIndex !== -1 ) { + // Merge into existing query arguments. + args = Object.assign( + parse( url.substr( queryStringIndex + 1 ) ), + args + ); + + // Change working base URL to omit previous query arguments. + baseUrl = baseUrl.substr( 0, queryStringIndex ); + } + + return baseUrl + '?' + stringify( args ); +} diff --git a/packages/url/src/filter-url-for-display.js b/packages/url/src/filter-url-for-display.js new file mode 100644 index 00000000000000..978594aced536c --- /dev/null +++ b/packages/url/src/filter-url-for-display.js @@ -0,0 +1,23 @@ +/** + * Returns a URL for display. + * + * @param {string} url Original URL. + * + * @example + * ```js + * const displayUrl = filterURLForDisplay( 'https://www.wordpress.org/gutenberg/' ); // wordpress.org/gutenberg + * ``` + * + * @return {string} Displayed URL. + */ +export function filterURLForDisplay( url ) { + // Remove protocol and www prefixes. + const filteredURL = url.replace( /^(?:https?:)\/\/(?:www\.)?/, '' ); + + // Ends with / and only has that single slash, strip it. + if ( filteredURL.match( /^[^\/]+\/$/ ) ) { + return filteredURL.replace( '/', '' ); + } + + return filteredURL; +} diff --git a/packages/url/src/get-authority.js b/packages/url/src/get-authority.js new file mode 100644 index 00000000000000..68315432f07cde --- /dev/null +++ b/packages/url/src/get-authority.js @@ -0,0 +1,19 @@ +/** + * Returns the authority part of the URL. + * + * @param {string} url The full URL. + * + * @example + * ```js + * const authority1 = getAuthority( 'https://wordpress.org/help/' ); // 'wordpress.org' + * const authority2 = getAuthority( 'https://localhost:8080/test/' ); // 'localhost:8080' + * ``` + * + * @return {string|void} The authority part of the URL. + */ +export function getAuthority( url ) { + const matches = /^[^\/\s:]+:(?:\/\/)?\/?([^\/\s#?]+)[\/#?]{0,1}\S*$/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} diff --git a/packages/url/src/get-fragment.js b/packages/url/src/get-fragment.js new file mode 100644 index 00000000000000..4f15ef0ce06af8 --- /dev/null +++ b/packages/url/src/get-fragment.js @@ -0,0 +1,19 @@ +/** + * Returns the fragment part of the URL. + * + * @param {string} url The full URL + * + * @example + * ```js + * const fragment1 = getFragment( 'http://localhost:8080/this/is/a/test?query=true#fragment' ); // '#fragment' + * const fragment2 = getFragment( 'https://wordpress.org#another-fragment?query=true' ); // '#another-fragment' + * ``` + * + * @return {string|void} The fragment part of the URL. + */ +export function getFragment( url ) { + const matches = /^\S+?(#[^\s\?]*)/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} diff --git a/packages/url/src/get-path.js b/packages/url/src/get-path.js new file mode 100644 index 00000000000000..ddc79b4963c212 --- /dev/null +++ b/packages/url/src/get-path.js @@ -0,0 +1,19 @@ +/** + * Returns the path part of the URL. + * + * @param {string} url The full URL. + * + * @example + * ```js + * const path1 = getPath( 'http://localhost:8080/this/is/a/test?query=true' ); // 'this/is/a/test' + * const path2 = getPath( 'https://wordpress.org/help/faq/' ); // 'help/faq' + * ``` + * + * @return {string|void} The path part of the URL. + */ +export function getPath( url ) { + const matches = /^[^\/\s:]+:(?:\/\/)?[^\/\s#?]+[\/]([^\s#?]+)[#?]{0,1}\S*$/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} diff --git a/packages/url/src/get-protocol.js b/packages/url/src/get-protocol.js new file mode 100644 index 00000000000000..07b375c10f841b --- /dev/null +++ b/packages/url/src/get-protocol.js @@ -0,0 +1,19 @@ +/** + * Returns the protocol part of the URL. + * + * @param {string} url The full URL. + * + * @example + * ```js + * const protocol1 = getProtocol( 'tel:012345678' ); // 'tel:' + * const protocol2 = getProtocol( 'https://wordpress.org' ); // 'https:' + * ``` + * + * @return {string|void} The protocol part of the URL. + */ +export function getProtocol( url ) { + const matches = /^([^\s:]+:)/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} diff --git a/packages/url/src/get-query-arg.js b/packages/url/src/get-query-arg.js new file mode 100644 index 00000000000000..2dffc6fd4135a7 --- /dev/null +++ b/packages/url/src/get-query-arg.js @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { parse } from 'qs'; + +/** + * @typedef {{[key: string]: QueryArgParsed}} QueryArgObject + */ + +/** + * @typedef {string|string[]|QueryArgObject} QueryArgParsed + */ + +/** + * Returns a single query argument of the url + * + * @param {string} url URL. + * @param {string} arg Query arg name. + * + * @example + * ```js + * const foo = getQueryArg( 'https://wordpress.org?foo=bar&bar=baz', 'foo' ); // bar + * ``` + * + * @return {QueryArgParsed|undefined} Query arg value. + */ +export function getQueryArg( url, arg ) { + const queryStringIndex = url.indexOf( '?' ); + const query = queryStringIndex !== -1 ? parse( url.substr( queryStringIndex + 1 ) ) : {}; + + return query[ arg ]; +} diff --git a/packages/url/src/get-query-string.js b/packages/url/src/get-query-string.js new file mode 100644 index 00000000000000..8341da1f9fa1bd --- /dev/null +++ b/packages/url/src/get-query-string.js @@ -0,0 +1,19 @@ +/** + * Returns the query string part of the URL. + * + * @param {string} url The full URL. + * + * @example + * ```js + * const queryString1 = getQueryString( 'http://localhost:8080/this/is/a/test?query=true#fragment' ); // 'query=true' + * const queryString2 = getQueryString( 'https://wordpress.org#fragment?query=false&search=hello' ); // 'query=false&search=hello' + * ``` + * + * @return {string|void} The query string part of the URL. + */ +export function getQueryString( url ) { + const matches = /^\S+?\?([^\s#]+)/.exec( url ); + if ( matches ) { + return matches[ 1 ]; + } +} diff --git a/packages/url/src/has-query-arg.js b/packages/url/src/has-query-arg.js new file mode 100644 index 00000000000000..a940338dd16655 --- /dev/null +++ b/packages/url/src/has-query-arg.js @@ -0,0 +1,21 @@ +/** + * Internal dependencies + */ +import { getQueryArg } from './get-query-arg'; + +/** + * Determines whether the URL contains a given query arg. + * + * @param {string} url URL. + * @param {string} arg Query arg name. + * + * @example + * ```js + * const hasBar = hasQueryArg( 'https://wordpress.org?foo=bar&bar=baz', 'bar' ); // true + * ``` + * + * @return {boolean} Whether or not the URL contains the query arg. + */ +export function hasQueryArg( url, arg ) { + return getQueryArg( url, arg ) !== undefined; +} diff --git a/packages/url/src/index.js b/packages/url/src/index.js index 2b549d6ba1dc4e..e39d95f2804867 100644 --- a/packages/url/src/index.js +++ b/packages/url/src/index.js @@ -1,434 +1,20 @@ -/** - * External dependencies - */ -import { parse, stringify } from 'qs'; - -const URL_REGEXP = /^(?:https?:)?\/\/\S+$/i; -const EMAIL_REGEXP = /^(mailto:)?[a-z0-9._%+-]+@[a-z0-9][a-z0-9.-]*\.[a-z]{2,63}$/i; -const USABLE_HREF_REGEXP = /^(?:[a-z]+:|#|\?|\.|\/)/i; - -/** - * @typedef {{[key: string]: QueryArgParsed}} QueryArgObject - */ - -/** - * @typedef {string|string[]|QueryArgObject} QueryArgParsed - */ - -/** - * Determines whether the given string looks like a URL. - * - * @param {string} url The string to scrutinise. - * - * @example - * ```js - * const isURL = isURL( 'https://wordpress.org' ); // true - * ``` - * - * @return {boolean} Whether or not it looks like a URL. - */ -export function isURL( url ) { - return URL_REGEXP.test( url ); -} - -/** - * Determines whether the given string looks like an email. - * - * @param {string} email The string to scrutinise. - * - * @example - * ```js - * const isEmail = isEmail( 'hello@wordpress.org' ); // true - * ``` - * - * @return {boolean} Whether or not it looks like an email. - */ -export function isEmail( email ) { - return EMAIL_REGEXP.test( email ); -} - -/** - * Returns the protocol part of the URL. - * - * @param {string} url The full URL. - * - * @example - * ```js - * const protocol1 = getProtocol( 'tel:012345678' ); // 'tel:' - * const protocol2 = getProtocol( 'https://wordpress.org' ); // 'https:' - * ``` - * - * @return {string|void} The protocol part of the URL. - */ -export function getProtocol( url ) { - const matches = /^([^\s:]+:)/.exec( url ); - if ( matches ) { - return matches[ 1 ]; - } -} - -/** - * Tests if a url protocol is valid. - * - * @param {string} protocol The url protocol. - * - * @example - * ```js - * const isValid = isValidProtocol( 'https:' ); // true - * const isNotValid = isValidProtocol( 'https :' ); // false - * ``` - * - * @return {boolean} True if the argument is a valid protocol (e.g. http:, tel:). - */ -export function isValidProtocol( protocol ) { - if ( ! protocol ) { - return false; - } - return /^[a-z\-.\+]+[0-9]*:$/i.test( protocol ); -} - -/** - * Returns the authority part of the URL. - * - * @param {string} url The full URL. - * - * @example - * ```js - * const authority1 = getAuthority( 'https://wordpress.org/help/' ); // 'wordpress.org' - * const authority2 = getAuthority( 'https://localhost:8080/test/' ); // 'localhost:8080' - * ``` - * - * @return {string|void} The authority part of the URL. - */ -export function getAuthority( url ) { - const matches = /^[^\/\s:]+:(?:\/\/)?\/?([^\/\s#?]+)[\/#?]{0,1}\S*$/.exec( url ); - if ( matches ) { - return matches[ 1 ]; - } -} - -/** - * Checks for invalid characters within the provided authority. - * - * @param {string} authority A string containing the URL authority. - * - * @example - * ```js - * const isValid = isValidAuthority( 'wordpress.org' ); // true - * const isNotValid = isValidAuthority( 'wordpress#org' ); // false - * ``` - * - * @return {boolean} True if the argument contains a valid authority. - */ -export function isValidAuthority( authority ) { - if ( ! authority ) { - return false; - } - return /^[^\s#?]+$/.test( authority ); -} - -/** - * Returns the path part of the URL. - * - * @param {string} url The full URL. - * - * @example - * ```js - * const path1 = getPath( 'http://localhost:8080/this/is/a/test?query=true' ); // 'this/is/a/test' - * const path2 = getPath( 'https://wordpress.org/help/faq/' ); // 'help/faq' - * ``` - * - * @return {string|void} The path part of the URL. - */ -export function getPath( url ) { - const matches = /^[^\/\s:]+:(?:\/\/)?[^\/\s#?]+[\/]([^\s#?]+)[#?]{0,1}\S*$/.exec( url ); - if ( matches ) { - return matches[ 1 ]; - } -} - -/** - * Checks for invalid characters within the provided path. - * - * @param {string} path The URL path. - * - * @example - * ```js - * const isValid = isValidPath( 'test/path/' ); // true - * const isNotValid = isValidPath( '/invalid?test/path/' ); // false - * ``` - * - * @return {boolean} True if the argument contains a valid path - */ -export function isValidPath( path ) { - if ( ! path ) { - return false; - } - return /^[^\s#?]+$/.test( path ); -} - -/** - * Returns the query string part of the URL. - * - * @param {string} url The full URL. - * - * @example - * ```js - * const queryString1 = getQueryString( 'http://localhost:8080/this/is/a/test?query=true#fragment' ); // 'query=true' - * const queryString2 = getQueryString( 'https://wordpress.org#fragment?query=false&search=hello' ); // 'query=false&search=hello' - * ``` - * - * @return {string|void} The query string part of the URL. - */ -export function getQueryString( url ) { - const matches = /^\S+?\?([^\s#]+)/.exec( url ); - if ( matches ) { - return matches[ 1 ]; - } -} - -/** - * Checks for invalid characters within the provided query string. - * - * @param {string} queryString The query string. - * - * @example - * ```js - * const isValid = isValidQueryString( 'query=true&another=false' ); // true - * const isNotValid = isValidQueryString( 'query=true?another=false' ); // false - * ``` - * - * @return {boolean} True if the argument contains a valid query string. - */ -export function isValidQueryString( queryString ) { - if ( ! queryString ) { - return false; - } - return /^[^\s#?\/]+$/.test( queryString ); -} - -/** - * Returns the fragment part of the URL. - * - * @param {string} url The full URL - * - * @example - * ```js - * const fragment1 = getFragment( 'http://localhost:8080/this/is/a/test?query=true#fragment' ); // '#fragment' - * const fragment2 = getFragment( 'https://wordpress.org#another-fragment?query=true' ); // '#another-fragment' - * ``` - * - * @return {string|void} The fragment part of the URL. - */ -export function getFragment( url ) { - const matches = /^\S+?(#[^\s\?]*)/.exec( url ); - if ( matches ) { - return matches[ 1 ]; - } -} - -/** - * Checks for invalid characters within the provided fragment. - * - * @param {string} fragment The url fragment. - * - * @example - * ```js - * const isValid = isValidFragment( '#valid-fragment' ); // true - * const isNotValid = isValidFragment( '#invalid-#fragment' ); // false - * ``` - * - * @return {boolean} True if the argument contains a valid fragment. - */ -export function isValidFragment( fragment ) { - if ( ! fragment ) { - return false; - } - return /^#[^\s#?\/]*$/.test( fragment ); -} - -/** - * Appends arguments as querystring to the provided URL. If the URL already - * includes query arguments, the arguments are merged with (and take precedent - * over) the existing set. - * - * @param {string} [url=''] URL to which arguments should be appended. If omitted, - * only the resulting querystring is returned. - * @param {Object} args Query arguments to apply to URL. - * - * @example - * ```js - * const newURL = addQueryArgs( 'https://google.com', { q: 'test' } ); // https://google.com/?q=test - * ``` - * - * @return {string} URL with arguments applied. - */ -export function addQueryArgs( url = '', args ) { - // If no arguments are to be appended, return original URL. - if ( ! args || ! Object.keys( args ).length ) { - return url; - } - - let baseUrl = url; - - // Determine whether URL already had query arguments. - const queryStringIndex = url.indexOf( '?' ); - if ( queryStringIndex !== -1 ) { - // Merge into existing query arguments. - args = Object.assign( - parse( url.substr( queryStringIndex + 1 ) ), - args - ); - - // Change working base URL to omit previous query arguments. - baseUrl = baseUrl.substr( 0, queryStringIndex ); - } - - return baseUrl + '?' + stringify( args ); -} - -/** - * Returns a single query argument of the url - * - * @param {string} url URL. - * @param {string} arg Query arg name. - * - * @example - * ```js - * const foo = getQueryArg( 'https://wordpress.org?foo=bar&bar=baz', 'foo' ); // bar - * ``` - * - * @return {QueryArgParsed|undefined} Query arg value. - */ -export function getQueryArg( url, arg ) { - const queryStringIndex = url.indexOf( '?' ); - const query = queryStringIndex !== -1 ? parse( url.substr( queryStringIndex + 1 ) ) : {}; - - return query[ arg ]; -} - -/** - * Determines whether the URL contains a given query arg. - * - * @param {string} url URL. - * @param {string} arg Query arg name. - * - * @example - * ```js - * const hasBar = hasQueryArg( 'https://wordpress.org?foo=bar&bar=baz', 'bar' ); // true - * ``` - * - * @return {boolean} Whether or not the URL contains the query arg. - */ -export function hasQueryArg( url, arg ) { - return getQueryArg( url, arg ) !== undefined; -} - -/** - * Removes arguments from the query string of the url - * - * @param {string} url URL. - * @param {...string} args Query Args. - * - * @example - * ```js - * const newUrl = removeQueryArgs( 'https://wordpress.org?foo=bar&bar=baz&baz=foobar', 'foo', 'bar' ); // https://wordpress.org?baz=foobar - * ``` - * - * @return {string} Updated URL. - */ -export function removeQueryArgs( url, ...args ) { - const queryStringIndex = url.indexOf( '?' ); - const query = queryStringIndex !== -1 ? parse( url.substr( queryStringIndex + 1 ) ) : {}; - const baseUrl = queryStringIndex !== -1 ? url.substr( 0, queryStringIndex ) : url; - - args.forEach( ( arg ) => delete query[ arg ] ); - - return baseUrl + '?' + stringify( query ); -} - -/** - * Prepends "http://" to a url, if it looks like something that is meant to be a TLD. - * - * @param {string} url The URL to test. - * - * @example - * ```js - * const actualURL = prependHTTP( 'wordpress.org' ); // http://wordpress.org - * ``` - * - * @return {string} The updated URL. - */ -export function prependHTTP( url ) { - if ( ! url ) { - return url; - } - - url = url.trim(); - if ( ! USABLE_HREF_REGEXP.test( url ) && ! EMAIL_REGEXP.test( url ) ) { - return 'http://' + url; - } - - return url; -} - -/** - * Safely decodes a URI with `decodeURI`. Returns the URI unmodified if - * `decodeURI` throws an error. - * - * @param {string} uri URI to decode. - * - * @example - * ```js - * const badUri = safeDecodeURI( '%z' ); // does not throw an Error, simply returns '%z' - * ``` - * - * @return {string} Decoded URI if possible. - */ -export function safeDecodeURI( uri ) { - try { - return decodeURI( uri ); - } catch ( uriError ) { - return uri; - } -} - -/** - * Returns a URL for display. - * - * @param {string} url Original URL. - * - * @example - * ```js - * const displayUrl = filterURLForDisplay( 'https://www.wordpress.org/gutenberg/' ); // wordpress.org/gutenberg - * ``` - * - * @return {string} Displayed URL. - */ -export function filterURLForDisplay( url ) { - // Remove protocol and www prefixes. - const filteredURL = url.replace( /^(?:https?:)\/\/(?:www\.)?/, '' ); - - // Ends with / and only has that single slash, strip it. - if ( filteredURL.match( /^[^\/]+\/$/ ) ) { - return filteredURL.replace( '/', '' ); - } - - return filteredURL; -} - -/** - * Safely decodes a URI component with `decodeURIComponent`. Returns the URI component unmodified if - * `decodeURIComponent` throws an error. - * - * @param {string} uriComponent URI component to decode. - * - * @return {string} Decoded URI component if possible. - */ -export function safeDecodeURIComponent( uriComponent ) { - try { - return decodeURIComponent( uriComponent ); - } catch ( uriComponentError ) { - return uriComponent; - } -} +export { isURL } from './is-url'; +export { isEmail } from './is-email'; +export { getProtocol } from './get-protocol'; +export { isValidProtocol } from './is-valid-protocol'; +export { getAuthority } from './get-authority'; +export { isValidAuthority } from './is-valid-authority'; +export { getPath } from './get-path'; +export { isValidPath } from './is-valid-path'; +export { getQueryString } from './get-query-string'; +export { isValidQueryString } from './is-valid-query-string'; +export { getFragment } from './get-fragment'; +export { isValidFragment } from './is-valid-fragment'; +export { addQueryArgs } from './add-query-args'; +export { getQueryArg } from './get-query-arg'; +export { hasQueryArg } from './has-query-arg'; +export { removeQueryArgs } from './remove-query-args'; +export { prependHTTP } from './prepend-http'; +export { safeDecodeURI } from './safe-decode-uri'; +export { safeDecodeURIComponent } from './safe-decode-uri-component'; +export { filterURLForDisplay } from './filter-url-for-display'; diff --git a/packages/url/src/is-email.js b/packages/url/src/is-email.js new file mode 100644 index 00000000000000..85f007d5d2713d --- /dev/null +++ b/packages/url/src/is-email.js @@ -0,0 +1,17 @@ +const EMAIL_REGEXP = /^(mailto:)?[a-z0-9._%+-]+@[a-z0-9][a-z0-9.-]*\.[a-z]{2,63}$/i; + +/** + * Determines whether the given string looks like an email. + * + * @param {string} email The string to scrutinise. + * + * @example + * ```js + * const isEmail = isEmail( 'hello@wordpress.org' ); // true + * ``` + * + * @return {boolean} Whether or not it looks like an email. + */ +export function isEmail( email ) { + return EMAIL_REGEXP.test( email ); +} diff --git a/packages/url/src/is-url.js b/packages/url/src/is-url.js new file mode 100644 index 00000000000000..5f3c854f93ad7d --- /dev/null +++ b/packages/url/src/is-url.js @@ -0,0 +1,17 @@ +const URL_REGEXP = /^(?:https?:)?\/\/\S+$/i; + +/** + * Determines whether the given string looks like a URL. + * + * @param {string} url The string to scrutinise. + * + * @example + * ```js + * const isURL = isURL( 'https://wordpress.org' ); // true + * ``` + * + * @return {boolean} Whether or not it looks like a URL. + */ +export function isURL( url ) { + return URL_REGEXP.test( url ); +} diff --git a/packages/url/src/is-valid-authority.js b/packages/url/src/is-valid-authority.js new file mode 100644 index 00000000000000..4734259b34b19c --- /dev/null +++ b/packages/url/src/is-valid-authority.js @@ -0,0 +1,19 @@ +/** + * Checks for invalid characters within the provided authority. + * + * @param {string} authority A string containing the URL authority. + * + * @example + * ```js + * const isValid = isValidAuthority( 'wordpress.org' ); // true + * const isNotValid = isValidAuthority( 'wordpress#org' ); // false + * ``` + * + * @return {boolean} True if the argument contains a valid authority. + */ +export function isValidAuthority( authority ) { + if ( ! authority ) { + return false; + } + return /^[^\s#?]+$/.test( authority ); +} diff --git a/packages/url/src/is-valid-fragment.js b/packages/url/src/is-valid-fragment.js new file mode 100644 index 00000000000000..ce19c0f859e5a7 --- /dev/null +++ b/packages/url/src/is-valid-fragment.js @@ -0,0 +1,19 @@ +/** + * Checks for invalid characters within the provided fragment. + * + * @param {string} fragment The url fragment. + * + * @example + * ```js + * const isValid = isValidFragment( '#valid-fragment' ); // true + * const isNotValid = isValidFragment( '#invalid-#fragment' ); // false + * ``` + * + * @return {boolean} True if the argument contains a valid fragment. + */ +export function isValidFragment( fragment ) { + if ( ! fragment ) { + return false; + } + return /^#[^\s#?\/]*$/.test( fragment ); +} diff --git a/packages/url/src/is-valid-path.js b/packages/url/src/is-valid-path.js new file mode 100644 index 00000000000000..7ab5d9d95eeca7 --- /dev/null +++ b/packages/url/src/is-valid-path.js @@ -0,0 +1,19 @@ +/** + * Checks for invalid characters within the provided path. + * + * @param {string} path The URL path. + * + * @example + * ```js + * const isValid = isValidPath( 'test/path/' ); // true + * const isNotValid = isValidPath( '/invalid?test/path/' ); // false + * ``` + * + * @return {boolean} True if the argument contains a valid path + */ +export function isValidPath( path ) { + if ( ! path ) { + return false; + } + return /^[^\s#?]+$/.test( path ); +} diff --git a/packages/url/src/is-valid-protocol.js b/packages/url/src/is-valid-protocol.js new file mode 100644 index 00000000000000..689768114318c4 --- /dev/null +++ b/packages/url/src/is-valid-protocol.js @@ -0,0 +1,19 @@ +/** + * Tests if a url protocol is valid. + * + * @param {string} protocol The url protocol. + * + * @example + * ```js + * const isValid = isValidProtocol( 'https:' ); // true + * const isNotValid = isValidProtocol( 'https :' ); // false + * ``` + * + * @return {boolean} True if the argument is a valid protocol (e.g. http:, tel:). + */ +export function isValidProtocol( protocol ) { + if ( ! protocol ) { + return false; + } + return /^[a-z\-.\+]+[0-9]*:$/i.test( protocol ); +} diff --git a/packages/url/src/is-valid-query-string.js b/packages/url/src/is-valid-query-string.js new file mode 100644 index 00000000000000..039cede44de959 --- /dev/null +++ b/packages/url/src/is-valid-query-string.js @@ -0,0 +1,19 @@ +/** + * Checks for invalid characters within the provided query string. + * + * @param {string} queryString The query string. + * + * @example + * ```js + * const isValid = isValidQueryString( 'query=true&another=false' ); // true + * const isNotValid = isValidQueryString( 'query=true?another=false' ); // false + * ``` + * + * @return {boolean} True if the argument contains a valid query string. + */ +export function isValidQueryString( queryString ) { + if ( ! queryString ) { + return false; + } + return /^[^\s#?\/]+$/.test( queryString ); +} diff --git a/packages/url/src/prepend-http.js b/packages/url/src/prepend-http.js new file mode 100644 index 00000000000000..e76488c51a4266 --- /dev/null +++ b/packages/url/src/prepend-http.js @@ -0,0 +1,31 @@ +/** + * Internal dependencies + */ +import { isEmail } from './is-email'; + +const USABLE_HREF_REGEXP = /^(?:[a-z]+:|#|\?|\.|\/)/i; + +/** + * Prepends "http://" to a url, if it looks like something that is meant to be a TLD. + * + * @param {string} url The URL to test. + * + * @example + * ```js + * const actualURL = prependHTTP( 'wordpress.org' ); // http://wordpress.org + * ``` + * + * @return {string} The updated URL. + */ +export function prependHTTP( url ) { + if ( ! url ) { + return url; + } + + url = url.trim(); + if ( ! USABLE_HREF_REGEXP.test( url ) && ! isEmail( url ) ) { + return 'http://' + url; + } + + return url; +} diff --git a/packages/url/src/remove-query-args.js b/packages/url/src/remove-query-args.js new file mode 100644 index 00000000000000..58dc1edd8ebab2 --- /dev/null +++ b/packages/url/src/remove-query-args.js @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { parse, stringify } from 'qs'; + +/** + * Removes arguments from the query string of the url + * + * @param {string} url URL. + * @param {...string} args Query Args. + * + * @example + * ```js + * const newUrl = removeQueryArgs( 'https://wordpress.org?foo=bar&bar=baz&baz=foobar', 'foo', 'bar' ); // https://wordpress.org?baz=foobar + * ``` + * + * @return {string} Updated URL. + */ +export function removeQueryArgs( url, ...args ) { + const queryStringIndex = url.indexOf( '?' ); + const query = queryStringIndex !== -1 ? parse( url.substr( queryStringIndex + 1 ) ) : {}; + const baseUrl = queryStringIndex !== -1 ? url.substr( 0, queryStringIndex ) : url; + + args.forEach( ( arg ) => delete query[ arg ] ); + + return baseUrl + '?' + stringify( query ); +} diff --git a/packages/url/src/safe-decode-uri-component.js b/packages/url/src/safe-decode-uri-component.js new file mode 100644 index 00000000000000..2bf2047803fba3 --- /dev/null +++ b/packages/url/src/safe-decode-uri-component.js @@ -0,0 +1,15 @@ +/** + * Safely decodes a URI component with `decodeURIComponent`. Returns the URI component unmodified if + * `decodeURIComponent` throws an error. + * + * @param {string} uriComponent URI component to decode. + * + * @return {string} Decoded URI component if possible. + */ +export function safeDecodeURIComponent( uriComponent ) { + try { + return decodeURIComponent( uriComponent ); + } catch ( uriComponentError ) { + return uriComponent; + } +} diff --git a/packages/url/src/safe-decode-uri.js b/packages/url/src/safe-decode-uri.js new file mode 100644 index 00000000000000..f5f6379e19164a --- /dev/null +++ b/packages/url/src/safe-decode-uri.js @@ -0,0 +1,20 @@ +/** + * Safely decodes a URI with `decodeURI`. Returns the URI unmodified if + * `decodeURI` throws an error. + * + * @param {string} uri URI to decode. + * + * @example + * ```js + * const badUri = safeDecodeURI( '%z' ); // does not throw an Error, simply returns '%z' + * ``` + * + * @return {string} Decoded URI if possible. + */ +export function safeDecodeURI( uri ) { + try { + return decodeURI( uri ); + } catch ( uriError ) { + return uri; + } +}