All files / Bandstand/src/lib url.ts

89.36% Statements 84/94
65.95% Branches 31/47
100% Functions 3/3
89.36% Lines 84/94

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 1241x 1x                 1x 20x 20x   20x 17x   17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 17x 19x 3x 3x     14x 14x 19x 19x 11x 19x 20x 5x 18x     20x         1x 5x 5x 4x 5x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 5x     3x 5x 3x 5x 2x 5x 5x 1x 1x             5x     1x 2x 2x     2x 2x 2x 2x 2x   2x     2x 2x 2x 2x     2x 2x 2x     2x 2x 2x 2x 6x 6x 2x     2x 2x 2x  
import validator from 'validator'
import {parse as parseDomain} from 'tldts'
 
/**
 * Validate that a string is a well-formed HTTP(S) URL.
 * - Trims input
 * - Uses validator + URL parsing
 * - Ensures protocol is http or https
 * - Ensures a hostname is present and either localhost, IP, or ICANN domain
 */
export function isHttpUrl(value: string): boolean {
  const v = (value ?? '').trim()
  if (!v) return false
  // Require explicit authority form: scheme://
  if (!/^https?:\/\//i.test(v)) return false
  try {
    // First pass: general URL shape using validator
    if (
      !validator.isURL(v, {
        protocols: ['http', 'https'],
        require_protocol: true,
        require_valid_protocol: true,
        allow_underscores: false,
        allow_trailing_dot: false,
        allow_protocol_relative_urls: false,
        validate_length: true,
        require_tld: false, // we'll enforce TLD/localhost/IP via tldts
      })
    ) {
      return false
    }
 
    // Second pass: host must be localhost, IP, or ICANN domain with TLD
    const u = new URL(v)
    const host = u.hostname
    if (!host) return false
    if (host.toLowerCase() === 'localhost') return true
    const info = parseDomain(host)
    if (info.isIp) return true
    if (info.isIcann && info.domain && info.publicSuffix) return true
    return false
  } catch {
    return false
  }
}
 
/**
 * Gives a reason why a URL failed validation. Useful for fine-grained UX.
 */
export function httpUrlError(value: string): string | null {
  const v = (value ?? '').trim()
  if (!v) return 'Empty URL'
  try {
    if (!/^https?:\/\//i.test(v)) return 'Must start with http(s)://'
    if (
      !validator.isURL(v, {
        protocols: ['http', 'https'],
        require_protocol: true,
        require_valid_protocol: true,
        allow_underscores: false,
        allow_trailing_dot: false,
        allow_protocol_relative_urls: false,
        validate_length: true,
        require_tld: false,
      })
    ) {
      return 'Invalid URL'
    }
    const u = new URL(v)
    if (!u.hostname) return 'Missing host'
    const host = u.hostname
    if (host.toLowerCase() === 'localhost') return null
    const info = parseDomain(host)
    if (info.isIp) return null
    if (!(info.isIcann && info.domain && info.publicSuffix)) {
      if (!host.includes('.'))
        return 'Domain must contain a dot or be localhost'
      return 'Invalid domain/TLD'
    }
    return null
  } catch {
    return 'Invalid URL'
  }
}
 
// Generate helpful URL suggestions for a partially entered value.
export function suggestHttpUrls(input: string, limit = 5): string[] {
  const raw = (input ?? '').trim()
  if (!raw) return []
 
  // Separate host-ish part from path-ish part
  const hasScheme = /^https?:\/\//i.test(raw)
  const withoutScheme = hasScheme ? raw.replace(/^https?:\/\//i, '') : raw
  const slash = withoutScheme.indexOf('/')
  const hostPart = slash === -1 ? withoutScheme : withoutScheme.slice(0, slash)
  const pathPart = slash === -1 ? '' : withoutScheme.slice(slash)
 
  const candidates = new Set<string>()
 
  // Always suggest adding https:// and http:// when missing
  if (!hasScheme) {
    candidates.add(`https://${withoutScheme}`)
    candidates.add(`http://${withoutScheme}`)
  }
 
  // Suggest www. variant for bare domains
  if (hostPart && !/^www\./i.test(hostPart)) {
    candidates.add(`https://www.${hostPart}${pathPart}`)
  }
 
  // If single-label host (not localhost), suggest common TLDs
  const info = parseDomain(hostPart)
  const isSingleLabel = hostPart && !hostPart.includes('.')
  if (isSingleLabel && hostPart.toLowerCase() !== 'localhost' && !info.isIp) {
    for (const tld of ['.com', '.org', '.net']) {
      candidates.add(`https://${hostPart}${tld}${pathPart}`)
    }
  }
 
  // Filter by our validator and de-dup
  const valid = Array.from(candidates).filter((c) => isHttpUrl(c))
  return valid.slice(0, limit)
}