(function() {
// Prevent multiple instances from running at once
if (window.immoralFontVacuum) {
alert('Web Font Vacuum is already running!');
return;
}
window.immoralFontVacuum = true;
const logCollector = {
logs: [],
group: function(label) {
this.logs.push(`\n### ${label}`);
},
groupEnd: function() {
this.logs.push(`### End Group\n`);
},
log: function(...args) {
this.logs.push(args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg
).join(' '));
},
warn: function(...args) {
this.logs.push(`⚠️ ${args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg
).join(' ')}`);
},
error: function(...args) {
this.logs.push(`❌ ${args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg
).join(' ')}`);
},
getReport: function() {
return `Font Vacuum Report
==================
Time: ${new Date().toISOString()}
URL: ${window.location.href}
${this.logs.join('\n')}`;
}
};
const styleRoot = document.createElement('div');
styleRoot.className = 'fv-root';
styleRoot.style.all = 'initial'; // Reset all styles
const style = document.createElement('style');
style.textContent = `
.fv-root {
font: 16px system-ui, -apple-system, sans-serif;
color: #333333;
line-height: 1.4;
box-sizing: border-box;
}
.fv-root * {
box-sizing: inherit;
font-family: inherit;
line-height: inherit;
color: inherit;
}
.fv-container {
position: fixed;
top: 20px;
right: 20px;
width: 400px;
max-height: 90vh;
background: #f5f5f5;
z-index: 999999;
overflow-y: auto;
display: flex;
flex-direction: column;
border: 3px solid #333;
box-shadow: 8px 8px 0 #ff4d00;
}
.fv-header {
padding: 1rem;
background: #333333;
color: #f5f5f5;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
user-select: none;
flex-shrink: 0;
}
.fv-header h1 {
margin: 0;
font-size: 1.1rem;
line-height: 1;
}
.fv-close {
background: none;
border: none;
color: #f5f5f5;
cursor: pointer;
font-size: 1.5rem;
padding: 0;
margin: 0;
line-height: 1;
display: flex;
align-items: center;
}
.fv-content {
padding: 1rem;
overflow-y: auto;
flex-grow: 1;
}
.fv-footer {
padding: 0.75rem 1rem;
background: #333333;
color: #f5f5f5;
display: flex;
justify-content: flex-end;
align-items: center;
flex-shrink: 0;
}
.fv-footer-button {
background: #555;
color: #f5f5f5;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 0.9em;
display: flex;
align-items: center;
gap: 0.5rem;
}
.fv-footer-button:hover {
background: #666;
}
.fv-font-item {
margin-bottom: 1rem;
padding: 1rem;
border: 1px solid #ddd;
background: #ffffff;
}
.fv-font-item h3 {
margin: 0 0 1rem 0;
padding: 0;
font-size: 1.1em;
font-weight: 600;
}
.fv-preview {
margin: 1rem 0;
padding: 1rem;
border: 1px dashed #333;
background: #ffffff;
}
.fv-button {
background: #333;
color: #f5f5f5;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
margin: 0.25rem 0.5rem 0.25rem 0;
font-size: 0.9em;
}
.fv-button:last-child {
margin-right: 0;
}
.fv-button:hover {
background: #444;
}
`;
styleRoot.appendChild(style);
document.body.appendChild(styleRoot);
const container = document.createElement('div');
container.className = 'fv-container';
styleRoot.appendChild(container);
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
const header = document.createElement('header');
header.className = 'fv-header';
header.innerHTML = `
Web Font Vacuum
`;
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;
}
header.querySelector('.fv-close').addEventListener('click', () => {
document.body.removeChild(styleRoot);
window.immoralFontVacuum = false;
});
const content = document.createElement('div');
content.className = 'fv-content';
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 {
// Protocol-relative URLs
if (url.startsWith('//')) {
return `${location.protocol}${url}`;
}
// Absolute URLs
if (url.match(/^https?:\/\//)) {
return url;
}
// Root-relative URLs
if (url.startsWith('/')) {
return `${location.origin}${url}`;
}
// 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 findFonts() {
const fonts = new Map();
logCollector.group('Font Vacuum: Scanning Stylesheets');
logCollector.log(`Found ${document.styleSheets.length} stylesheets`);
for (const sheet of document.styleSheets) {
try {
const baseUrl = sheet.href;
logCollector.group(`Stylesheet: ${baseUrl || 'inline'}`);
const cssRules = sheet.cssRules || sheet.rules;
logCollector.log(`- Rules found: ${cssRules.length}`);
let cssText = '';
let fontFaceCount = 0;
for (const rule of cssRules) {
if (rule.constructor.name === 'CSSFontFaceRule') {
fontFaceCount++;
}
cssText += rule.cssText + '\n';
}
logCollector.log(`- @font-face rules found: ${fontFaceCount}`);
const fontUrls = extractFontUrls(cssText, baseUrl);
logCollector.log(`- Font URLs extracted: ${fontUrls.length}`);
fontUrls.forEach(font => {
logCollector.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);
});
logCollector.groupEnd();
} catch (e) {
logCollector.warn(`Could not access stylesheet:`, sheet.href, e);
logCollector.groupEnd();
}
}
const results = Array.from(fonts.entries()).map(([family, data]) => ({
family,
variants: data.variants,
cssRule: data.cssRule
}));
logCollector.log('Final Results:', {
totalFamilies: results.length,
families: results.map(f => ({
family: f.family,
variants: f.variants.length,
urls: f.variants.map(v => v.url)
}))
});
logCollector.groupEnd();
return results;
}
async function downloadFont(url, filename) {
try {
logCollector.group(`Font Vacuum: Downloading ${filename} from ${url}`);
logCollector.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.constructor.name === 'CSSFontFaceRule' &&
rule.cssText.includes(url)
);
logCollector.log('Existing font-face rule found:', !!existingFontRule);
let response;
if (existingFontRule) {
logCollector.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 {
logCollector.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}`);
}
logCollector.log('Font fetched successfully, preparing download...');
const blob = await response.blob();
logCollector.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);
logCollector.log('Download initiated successfully');
logCollector.groupEnd();
return true;
} catch (error) {
logCollector.error('Error downloading font:', error);
logCollector.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 {
logCollector.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.constructor.name === 'CSSFontFaceRule' &&
rule.cssText.includes(url)
);
if (existingFontRule) {
logCollector.log('Using existing font-face rule for preview');
logCollector.groupEnd();
return true;
}
logCollector.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();
logCollector.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);
logCollector.log('Font loaded successfully');
logCollector.groupEnd();
return true;
} catch (error) {
logCollector.error('Error loading font:', error);
logCollector.groupEnd();
return false;
}
}
const fonts = findFonts();
if (fonts.length === 0) {
content.innerHTML = 'No web fonts found on this page.
';
} else {
fonts.forEach(fontData => {
const fontItem = document.createElement('div');
fontItem.className = 'fv-font-item';
const fontName = document.createElement('h3');
fontName.style.margin = '0 0 1rem 0';
fontName.textContent = fontData.family;
fontItem.appendChild(fontName);
const preview = document.createElement('div');
preview.className = 'fv-preview';
preview.innerHTML = '0123456789
Society for me my misery
Since Gift of Thee --
The quick brown fox jumps over the lazy dog!?';
fontItem.appendChild(preview);
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);
});
}
const footer = document.createElement('div');
footer.className = 'fv-footer';
const reportBtn = document.createElement('button');
reportBtn.className = 'fv-footer-button';
reportBtn.innerHTML = '📋Copy Debug Report';
reportBtn.addEventListener('click', () => {
const report = logCollector.getReport();
navigator.clipboard.writeText(report).then(() => {
reportBtn.innerHTML = '✅Report Copied!';
setTimeout(() => {
reportBtn.innerHTML = '📋Copy Debug Report';
}, 2000);
});
});
footer.appendChild(reportBtn);
container.appendChild(header);
container.appendChild(content);
container.appendChild(footer);
styleRoot.appendChild(container);
})();