diff options
author | elioat <elioat@tilde.institute> | 2025-03-15 21:32:47 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2025-03-15 21:32:47 -0400 |
commit | 08bc2eccd539f1bd30d9807e76de79fd30b389c0 (patch) | |
tree | 5889390264a295673cb8f9a79ac0ab4949969f18 | |
parent | 048d5be07a287e7ea0650264eb28908e15993dda (diff) | |
download | tour-08bc2eccd539f1bd30d9807e76de79fd30b389c0.tar.gz |
*
-rw-r--r-- | html/immoral/app.js | 730 | ||||
-rw-r--r-- | html/immoral/index.html | 167 |
2 files changed, 897 insertions, 0 deletions
diff --git a/html/immoral/app.js b/html/immoral/app.js new file mode 100644 index 0000000..f90a6f3 --- /dev/null +++ b/html/immoral/app.js @@ -0,0 +1,730 @@ +/** + * An Immoral Web Font Vacuum + * A tool to find and extract web fonts from any website + * + * We sort of set up a pipeline where each step processes the data and passes it to the next: + * 1. URL Input > 2. Fetch HTML > 3. Parse & Extract > 4. Process CSS > 5. Display Results + * + */ + +/** + * Proxy List + * Proxies are buggy, and temperamental...so why not use a whole lot of them! + * List of CORS proxies we can try if the main one fails. + * Keeps track of the last working proxy to optimize future requests. + * @type {Array<{name: string, url: string, urlFormatter: (url: string) => string}>} + */ +const CORS_PROXIES = [ + { + name: 'allorigins', + url: 'https://api.allorigins.win/raw?url=', + urlFormatter: url => `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}` + }, + { + name: 'corsproxy.io', + url: 'https://corsproxy.io/?', + urlFormatter: url => `https://corsproxy.io/?${encodeURIComponent(url)}` + }, + { + name: 'cors.sh', + url: 'https://cors.sh/', + urlFormatter: url => `https://cors.sh/${url}` + }, + { + name: 'corsanywhere', + url: 'https://cors-anywhere.herokuapp.com/', // FIXME: pretty certain this one doesn't work without human intervention + urlFormatter: url => `https://cors-anywhere.herokuapp.com/${url}` + }, + { + name: 'thingproxy', + url: 'https://thingproxy.freeboard.io/fetch/', + urlFormatter: url => `https://thingproxy.freeboard.io/fetch/${url}` + } +]; + +// Keep track of which proxy worked last +let lastWorkingProxyIndex = 0; +let proxyFailureCount = 0; +const MAX_PROXY_FAILURES = 3; + +async function fetchWithProxies(url, attempt = 0, isBinary = false) { + // Start with the last working proxy + const startIndex = lastWorkingProxyIndex; + + for (let i = 0; i < CORS_PROXIES.length; i++) { + // Calculate the current proxy index, wrapping around if necessary + const proxyIndex = (startIndex + i) % CORS_PROXIES.length; + const proxy = CORS_PROXIES[proxyIndex]; + + try { + console.log(`Trying proxy: ${proxy.name} for URL: ${url}`); + + const fetchOptions = { + headers: { + 'Accept': isBinary ? '*/*' : 'text/html,application/xhtml+xml,text/css', + 'Origin': window.location.origin + }, + mode: 'cors' + }; + + const response = await fetch(proxy.urlFormatter(url), fetchOptions); + + if (response.ok) { + lastWorkingProxyIndex = proxyIndex; + proxyFailureCount = 0; + return response; + } + } catch (error) { + console.log(`Proxy ${proxy.name} failed:`, error); + proxyFailureCount++; + + // If we've had too many failures, wait a bit before continuing + if (proxyFailureCount >= MAX_PROXY_FAILURES) { + await new Promise(resolve => setTimeout(resolve, 1000)); + proxyFailureCount = 0; + } + } + } + + throw new Error('All proxies failed to fetch the resource'); +} + +async function downloadFont(url, filename) { + try { + console.log('Downloading font from:', url); + const response = await fetchWithProxies(url, 0, true); + + const arrayBuffer = await response.arrayBuffer(); + if (arrayBuffer.byteLength === 0) { + throw new Error('Received empty font file'); + } + + // Convert ArrayBuffer to Blob with proper MIME type + const blob = new Blob([arrayBuffer], { type: getFontMimeType(url) }); + + // Temporary link to trigger the download + const objectUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = objectUrl; + link.download = filename; + + document.body.appendChild(link); + link.click(); + + // Small delay to ensure download starts before cleanup + setTimeout(() => { + document.body.removeChild(link); + URL.revokeObjectURL(objectUrl); + }, 100); + + return true; + } catch (error) { + console.error('Error downloading font:', error); + alert(`Error downloading font: ${error.message}`); + return false; + } +} + +// Assume the MIME type based on the file's extension +function getFontMimeType(url) { + const extension = url.split('.').pop().toLowerCase().split('?')[0]; + switch (extension) { + case 'woff': + return 'font/woff'; + case 'woff2': + return 'font/woff2'; + case 'ttf': + return 'font/ttf'; + case 'otf': + return 'font/otf'; + default: + return 'application/octet-stream'; + } +} + +/** + * Sets up event listeners and initializes the app. + * + * 1. User enters URL and clicks Analyze + * 2. Fetch the HTML through a CORS proxy + * 3. Parse HTML to find: + * - Direct font links + * - Stylesheet links + * - Inline styles + * 4. Process each CSS source to find @font-face rules + * 5. Display results with preview/download options + */ +document.addEventListener('DOMContentLoaded', () => { + const urlInput = document.getElementById('urlInput'); + const analyzeBtn = document.getElementById('analyzeBtn'); + const resultsDiv = document.getElementById('results'); + const errorDiv = document.getElementById('error'); + + /** + * Two different methods to load the font: + * - Using a data URL + * - Fallback: Using a blob URL if data URL fails + * + * @param {string} url - The URL of the font file to preview + * @param {string} fontFamily - The font-family name to use + * @returns {Promise<boolean>} - Whether the font was successfully loaded + */ + async function previewFont(url, fontFamily) { + try { + console.log('Loading font from:', url); + const response = await fetchWithProxies(url, 0, true); + + const arrayBuffer = await response.arrayBuffer(); + if (arrayBuffer.byteLength === 0) { + throw new Error('Received empty font file'); + } + + // Convert ArrayBuffer to Blob + const blob = new Blob([arrayBuffer], { type: getFontMimeType(url) }); + + // Try using a data URL instead of a blob URL + const reader = new FileReader(); + + return new Promise((resolve, reject) => { + reader.onload = async function() { + try { + const dataUrl = reader.result; + + const fontFace = new FontFace(fontFamily, `url(${dataUrl})`, { + style: 'normal', + weight: '400', + display: 'swap' + }); + + const loadedFont = await fontFace.load(); + document.fonts.add(loadedFont); + + resolve(true); + } catch (loadError) { + console.error('Font load error:', loadError); + + // Try fallback method with blob URL + try { + const fontUrl = URL.createObjectURL(blob); + const fontFace = new FontFace(fontFamily, `url(${fontUrl})`, { + style: 'normal', + weight: '400', + display: 'swap' + }); + + const loadedFont = await fontFace.load(); + document.fonts.add(loadedFont); + + URL.revokeObjectURL(fontUrl); + resolve(true); + } catch (fallbackError) { + console.error('Font fallback load error:', fallbackError); + reject(fallbackError); + } + } + }; + + reader.onerror = function() { + reject(new Error('Failed to read font file')); + }; + + reader.readAsDataURL(blob); + }); + } catch (error) { + console.error('Error loading font for preview:', error); + return false; + } + } + + /** + * Click handler for the Analyze button. + * + * 1. Validate input URL + * 2. Fetch and parse webpage + * 3. Look for fonts in: + * - Direct links (preload, regular links) + * - CSS files (external and inline) + * - Common font directories + * 4. Display results + */ + analyzeBtn.addEventListener('click', async () => { + const url = urlInput.value.trim(); + if (!url) { + showError('Please enter a valid URL'); + return; + } + + try { + // Clear previous results and errors + resultsDiv.innerHTML = ''; + errorDiv.style.display = 'none'; + + // Show loading state + resultsDiv.innerHTML = '<p>Analyzing webpage. Sometimes this takes a while.</p>'; + + // Fetch the target webpage through the proxy system + const response = await fetchWithProxies(url); + const html = await response.text(); + + // Create a temporary DOM to parse the HTML + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + // Find all potential font sources + const fontUrls = new Set(); + + // Get the base URL (domain) + const baseUrlObj = new URL(url); + const domain = baseUrlObj.origin; + + // Brute force common font paths, for scenarios where the font is not found in the css + const commonFontPaths = [ + '/assets/fonts/', + '/fonts/', + '/assets/', + '/css/fonts/', + '/wp-content/themes/*/fonts/', + '/static/fonts/' + ]; + + console.log('Checking common font paths for:', domain); + + // Check for direct font links in the HTML + extractDirectFontLinks(doc, url, fontUrls); + + // Check stylesheet links + const cssPromises = Array.from(doc.querySelectorAll('link[rel="stylesheet"]')).map(link => { + const href = link.getAttribute('href'); + if (!href) return Promise.resolve(); + + const cssUrl = new URL(href, url).href; + console.log('Processing CSS URL:', cssUrl); + return processCssUrl(cssUrl, fontUrls, url); + }); + + // Wait for all CSS files to be processed + await Promise.all(cssPromises.filter(Boolean)); + + // Check style tags + doc.querySelectorAll('style').forEach(style => { + extractFontUrlsFromCss(style.textContent, url, fontUrls); + }); + + // If no fonts found, try checking common paths + if (fontUrls.size === 0) { + console.log('No fonts found in CSS, checking common paths...'); + + const commonPathPromises = commonFontPaths.map(async path => { + try { + // Try to access the directory + const directoryUrl = new URL(path, domain).href; + console.log('Checking directory:', directoryUrl); + + // We can't list directory contents directly, but we can try common font names + const commonFontNames = ['font', 'fonts', 'webfont', 'custom-font', 'main']; + const fontExtensions = ['woff', 'woff2', 'ttf', 'otf']; + + for (const name of commonFontNames) { + for (const ext of fontExtensions) { + const fontUrl = `${directoryUrl}${name}.${ext}`; + try { + const fontResponse = await fetchWithProxies(fontUrl, 0, true); + if (fontResponse.ok) { + console.log('Found font at common path:', fontUrl); + fontUrls.add({ + url: fontUrl, + family: 'Unknown Font', + filename: `${name}.${ext}` + }); + } + } catch (error) { + // Ignore errors for common path checks + } + } + } + } catch (error) { + // Ignore errors for common path checks + } + }); + + await Promise.all(commonPathPromises); + } + + // Display results + if (fontUrls.size === 0) { + resultsDiv.innerHTML = '<p>No web fonts (WOFF/TTF/WOFF2/OTF) were found on this page.</p>'; + } else { + displayFontUrls(fontUrls); + } + + } catch (error) { + showError(`Error analyzing the webpage: ${error.message}`); + console.error('Full error:', error); + } + }); + + /** + * Processes a the URL of a CSS file to extract font information. + * + * @param {string} cssUrl - The URL of the CSS file to process + * @param {Set} fontUrls - Set to store found font URLs + * @param {string} baseUrl - Base URL for resolving relative paths + */ + async function processCssUrl(cssUrl, fontUrls, baseUrl) { + try { + console.log('Fetching CSS from:', cssUrl); + const response = await fetchWithProxies(cssUrl); + const css = await response.text(); + + // Extract font URLs from the CSS content + extractFontUrlsFromCss(css, cssUrl, fontUrls); + } catch (error) { + console.error(`Error processing CSS from ${cssUrl}:`, error); + } + } + + /** + * Extract font URLs from CSS content + * + * @param {string} css - The CSS content to process + * @param {string} cssUrl - The URL of the CSS file (for resolving relative paths) + * @param {Set} fontUrls - Set to store found font URLs + */ + function extractFontUrlsFromCss(css, cssUrl, fontUrls) { + // Get the base URL for resolving relative paths + const baseUrl = new URL(cssUrl).origin; + + // Match @font-face blocks + const fontFaceRegex = /@font-face\s*{[^}]*}/g; + const urlRegex = /url\(['"]?([^'"\)]+)['"]?\)/g; + const fontFamilyRegex = /font-family\s*:\s*['"]?([^'";]*)['"]?/; + + let fontFaceMatch; + while ((fontFaceMatch = fontFaceRegex.exec(css)) !== null) { + const fontFaceBlock = fontFaceMatch[0]; + + // Extract font-family name + const familyMatch = fontFaceBlock.match(fontFamilyRegex); + const fontFamily = familyMatch ? familyMatch[1].trim() : 'Unknown Font'; + + // Clean up the CSS rule for display + const cleanRule = fontFaceBlock.replace(/\s+/g, ' ').trim(); + + // Extract all URLs from this @font-face block + let urlMatch; + while ((urlMatch = urlRegex.exec(fontFaceBlock)) !== null) { + try { + let fontUrl = urlMatch[1].trim(); + + // Skip data: URLs + if (fontUrl.startsWith('data:')) { + console.log('Skipping data: URL font'); + continue; + } + + // Only process known font file types + if (!fontUrl.match(/\.(woff2?|ttf|otf|eot)(\?.*)?$/i)) { + continue; + } + + // Resolve relative URLs + if (fontUrl.startsWith('//')) { + fontUrl = 'https:' + fontUrl; + } else if (!fontUrl.startsWith('http')) { + fontUrl = new URL(fontUrl, cssUrl).href; + } + + const filename = fontUrl.split('/').pop().split('?')[0]; + console.log(`Found font in CSS: ${fontUrl} (${fontFamily})`); + + fontUrls.add({ + url: fontUrl, + family: fontFamily, + filename: filename, + cssRule: cleanRule + }); + } catch (error) { + console.error('Error processing font URL:', urlMatch[1], error); + } + } + } + } + + /** + * Find direct font links in HTML. + * Checks two types of links: + * 1. Preload links with as="font" + * 2. Regular <a> tags pointing to font files + * + * @param {Document} doc - Parsed HTML document + * @param {string} baseUrl - Base URL for resolving relative paths + * @param {Set} fontUrls - Set to store found font URLs + */ + function extractDirectFontLinks(doc, baseUrl, fontUrls) { + // Check for preload links + doc.querySelectorAll('link[rel="preload"][as="font"], link[rel="stylesheet"]').forEach(link => { + const href = link.getAttribute('href'); + if (href && href.match(/\.(woff|woff2|ttf|otf|css)(\?.*)?$/i)) { + try { + const absoluteUrl = new URL(href, baseUrl).href; + if (href.match(/\.css(\?.*)?$/i)) { + processCssUrl(absoluteUrl, fontUrls, baseUrl); + } else { + const filename = href.split('/').pop().split('?')[0]; + let fontFamilyName = 'Unknown Font'; + if (link.dataset.fontFamily) { + fontFamilyName = link.dataset.fontFamily; + } + + console.log(`Found preloaded font: ${absoluteUrl} (${fontFamilyName})`); + fontUrls.add({ + url: absoluteUrl, + family: fontFamilyName, + filename: filename, + cssRule: `@font-face { font-family: "${fontFamilyName}"; src: url("${absoluteUrl}") format("${getFormatFromFilename(filename)}"); }` + }); + } + } catch (error) { + console.error('Error resolving font URL:', href, error); + } + } + }); + } + + /** + * Get the format string for a font file based on its filename + * @param {string} filename + * @returns {string} + */ + function getFormatFromFilename(filename) { + const ext = filename.split('.').pop().toLowerCase(); + switch (ext) { + case 'woff2': + return 'woff2'; + case 'woff': + return 'woff'; + case 'ttf': + return 'truetype'; + case 'otf': + return 'opentype'; + default: + return ext; + } + } + + /** + * The V of MVC + * + * - Auto-preview for 3 or fewer fonts + * - Manual preview toggle for 4+ fonts + * - Download buttons + * - Font information display + * - CSS rule display + * - Preview with multiple sizes + * + * @param {Array} urls - Array of font data objects to display + */ + async function displayFontUrls(urls) { + const resultsDiv = document.getElementById('results'); + resultsDiv.innerHTML = ''; + + // Convert urls to array if it's a Set, or use as is if already array + const fontsArray = Array.isArray(urls) ? urls : Array.from(urls); + + if (fontsArray.length === 0) { + resultsDiv.innerHTML = '<p>No fonts found on this webpage.</p>'; + return; + } + + const container = document.createElement('div'); + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.gap = '2rem'; + container.style.maxWidth = '800px'; + container.style.margin = '0 auto'; + + const shouldAutoPreview = fontsArray.length < 4; + + for (let fontData of fontsArray) { + const fontItem = document.createElement('div'); + fontItem.style.border = '2px solid var(--dark)'; + fontItem.style.padding = '1rem'; + fontItem.style.background = 'var(--beige)'; + fontItem.style.position = 'relative'; + + const accentBar = document.createElement('div'); + accentBar.style.position = 'absolute'; + accentBar.style.top = '0'; + accentBar.style.left = '0'; + accentBar.style.right = '0'; + accentBar.style.height = '4px'; + accentBar.style.background = 'var(--accent)'; + fontItem.appendChild(accentBar); + + const fontInfo = document.createElement('div'); + fontInfo.style.marginTop = '0.5rem'; + + const fontName = document.createElement('h3'); + fontName.style.margin = '0'; + fontName.style.textTransform = 'uppercase'; + fontName.textContent = fontData.family; + fontInfo.appendChild(fontName); + + const fontType = document.createElement('div'); + fontType.style.display = 'inline-block'; + fontType.style.background = 'var(--dark)'; + fontType.style.color = 'var(--beige)'; + fontType.style.padding = '0.2rem 0.5rem'; + fontType.style.marginTop = '0.5rem'; + fontType.style.fontSize = '0.8rem'; + fontType.textContent = fontData.filename.split('.').pop().toUpperCase(); + fontInfo.appendChild(fontType); + + const previewContainer = document.createElement('div'); + previewContainer.style.marginTop = '1rem'; + previewContainer.style.padding = '1rem'; + previewContainer.style.border = '1px dashed var(--dark)'; + + // Hide preview container initially if not auto-previewing + if (!shouldAutoPreview) { + previewContainer.style.display = 'none'; + } + + const previewLabel = document.createElement('div'); + previewLabel.style.fontWeight = 'bold'; + previewLabel.style.marginBottom = '0.5rem'; + previewLabel.textContent = 'Preview:'; + previewContainer.appendChild(previewLabel); + + const preview = document.createElement('div'); + preview.style.marginBottom = '1rem'; + preview.id = `preview-${fontData.filename}`; + preview.textContent = 'The quick brown fox jumps over the lazy dog 0123456789'; + previewContainer.appendChild(preview); + + // Add CSS Rule section + if (fontData.cssRule) { + const cssContainer = document.createElement('div'); + cssContainer.style.marginTop = '1rem'; + cssContainer.style.marginBottom = '1rem'; + cssContainer.style.padding = '1rem'; + cssContainer.style.background = 'var(--dark)'; + cssContainer.style.color = 'var(--beige)'; + cssContainer.style.borderRadius = '4px'; + cssContainer.style.position = 'relative'; + + const cssLabel = document.createElement('div'); + cssLabel.style.position = 'absolute'; + cssLabel.style.top = '-10px'; + cssLabel.style.left = '10px'; + cssLabel.style.background = 'var(--accent)'; + cssLabel.style.color = 'var(--dark)'; + cssLabel.style.padding = '0 0.5rem'; + cssLabel.style.fontSize = '0.8rem'; + cssLabel.style.fontWeight = 'bold'; + cssLabel.textContent = '@font-face'; + cssContainer.appendChild(cssLabel); + + const cssContent = document.createElement('pre'); + cssContent.style.margin = '0'; + cssContent.style.fontFamily = 'monospace'; + cssContent.style.fontSize = '0.9rem'; + cssContent.style.whiteSpace = 'pre-wrap'; + cssContent.style.wordBreak = 'break-all'; + + // Format the CSS rule nicely + const formattedCss = fontData.cssRule + .replace(/{/, ' {\n ') + .replace(/;/g, ';\n ') + .replace(/}/g, '\n}') + .replace(/\s+}/g, '}') + .trim(); + + cssContent.textContent = formattedCss; + cssContainer.appendChild(cssContent); + + fontInfo.appendChild(cssContainer); + } + + const sizeVariations = document.createElement('div'); + sizeVariations.style.borderTop = '1px solid var(--dark)'; + sizeVariations.style.paddingTop = '0.5rem'; + sizeVariations.style.marginTop = '0.5rem'; + + [12, 18, 24].forEach(size => { + const sizePreview = document.createElement('div'); + sizePreview.style.fontSize = `${size}px`; + sizePreview.textContent = `${size}px - The quick brown fox jumps over the lazy dog 0123456789`; + sizeVariations.appendChild(sizePreview); + }); + + previewContainer.appendChild(sizeVariations); + + const buttonContainer = document.createElement('div'); + buttonContainer.style.display = 'flex'; + buttonContainer.style.gap = '0.5rem'; + buttonContainer.style.marginTop = '1rem'; + + const downloadBtn = document.createElement('button'); + downloadBtn.textContent = '⬇ Download'; + downloadBtn.style.flex = '1'; + downloadBtn.addEventListener('click', () => downloadFont(fontData.url, fontData.filename)); + buttonContainer.appendChild(downloadBtn); + + if (!shouldAutoPreview) { + const previewBtn = document.createElement('button'); + previewBtn.textContent = '👁 Preview'; + previewBtn.style.flex = '1'; + + let isPreviewVisible = false; + previewBtn.addEventListener('click', async () => { + if (!isPreviewVisible) { + previewContainer.style.display = 'block'; + const previewElement = document.getElementById(`preview-${fontData.filename}`); + if (await previewFont(fontData.url, fontData.family)) { + previewElement.style.fontFamily = fontData.family; + sizeVariations.querySelectorAll('div').forEach(div => { + div.style.fontFamily = fontData.family; + }); + previewBtn.textContent = '👁 Hide Preview'; + isPreviewVisible = true; + } else { + // If preview fails, hide the container again + previewContainer.style.display = 'none'; + } + } else { + previewContainer.style.display = 'none'; + previewBtn.textContent = '👁 Preview'; + isPreviewVisible = false; + } + }); + buttonContainer.appendChild(previewBtn); + } + + fontItem.appendChild(fontInfo); + fontItem.appendChild(previewContainer); + fontItem.appendChild(buttonContainer); + container.appendChild(fontItem); + + if (shouldAutoPreview) { + // Use setTimeout to ensure the DOM is ready + setTimeout(async () => { + const previewElement = document.getElementById(`preview-${fontData.filename}`); + if (await previewFont(fontData.url, fontData.family)) { + previewElement.style.fontFamily = fontData.family; + sizeVariations.querySelectorAll('div').forEach(div => { + div.style.fontFamily = fontData.family; + }); + } + }, 100); + } + } + + resultsDiv.appendChild(container); + } + + function showError(message) { + errorDiv.textContent = message; + errorDiv.style.display = 'block'; + } +}); \ No newline at end of file diff --git a/html/immoral/index.html b/html/immoral/index.html new file mode 100644 index 0000000..b6eee24 --- /dev/null +++ b/html/immoral/index.html @@ -0,0 +1,167 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>An Immoral Web Font Vacuum</title> + <meta name="description" content="Enter a URL to find, preview, and download web fonts (WOFF/TTF/WOFF2/OTF) present on the page."> + <style> + :root { + --beige: #f5f2e9; + --dark: #111111; + --accent: #ff4d00; + --grid-line: #ccbea7; + --container-bg: #ffffff; + --focus-outline: #2563eb; + } + + body { + font-family: 'Courier New', monospace; + max-width: 900px; + margin: 0 auto; + padding: 1rem; + line-height: 1.5; + background: var(--beige); + color: var(--dark); + } + + h1, h2 { + text-transform: uppercase; + letter-spacing: 2px; + border-bottom: 3px solid var(--accent); + padding-bottom: 0.5rem; + font-weight: 900; + } + + .container { + background: var(--container-bg); + padding: 2rem; + border: 3px solid var(--dark); + box-shadow: 8px 8px 0 var(--dark); + margin-top: 2rem; + } + + .input-group { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + border: 2px solid var(--dark); + padding: 1rem; + background: var(--beige); + } + + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + input[type="url"] { + flex: 1; + padding: 0.75rem; + font-size: 1rem; + border: 2px solid var(--dark); + background: var(--beige); + font-family: 'Courier New', monospace; + } + + input[type="url"]:focus { + outline: 3px solid var(--focus-outline); + outline-offset: 2px; + } + + button { + padding: 0.75rem 1.5rem; + font-size: 1rem; + background: var(--dark); + color: var(--beige); + border: 2px solid var(--dark); + cursor: pointer; + text-transform: uppercase; + font-weight: bold; + font-family: 'Courier New', monospace; + transition: all 0.2s; + } + + button:hover, + button:focus-visible { + background: var(--accent); + transform: translateY(-2px); + outline: 3px solid var(--focus-outline); + outline-offset: 2px; + } + + button:focus:not(:focus-visible) { + outline: none; + } + + .error { + color: var(--accent); + padding: 1rem; + background: rgba(255, 77, 0, 0.1); + border: 2px solid var(--accent); + margin-top: 1rem; + display: none; + font-weight: bold; + role: "alert"; + } + + #results { + margin-top: 1rem; + } + + /* Skip link for keyboard users */ + .skip-link { + position: absolute; + top: -40px; + left: 0; + background: var(--dark); + color: var(--beige); + padding: 8px; + z-index: 100; + transition: top 0.2s; + } + + .skip-link:focus { + top: 0; + outline: 3px solid var(--focus-outline); + } + </style> +</head> +<body> + <a href="#main-content" class="skip-link">Skip to main content</a> + <div class="container" id="main-content"> + <h1>Immoral Web Font Vacuum</h1> + <p>Enter a URL to find, preview, and download web fonts (WOFF/TTF/WOFF2/OTF) present on the page.</p> + + <form class="input-group" role="search" aria-label="Website URL search form" onsubmit="event.preventDefault();"> + <label for="urlInput" class="sr-only">Website URL</label> + <input + type="url" + id="urlInput" + name="urlInput" + placeholder="Enter website URL (e.g., https://example.com)" + required + aria-required="true" + aria-describedby="urlHint" + > + <span id="urlHint" class="sr-only">Enter the full website URL including https:// or http://</span> + <button + id="analyzeBtn" + type="submit" + aria-label="Analyze website for fonts" + >Analyze Fonts</button> + </form> + + <div id="error" class="error" role="alert" aria-live="polite"></div> + <div id="results" role="region" aria-label="Font analysis results"></div> + </div> + <script src="app.js"></script> +</body> +</html> |