@beta3-z/no-url
v0.0.1
Published
Minimal url validation utility around URL object
Downloads
31
Readme
Minimal url validation utility around URL object.
⚠️ Consider other libraries like is-url and validator.js
Installation
Recommended
Copy-paste the code to your lib/is-url.ts or utils/is-url.ts. You don't need this dependency.
export type IsUrlOptions = {
protocols?: string[]
restrictedWhitespaceCharactersRegExp?: RegExp
requireProtocol?: boolean
strictProtocolSlashes?: boolean
allowTrailingDot?: boolean
allowZeroPort?: boolean
allowWhitespaceCharacters?: boolean
allowLocalhost?: boolean
}
const defaultProps = {
protocols: ['http', 'https', 'ftp'],
restrictedWhitespaceCharactersRegExp: /[\s\t\n]/,
requireProtocol: true,
strictProtocolSlashes: true,
allowTrailingDot: false,
allowZeroPort: false,
allowWhitespaceCharacters: false,
allowLocalhost: true,
}
export function isUrl(str: string, options?: IsUrlOptions) {
const {
protocols,
restrictedWhitespaceCharactersRegExp,
requireProtocol,
strictProtocolSlashes,
allowTrailingDot,
allowZeroPort,
allowWhitespaceCharacters,
allowLocalhost
} = { ...defaultProps, ...options }
if (typeof str !== 'string' || str.length > 2083) {
return false
}
if (/[<>]/.test(str)) {
return false
}
if (!allowWhitespaceCharacters && restrictedWhitespaceCharactersRegExp.test(str)) {
return false
}
if (!requireProtocol && !ProtocolRegExp.test(str)) {
str = `https://${str}`
}
if (strictProtocolSlashes && !StrictProtocolRegExp.test(str)) {
return false
}
let url: URL
try {
url = new URL(str)
} catch(e) {
return false
}
let { hostname, protocol, port } = url
if (!hostname) {
return false
}
protocol = protocol.slice(0, -1)
if (!allowZeroPort && port === '0') {
return false
}
if (!protocols.includes(protocol)) {
return false
}
const parsed = parse(str)
const hasLeadingDot = parsed.hostname.startsWith('.')
const hasTrailingDot = !allowTrailingDot && parsed.hostname.endsWith('.')
const hasColonButNoPort = parsed.hasPortColon && !parsed.port
const hasZeroPort = !allowZeroPort && parsed.port === '0'
if (isIPv4(hostname)) {
return !hasLeadingDot && !hasTrailingDot && !hasColonButNoPort && !hasZeroPort
}
if (isIPv6(hostname)) {
return !hasZeroPort && !hasColonButNoPort
}
const topLevelDomain = parsed.domains.at(-1);
return HostNameSymbolsRegExp.test(parsed.hostname)
&& !hasLeadingDot
&& !hasTrailingDot
&& !hasColonButNoPort
&& !hasZeroPort
&& (parsed.domains.length >= 2 || (hostname === 'localhost' && allowLocalhost))
&& !parsed.domains.some(
domain => domain.endsWith('-') || domain.startsWith('-')
)
&& topLevelDomain && topLevelDomain.length >= 2
&& (!parsed.hasUserAndPasswordColon || !!parsed.user || !!parsed.password)
&& !parsed.hasMultipleColonsInUserInfo
}
// Protocol must start with a letter and may contain letters, digits, "+", "-", or "."
const StrictProtocolRegExp = /^[a-z][a-z+.\d-]+:\/\/[^\/]+/i
const ProtocolRegExp = /^[a-z][a-z+\d.-]+:\/\//i
const HostNameSymbolsRegExp = /^[\p{L}\p{M}\p{N}.-]+$/u
// Use simple RegExps assuming that url was validated by new URL()
const IPv4RegExp = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
const isIPv4 = (hostname: string): boolean => IPv4RegExp.test(hostname)
const isIPv6 = (hostname: string): boolean => hostname.startsWith('[') && hostname.endsWith(']')
const Protocol = '[a-z][a-z+.\\d-]+:\\/+'
const UserAndPassword = '(?:([^@]+)@)?'
const HostRegExp = new RegExp(`(${Protocol})?${UserAndPassword}(?:www\\.)?([^/?#]+)`, 'i')
const PortRegExp = /:(\d+)?$/
type ExtractHostAndPortResult = {
user: string,
password: string,
hasUserAndPasswordColon: boolean,
hasMultipleColonsInUserInfo: boolean,
hostname: string,
port: string,
hasPortColon: boolean,
domains: string[]
}
const parse = (str: string): ExtractHostAndPortResult => {
const matched = str.match(HostRegExp)
if (!matched || !matched[3]) {
return { user: '', password: '', hasUserAndPasswordColon: false, hasMultipleColonsInUserInfo: false, hostname: '', port: '', hasPortColon: false, domains: [] }
}
const userAndPasswordStr = matched[2] ?? ''
const userAndPassword = userAndPasswordStr.split(':')
const hasUserAndPasswordColon = userAndPassword?.length > 1
const hasMultipleColonsInUserInfo = userAndPassword.length > 2
const user = userAndPassword[0] ?? ''
const password = userAndPassword[1] ?? ''
const host = matched[3] || ''
const hostname = host.replace(PortRegExp, '')
const portMatched = host.match(PortRegExp)
const hasPortColon = !!portMatched
const port = portMatched?.[1] ?? ''
const domains: string[] = hostname.split('.').filter(Boolean)
return { user, password, hasUserAndPasswordColon, hasMultipleColonsInUserInfo, hostname, port, hasPortColon, domains }
}Package Manager (not recommended)
# npm
npm i @beta3-z/no-url# pnpm
pnpm add @beta3-z/no-url# yarn
yarn add @beta3-z/no-url