javascript
exercises
exercises.js⚡javascript
/**
* ========================================
* 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.'
);