javascript

exercises

exercises.js
/**
 * ========================================
 * 11.4 URL and History APIs - Exercises
 * ========================================
 *
 * Practice URL parsing, manipulation, and history management.
 * Complete each exercise by filling in the code.
 */

/**
 * EXERCISE 1: URL Parser
 *
 * Parse a URL string into its components.
 */

function parseURL(urlString) {
  // Return object with:
  // protocol, host, pathname, search, hash, origin
  // Handle invalid URLs by returning null
}

// Test:
// parseURL('https://example.com:8080/path?q=1#sec')
// → { protocol: 'https:', host: 'example.com:8080', ... }

/**
 * EXERCISE 2: Query String Builder
 *
 * Build a query string from an object.
 */

function buildQueryString(params) {
  // Convert object to query string
  // Handle arrays as multiple values with same key
  // Skip null/undefined values
}

// Test:
// buildQueryString({ name: 'John', tags: ['a', 'b'], empty: null })
// → 'name=John&tags=a&tags=b'

/**
 * EXERCISE 3: Query String Parser
 *
 * Parse a query string into an object.
 */

function parseQueryString(queryString) {
  // Parse query string to object
  // Handle multiple values for same key as array
  // Decode URI components
}

// Test:
// parseQueryString('?name=John%20Doe&tag=js&tag=web')
// → { name: 'John Doe', tag: ['js', 'web'] }

/**
 * EXERCISE 4: URL Builder
 *
 * Build a complete URL from parts.
 */

function buildURL(base, path, params = {}) {
  // Combine base URL, path, and query params
  // Handle trailing/leading slashes properly
}

// Test:
// buildURL('https://api.example.com/', '/users/', { page: 1 })
// → 'https://api.example.com/users/?page=1'

/**
 * EXERCISE 5: URL Validator
 *
 * Validate URLs with specific requirements.
 */

function validateURL(urlString, options = {}) {
  // options: { protocols: [], requirePath: bool, requireQuery: bool }
  // Return { valid: boolean, error?: string }
}

// Test:
// validateURL('ftp://example.com', { protocols: ['http:', 'https:'] })
// → { valid: false, error: 'Invalid protocol' }

/**
 * EXERCISE 6: Relative URL Resolver
 *
 * Resolve relative URLs against a base URL.
 */

function resolveURL(baseURL, relativeURL) {
  // Resolve relative URL to absolute
  // Handle various relative formats: /path, ./path, ../path, //host/path
}

// Test:
// resolveURL('https://example.com/dir/page.html', '../other.html')
// → 'https://example.com/other.html'

/**
 * EXERCISE 7: URL Comparison
 *
 * Compare two URLs for equality.
 */

function compareURLs(url1, url2, options = {}) {
  // options: { ignoreCase, ignoreTrailingSlash, ignoreHash, ignoreQuery }
  // Return boolean
}

// Test:
// compareURLs(
//     'https://EXAMPLE.com/path/',
//     'https://example.com/path',
//     { ignoreCase: true, ignoreTrailingSlash: true }
// ) → true

/**
 * EXERCISE 8: History Navigator
 *
 * Create a history wrapper with enhanced functionality.
 */

class HistoryNavigator {
  constructor() {
    // Initialize
  }

  push(path, state = {}) {
    // Navigate to new path, add to history
  }

  replace(path, state = {}) {
    // Replace current history entry
  }

  back() {
    // Go back one step
  }

  forward() {
    // Go forward one step
  }

  getState() {
    // Get current state
  }

  onChange(callback) {
    // Subscribe to history changes
    // Return unsubscribe function
  }
}

/**
 * EXERCISE 9: Route Matcher
 *
 * Match URLs against route patterns.
 */

function matchRoute(pattern, pathname) {
  // Match pathname against pattern with params
  // Pattern: /users/:id/posts/:postId
  // Return: { matched: boolean, params: {} }
}

// Test:
// matchRoute('/users/:id/posts/:postId', '/users/123/posts/456')
// → { matched: true, params: { id: '123', postId: '456' } }

/**
 * EXERCISE 10: Query State Manager
 *
 * Sync state with URL query parameters.
 */

class QueryStateManager {
  constructor(defaults = {}) {
    // Initialize with default values
  }

  get(key) {
    // Get current value from URL
  }

  set(key, value) {
    // Update URL with new value
  }

  getAll() {
    // Get all state as object
  }

  setAll(state) {
    // Set multiple values at once
  }

  reset() {
    // Reset to defaults
  }
}

/**
 * EXERCISE 11: Hash Router
 *
 * Simple hash-based router.
 */

class HashRouter {
  constructor() {
    // Initialize
  }

  addRoute(path, handler) {
    // Register a route handler
  }

  navigate(path) {
    // Navigate to path (updates hash)
  }

  getCurrentPath() {
    // Get current hash path
  }

  start() {
    // Start listening for hash changes
  }

  stop() {
    // Stop listening
  }
}

/**
 * EXERCISE 12: Path Normalizer
 *
 * Normalize and clean up paths.
 */

function normalizePath(path) {
  // Remove duplicate slashes
  // Resolve . and ..
  // Ensure leading slash
  // Handle trailing slash consistently
}

// Test:
// normalizePath('/api//v1/../v2/./users/')
// → '/api/v2/users'

/**
 * EXERCISE 13: URL Template
 *
 * Expand URL templates with values.
 */

function expandURLTemplate(template, values) {
  // Replace :param with values
  // Replace {param} with values
  // Handle missing values gracefully
}

// Test:
// expandURLTemplate('/users/:userId/posts/{postId}', { userId: 123, postId: 456 })
// → '/users/123/posts/456'

/**
 * EXERCISE 14: Breadcrumb Generator
 *
 * Generate breadcrumbs from URL path.
 */

function generateBreadcrumbs(pathname, names = {}) {
  // Generate array of { path, label } from pathname
  // Use names object for custom labels
}

// Test:
// generateBreadcrumbs('/products/electronics/phones', {
//     products: 'All Products',
//     electronics: 'Electronics'
// })
// → [
//     { path: '/', label: 'Home' },
//     { path: '/products', label: 'All Products' },
//     { path: '/products/electronics', label: 'Electronics' },
//     { path: '/products/electronics/phones', label: 'Phones' }
// ]

/**
 * EXERCISE 15: Deep Link Handler
 *
 * Handle app deep links.
 */

class DeepLinkHandler {
  constructor() {
    // Initialize
  }

  register(pattern, handler) {
    // Register handler for pattern
  }

  handle(url) {
    // Match URL and call appropriate handler
    // Return true if handled, false otherwise
  }

  generateLink(pattern, params, query = {}) {
    // Generate URL from pattern and params
  }
}

// ============================================
// SOLUTIONS (Hidden - Scroll to reveal)
// ============================================

/*
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 * SOLUTIONS BELOW
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

// SOLUTION 1: URL Parser
function parseURLSolution(urlString) {
  try {
    const url = new URL(urlString);
    return {
      protocol: url.protocol,
      host: url.host,
      hostname: url.hostname,
      port: url.port,
      pathname: url.pathname,
      search: url.search,
      hash: url.hash,
      origin: url.origin,
    };
  } catch {
    return null;
  }
}

// SOLUTION 2: Query String Builder
function buildQueryStringSolution(params) {
  const parts = [];

  for (const [key, value] of Object.entries(params)) {
    if (value === null || value === undefined) continue;

    if (Array.isArray(value)) {
      value.forEach((v) => {
        parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
      });
    } else {
      parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
    }
  }

  return parts.join('&');
}

// SOLUTION 3: Query String Parser
function parseQueryStringSolution(queryString) {
  const params = new URLSearchParams(queryString);
  const result = {};

  for (const [key, value] of params.entries()) {
    const decoded = decodeURIComponent(value);
    if (result.hasOwnProperty(key)) {
      result[key] = Array.isArray(result[key])
        ? [...result[key], decoded]
        : [result[key], decoded];
    } else {
      result[key] = decoded;
    }
  }

  return result;
}

// SOLUTION 4: URL Builder
function buildURLSolution(base, path, params = {}) {
  // Ensure base ends without slash
  const cleanBase = base.replace(/\/+$/, '');
  // Ensure path starts with slash
  const cleanPath = path.startsWith('/') ? path : '/' + path;

  const url = new URL(cleanBase + cleanPath);

  Object.entries(params).forEach(([key, value]) => {
    if (value !== null && value !== undefined) {
      url.searchParams.set(key, value);
    }
  });

  return url.toString();
}

// SOLUTION 5: URL Validator
function validateURLSolution(urlString, options = {}) {
  const { protocols = [], requirePath = false, requireQuery = false } = options;

  try {
    const url = new URL(urlString);

    if (protocols.length > 0 && !protocols.includes(url.protocol)) {
      return { valid: false, error: 'Invalid protocol' };
    }

    if (requirePath && url.pathname === '/') {
      return { valid: false, error: 'Path required' };
    }

    if (requireQuery && !url.search) {
      return { valid: false, error: 'Query string required' };
    }

    return { valid: true };
  } catch {
    return { valid: false, error: 'Invalid URL format' };
  }
}

// SOLUTION 6: Relative URL Resolver
function resolveURLSolution(baseURL, relativeURL) {
  try {
    return new URL(relativeURL, baseURL).toString();
  } catch {
    return null;
  }
}

// SOLUTION 7: URL Comparison
function compareURLsSolution(url1, url2, options = {}) {
  const {
    ignoreCase = false,
    ignoreTrailingSlash = false,
    ignoreHash = false,
    ignoreQuery = false,
  } = options;

  try {
    const a = new URL(url1);
    const b = new URL(url2);

    let pathA = a.pathname;
    let pathB = b.pathname;

    if (ignoreCase) {
      pathA = pathA.toLowerCase();
      pathB = pathB.toLowerCase();
      a.hostname = a.hostname.toLowerCase();
      b.hostname = b.hostname.toLowerCase();
    }

    if (ignoreTrailingSlash) {
      pathA = pathA.replace(/\/+$/, '');
      pathB = pathB.replace(/\/+$/, '');
    }

    if (a.protocol !== b.protocol) return false;
    if (a.hostname !== b.hostname) return false;
    if (a.port !== b.port) return false;
    if (pathA !== pathB) return false;
    if (!ignoreQuery && a.search !== b.search) return false;
    if (!ignoreHash && a.hash !== b.hash) return false;

    return true;
  } catch {
    return false;
  }
}

// SOLUTION 8: History Navigator
class HistoryNavigatorSolution {
  constructor() {
    this.listeners = [];
    this.boundHandler = this.handlePopstate.bind(this);
    window.addEventListener('popstate', this.boundHandler);
  }

  handlePopstate(event) {
    this.listeners.forEach((callback) => callback(event.state, location.href));
  }

  push(path, state = {}) {
    history.pushState(state, '', path);
    this.listeners.forEach((cb) => cb(state, path));
  }

  replace(path, state = {}) {
    history.replaceState(state, '', path);
  }

  back() {
    history.back();
  }

  forward() {
    history.forward();
  }

  getState() {
    return history.state;
  }

  onChange(callback) {
    this.listeners.push(callback);
    return () => {
      this.listeners = this.listeners.filter((cb) => cb !== callback);
    };
  }
}

// SOLUTION 9: Route Matcher
function matchRouteSolution(pattern, pathname) {
  const paramNames = [];
  const regexPattern = pattern.replace(/:([^/]+)/g, (_, name) => {
    paramNames.push(name);
    return '([^/]+)';
  });

  const regex = new RegExp(`^${regexPattern}$`);
  const match = pathname.match(regex);

  if (!match) {
    return { matched: false, params: {} };
  }

  const params = {};
  paramNames.forEach((name, index) => {
    params[name] = match[index + 1];
  });

  return { matched: true, params };
}

// SOLUTION 10: Query State Manager
class QueryStateManagerSolution {
  constructor(defaults = {}) {
    this.defaults = defaults;
  }

  get(key) {
    const params = new URLSearchParams(location.search);
    const value = params.get(key);
    return value ?? this.defaults[key];
  }

  set(key, value) {
    const params = new URLSearchParams(location.search);

    if (value === this.defaults[key] || value === null || value === undefined) {
      params.delete(key);
    } else {
      params.set(key, value);
    }

    const search = params.toString();
    const url = search ? `${location.pathname}?${search}` : location.pathname;
    history.pushState(null, '', url);
  }

  getAll() {
    const result = { ...this.defaults };
    const params = new URLSearchParams(location.search);

    for (const [key, value] of params.entries()) {
      result[key] = value;
    }

    return result;
  }

  setAll(state) {
    const params = new URLSearchParams();

    for (const [key, value] of Object.entries(state)) {
      if (
        value !== this.defaults[key] &&
        value !== null &&
        value !== undefined
      ) {
        params.set(key, value);
      }
    }

    const search = params.toString();
    const url = search ? `${location.pathname}?${search}` : location.pathname;
    history.pushState(null, '', url);
  }

  reset() {
    history.pushState(null, '', location.pathname);
  }
}

// SOLUTION 11: Hash Router
class HashRouterSolution {
  constructor() {
    this.routes = new Map();
    this.boundHandler = this.handleHashChange.bind(this);
  }

  addRoute(path, handler) {
    this.routes.set(path, handler);
  }

  navigate(path) {
    location.hash = path;
  }

  getCurrentPath() {
    return location.hash.slice(1) || '/';
  }

  handleHashChange() {
    const path = this.getCurrentPath();
    const handler = this.routes.get(path) || this.routes.get('*');
    if (handler) handler();
  }

  start() {
    window.addEventListener('hashchange', this.boundHandler);
    this.handleHashChange();
  }

  stop() {
    window.removeEventListener('hashchange', this.boundHandler);
  }
}

// SOLUTION 12: Path Normalizer
function normalizePathSolution(path) {
  // Split into parts
  const parts = path.split('/').filter(Boolean);
  const result = [];

  for (const part of parts) {
    if (part === '..') {
      result.pop();
    } else if (part !== '.') {
      result.push(part);
    }
  }

  return '/' + result.join('/');
}

// SOLUTION 13: URL Template
function expandURLTemplateSolution(template, values) {
  let result = template;

  // Replace :param style
  result = result.replace(/:(\w+)/g, (_, key) => {
    return values[key] !== undefined ? values[key] : `:${key}`;
  });

  // Replace {param} style
  result = result.replace(/\{(\w+)\}/g, (_, key) => {
    return values[key] !== undefined ? values[key] : `{${key}}`;
  });

  return result;
}

// SOLUTION 14: Breadcrumb Generator
function generateBreadcrumbsSolution(pathname, names = {}) {
  const parts = pathname.split('/').filter(Boolean);
  const breadcrumbs = [{ path: '/', label: 'Home' }];

  let currentPath = '';
  for (const part of parts) {
    currentPath += '/' + part;
    const label = names[part] || part.charAt(0).toUpperCase() + part.slice(1);
    breadcrumbs.push({ path: currentPath, label });
  }

  return breadcrumbs;
}

// SOLUTION 15: Deep Link Handler
class DeepLinkHandlerSolution {
  constructor() {
    this.routes = [];
  }

  register(pattern, handler) {
    this.routes.push({ pattern, handler });
  }

  handle(urlString) {
    const url = new URL(urlString, location.origin);
    const pathname = url.pathname;
    const query = Object.fromEntries(url.searchParams);

    for (const { pattern, handler } of this.routes) {
      const match = matchRouteSolution(pattern, pathname);
      if (match.matched) {
        handler({ ...match.params, query, hash: url.hash.slice(1) });
        return true;
      }
    }

    return false;
  }

  generateLink(pattern, params, query = {}) {
    let url = expandURLTemplateSolution(pattern, params);

    const queryString = buildQueryStringSolution(query);
    if (queryString) {
      url += '?' + queryString;
    }

    return url;
  }
}

console.log('URL and History API exercises loaded!');
console.log(
  'Complete each exercise and check against solutions at the bottom.'
);
Exercises - JavaScript Tutorial | DeepML