diff options
author | elioat <elioat@tilde.institute> | 2025-03-15 22:51:37 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2025-03-15 22:51:37 -0400 |
commit | a78e5d4db2d220b0f19280c4723539d9c1d564c0 (patch) | |
tree | 8b7ffae2dac98982994b5cc9915f5548f688e360 | |
parent | 4c788c1865e52b61a94922328c245bc1c22e8659 (diff) | |
download | tour-a78e5d4db2d220b0f19280c4723539d9c1d564c0.tar.gz |
*
-rw-r--r-- | html/immoral/bookmarklet.js | 451 | ||||
-rw-r--r-- | html/immoral/index.html | 59 |
2 files changed, 490 insertions, 20 deletions
diff --git a/html/immoral/bookmarklet.js b/html/immoral/bookmarklet.js new file mode 100644 index 0000000..a7b929b --- /dev/null +++ b/html/immoral/bookmarklet.js @@ -0,0 +1,451 @@ +(function() { + // Prevent multiple instances + if (window.immoralFontVacuum) { + alert('Font Vacuum is already running!'); + return; + } + window.immoralFontVacuum = true; + + const style = document.createElement('style'); + style.textContent = ` + .fv-container { + position: fixed; + top: 20px; + right: 20px; + width: 400px; + max-height: 90vh; + color: #333333; + background: #f5f5f5; + border: 2px solid #333; + border-radius: 8px; + z-index: 999999; + overflow-y: auto; + font-family: system-ui, -apple-system, sans-serif; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + } + .fv-header { + padding: 1rem; + background: #333333; + color: #f5f5f5; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + } + .fv-close { + background: none; + border: none; + color: #f5f5f5; + cursor: pointer; + font-size: 1.5rem; + padding: 0; + } + .fv-content { + padding: 1rem; + } + .fv-font-item { + margin-bottom: 1rem; + padding: 1rem; + border: 1px solid #ddd; + border-radius: 4px; + } + .fv-preview { + margin: 1rem 0; + padding: 1rem; + border: 1px dashed #333; + } + .fv-button { + background: #333; + color: #f5f5f5; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + margin: 0.25rem 0.5rem 0.25rem 0; + } + .fv-button:last-child { + margin-right: 0; + } + .fv-button:hover { + background: #444; + } + `; + document.head.appendChild(style); + + // Create UI container + const container = document.createElement('div'); + container.className = 'fv-container'; + + // Make the container draggable + let isDragging = false; + let currentX; + let currentY; + let initialX; + let initialY; + let xOffset = 0; + let yOffset = 0; + + // Create header with drag handle + const header = document.createElement('div'); + header.className = 'fv-header'; + header.innerHTML = ` + <div>Font Vacuum</div> + <button class="fv-close">×</button> + `; + + // Add drag functionality + header.addEventListener('mousedown', dragStart); + document.addEventListener('mousemove', drag); + document.addEventListener('mouseup', dragEnd); + + function dragStart(e) { + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + if (e.target === header) { + isDragging = true; + } + } + + function drag(e) { + if (isDragging) { + e.preventDefault(); + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + xOffset = currentX; + yOffset = currentY; + container.style.transform = `translate(${currentX}px, ${currentY}px)`; + } + } + + function dragEnd() { + isDragging = false; + } + + // Add close button functionality + header.querySelector('.fv-close').addEventListener('click', () => { + document.body.removeChild(container); + window.immoralFontVacuum = false; + }); + + // Create content area + const content = document.createElement('div'); + content.className = 'fv-content'; + + // Helper to extract font URLs from CSS text + function extractFontUrls(cssText, baseUrl) { + const fontUrls = []; + const fontFaceRegex = /@font-face\s*{[^}]*}/g; + const urlRegex = /url\(['"]?([^'"\)]+)['"]?\)/g; + const fontFamilyRegex = /font-family\s*:\s*['"]?([^'";]*)['"]?/; + + function resolveUrl(url, base) { + try { + // Handle protocol-relative URLs + if (url.startsWith('//')) { + return `${location.protocol}${url}`; + } + // Handle absolute URLs + if (url.match(/^https?:\/\//)) { + return url; + } + // Handle root-relative URLs + if (url.startsWith('/')) { + return `${location.origin}${url}`; + } + // Handle relative URLs - use stylesheet URL as base if available + return new URL(url, base || location.href).href; + } catch (e) { + console.warn('Failed to resolve URL:', url, e); + return url; + } + } + + let fontFaceMatch; + while ((fontFaceMatch = fontFaceRegex.exec(cssText)) !== null) { + const fontFaceBlock = fontFaceMatch[0]; + const familyMatch = fontFaceBlock.match(fontFamilyRegex); + const fontFamily = familyMatch ? familyMatch[1].trim() : 'Unknown Font'; + + let urlMatch; + while ((urlMatch = urlRegex.exec(fontFaceBlock)) !== null) { + let fontUrl = urlMatch[1].trim(); + + // Skip data: URLs + if (fontUrl.startsWith('data:')) continue; + + // Only process known font file types + if (!fontUrl.match(/\.(woff2?|ttf|otf|eot)(\?.*)?$/i)) continue; + + // Resolve the URL relative to the stylesheet's URL + fontUrl = resolveUrl(fontUrl, baseUrl); + + fontUrls.push({ + family: fontFamily, + url: fontUrl, + filename: fontUrl.split('/').pop().split('?')[0], + cssRule: fontFaceBlock + }); + } + } + return fontUrls; + } + + // Function to find all fonts on the page + function findFonts() { + const fonts = new Map(); + console.group('Font Vacuum: Scanning Stylesheets'); + + // Log total number of stylesheets + console.log(`Found ${document.styleSheets.length} stylesheets`); + + for (const sheet of document.styleSheets) { + try { + const baseUrl = sheet.href; + console.group(`Stylesheet: ${baseUrl || 'inline'}`); + const cssRules = sheet.cssRules || sheet.rules; + console.log(`- Rules found: ${cssRules.length}`); + + let cssText = ''; + let fontFaceCount = 0; + for (const rule of cssRules) { + if (rule.type === CSSRule.FONT_FACE_RULE) { + fontFaceCount++; + } + cssText += rule.cssText + '\n'; + } + console.log(`- @font-face rules found: ${fontFaceCount}`); + + const fontUrls = extractFontUrls(cssText, baseUrl); + console.log(`- Font URLs extracted: ${fontUrls.length}`); + fontUrls.forEach(font => { + console.log(` • ${font.family}: ${font.url}`); + if (!fonts.has(font.family)) { + fonts.set(font.family, { + variants: [], + cssRule: font.cssRule + }); + } + fonts.get(font.family).variants.push(font); + }); + console.groupEnd(); + } catch (e) { + console.warn(`Could not access stylesheet:`, sheet.href, e); + console.groupEnd(); + } + } + + const results = Array.from(fonts.entries()).map(([family, data]) => ({ + family, + variants: data.variants, + cssRule: data.cssRule + })); + + console.log('Final Results:', { + totalFamilies: results.length, + families: results.map(f => ({ + family: f.family, + variants: f.variants.length, + urls: f.variants.map(v => v.url) + })) + }); + console.groupEnd(); + + return results; + } + + async function downloadFont(url, filename) { + try { + console.group(`Font Vacuum: Downloading ${filename} from ${url}`); + + // Try to get the font from an already loaded stylesheet first + console.log('Searching for existing font-face rule...'); + const existingFontRule = Array.from(document.styleSheets) + .flatMap(sheet => { + try { + return Array.from(sheet.cssRules); + } catch (e) { + return []; + } + }) + .find(rule => + rule.type === CSSRule.FONT_FACE_RULE && + rule.cssText.includes(url) + ); + + console.log('Existing font-face rule found:', !!existingFontRule); + let response; + + if (existingFontRule) { + console.log('Attempting to fetch using existing rule credentials...'); + const fontBlob = await fetch(url, { + mode: 'cors', + credentials: 'include', + headers: { + 'Origin': window.location.origin + } + }).then(r => r.blob()); + response = new Response(fontBlob); + } else { + console.log('No existing rule found, attempting direct fetch...'); + response = await fetch(url, { + mode: 'cors', + credentials: 'include', + headers: { + 'Origin': window.location.origin + } + }); + } + + if (!response.ok) { + throw new Error(`Network response was not ok. Status: ${response.status}`); + } + + console.log('Font fetched successfully, preparing download...'); + const blob = await response.blob(); + console.log('Font blob size:', blob.size, 'bytes'); + + const objectUrl = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = objectUrl; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + setTimeout(() => URL.revokeObjectURL(objectUrl), 100); + console.log('Download initiated successfully'); + console.groupEnd(); + return true; + } catch (error) { + console.error('Error downloading font:', error); + console.groupEnd(); + alert(`Error downloading font: ${error.message}\n\nTroubleshooting tips:\n1. Check the console for detailed logs\n2. Try using your browser's developer tools Network tab to find and download the font file directly\n3. Some sites may block direct font downloads`); + return false; + } + } + + async function previewFont(url, fontFamily) { + try { + console.group(`Font Vacuum: Previewing ${fontFamily} from ${url}`); + + const existingFontRule = Array.from(document.styleSheets) + .flatMap(sheet => { + try { + return Array.from(sheet.cssRules); + } catch (e) { + return []; + } + }) + .find(rule => + rule.type === CSSRule.FONT_FACE_RULE && + rule.cssText.includes(url) + ); + + if (existingFontRule) { + console.log('Using existing font-face rule for preview'); + console.groupEnd(); + return true; + } + + console.log('No existing rule found, attempting to load font...'); + const response = await fetch(url, { + mode: 'cors', + credentials: 'include', + headers: { + 'Origin': window.location.origin + } + }); + + if (!response.ok) { + throw new Error(`Network response was not ok. Status: ${response.status}`); + } + + const blob = await response.blob(); + console.log('Font blob size:', blob.size, 'bytes'); + 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); + + console.log('Font loaded successfully'); + console.groupEnd(); + return true; + } catch (error) { + console.error('Error loading font:', error); + console.groupEnd(); + return false; + } + } + + const fonts = findFonts(); + + if (fonts.length === 0) { + content.innerHTML = '<p>No web fonts found on this page.</p>'; + } else { + fonts.forEach(fontData => { + const fontItem = document.createElement('div'); + fontItem.className = 'fv-font-item'; + + // Font family name + const fontName = document.createElement('h3'); + fontName.style.margin = '0 0 1rem 0'; + fontName.textContent = fontData.family; + fontItem.appendChild(fontName); + + // Preview section + const preview = document.createElement('div'); + preview.className = 'fv-preview'; + preview.innerHTML = 'Society for me my misery<br>Since Gift of Thee --'; + fontItem.appendChild(preview); + + // Download buttons for unique files + const uniqueDownloads = new Map(); + fontData.variants.forEach(variant => { + if (!uniqueDownloads.has(variant.url)) { + uniqueDownloads.set(variant.url, { + filename: variant.filename, + url: variant.url + }); + } + }); + + const buttonContainer = document.createElement('div'); + buttonContainer.style.marginTop = '1rem'; + + uniqueDownloads.forEach(({filename, url}) => { + const downloadBtn = document.createElement('button'); + downloadBtn.className = 'fv-button'; + downloadBtn.textContent = `⬇ Download ${filename}`; + downloadBtn.addEventListener('click', () => downloadFont(url, filename)); + buttonContainer.appendChild(downloadBtn); + }); + + fontItem.appendChild(buttonContainer); + + fontData.variants.forEach(async (variant) => { + if (await previewFont(variant.url, fontData.family)) { + preview.style.fontFamily = fontData.family; + if (variant.cssRule) { + const fontStyle = variant.cssRule.match(/font-style:\s*([^;]+)/); + const fontWeight = variant.cssRule.match(/font-weight:\s*([^;]+)/); + if (fontStyle) preview.style.fontStyle = fontStyle[1]; + if (fontWeight) preview.style.fontWeight = fontWeight[1]; + } + } + }); + + content.appendChild(fontItem); + }); + } + + container.appendChild(header); + container.appendChild(content); + document.body.appendChild(container); +})(); \ No newline at end of file diff --git a/html/immoral/index.html b/html/immoral/index.html index b7c089d..c2f4b26 100644 --- a/html/immoral/index.html +++ b/html/immoral/index.html @@ -145,26 +145,45 @@ <main class="container"> <h1><span class="immoral">Immoral</span> 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="urlForm"> + <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> + <div class="bookmarklet-section" style="margin: 2rem 0; padding: 1rem; border: 2px dashed var(--dark); background: var(--beige);"> + <h2>Font Vacuum Bookmarklet</h2> + <p>Drag this link to your bookmarks bar to vacuum fonts from any webpage:</p> + <p style="text-align: center;"> + <a href="javascript:(function(){ + const script = document.createElement('script'); + script.src = 'https://smallandnearlysilent.com/immoral/bookmarklet.js'; + document.body.appendChild(script); + })()" + class="bookmarklet-link" + style="display: inline-block; padding: 0.5rem 1rem; background: var(--dark); color: var(--beige); text-decoration: none; border-radius: 4px; font-weight: bold;" + onclick="event.preventDefault(); alert('Drag this to your bookmarks bar!');"> + Font Vacuum + </a> + </p> + <p style="font-size: 0.9rem; color: var(--dark); margin-top: 1rem;"> + How to: Click the bookmarklet on any webpage to find and download its fonts directly. + </p> + </div> <div id="error" class="error" role="alert" aria-live="polite"></div> <div id="results" role="region" aria-label="Font analysis results"></div> </main> |