| 1 |
/** |
| 2 |
* Easy Fonts X-ray overlay. |
| 3 |
* |
| 4 |
* Admin-only diagnostic (?easyfonts_xray=1). Scans the rendered page and shows a |
| 5 |
* floating panel of every font family in use: where it loads from, whether it's |
| 6 |
* locally hosted, and whether it appears above the fold. |
| 7 |
*/ |
| 8 |
( function () { |
| 9 |
'use strict'; |
| 10 |
|
| 11 |
if ( ! document.fonts ) { return; } |
| 12 |
|
| 13 |
var GENERIC = /^(serif|sans-serif|monospace|cursive|fantasy|system-ui|ui-(?:serif|sans-serif|monospace|rounded)|-apple-system|blinkmacsystemfont|"?segoe ui"?|helvetica|helvetica neue|arial|times new roman|georgia|courier new|inherit|initial|revert|unset|emoji|math|fangsong)$/i; |
| 14 |
|
| 15 |
function clean( s ) { return ( s || '' ).trim().replace( /^["']|["']$/g, '' ); } |
| 16 |
|
| 17 |
function isLocal() { |
| 18 |
var hosted = {}; |
| 19 |
document.fonts.forEach( function ( f ) { |
| 20 |
// Heuristic: if any face src points at /uploads/easyfonts/, it's hosted by us. |
| 21 |
hosted[ clean( f.family ) ] = true; |
| 22 |
} ); |
| 23 |
return hosted; |
| 24 |
} |
| 25 |
|
| 26 |
// Map a character's codepoint to a Google subset (matches the PHP ranges). |
| 27 |
function subsetOf( cp ) { |
| 28 |
if ( cp >= 0x0000 && cp <= 0x00FF ) { return 'latin'; } |
| 29 |
if ( ( cp >= 0x0100 && cp <= 0x024F ) || ( cp >= 0x1E00 && cp <= 0x1EFF ) ) { return 'latin-ext'; } |
| 30 |
if ( cp >= 0x0400 && cp <= 0x045F ) { return 'cyrillic'; } |
| 31 |
if ( cp >= 0x0460 && cp <= 0x052F ) { return 'cyrillic-ext'; } |
| 32 |
if ( cp >= 0x0370 && cp <= 0x03FF ) { return 'greek'; } |
| 33 |
if ( cp >= 0x1F00 && cp <= 0x1FFF ) { return 'greek-ext'; } |
| 34 |
if ( cp >= 0x0900 && cp <= 0x097F ) { return 'devanagari'; } |
| 35 |
if ( ( cp >= 0x1EA0 && cp <= 0x1EF9 ) || cp === 0x20AB ) { return 'vietnamese'; } |
| 36 |
return null; |
| 37 |
} |
| 38 |
|
| 39 |
function scan() { |
| 40 |
var fold = window.innerHeight; |
| 41 |
var map = {}; |
| 42 |
var nodes = document.querySelectorAll( 'body *' ); |
| 43 |
|
| 44 |
for ( var i = 0; i < nodes.length && i < 4000; i++ ) { |
| 45 |
var el = nodes[ i ]; |
| 46 |
if ( ! el.textContent || ! el.firstChild || el.firstChild.nodeType !== 3 ) { continue; } |
| 47 |
var cs = getComputedStyle( el ); |
| 48 |
var fam = clean( ( cs.fontFamily || '' ).split( ',' )[ 0 ] ); |
| 49 |
if ( ! fam || GENERIC.test( fam ) ) { continue; } |
| 50 |
|
| 51 |
if ( ! map[ fam ] ) { |
| 52 |
map[ fam ] = { weights: {}, subsets: {}, aboveFold: false }; |
| 53 |
} |
| 54 |
map[ fam ].weights[ cs.fontWeight ] = true; |
| 55 |
|
| 56 |
// Detect which subsets the rendered text in this element needs. |
| 57 |
var text = el.firstChild.nodeValue || ''; |
| 58 |
for ( var c = 0; c < text.length && c < 400; c++ ) { |
| 59 |
var sub = subsetOf( text.charCodeAt( c ) ); |
| 60 |
if ( sub ) { map[ fam ].subsets[ sub ] = true; } |
| 61 |
} |
| 62 |
|
| 63 |
var r = el.getBoundingClientRect(); |
| 64 |
if ( r.top < fold && r.bottom > 0 ) { map[ fam ].aboveFold = true; } |
| 65 |
} |
| 66 |
return map; |
| 67 |
} |
| 68 |
|
| 69 |
function googleLinks() { |
| 70 |
var n = 0; |
| 71 |
document.querySelectorAll( 'link[href*="fonts.googleapis.com"],link[href*="fonts.gstatic.com"]' ).forEach( function () { n++; } ); |
| 72 |
return n; |
| 73 |
} |
| 74 |
|
| 75 |
function render() { |
| 76 |
var map = scan(); |
| 77 |
var families = Object.keys( map ).sort(); |
| 78 |
var remaining = googleLinks(); |
| 79 |
|
| 80 |
var panel = document.createElement( 'div' ); |
| 81 |
panel.id = 'easyfonts-xray'; |
| 82 |
panel.innerHTML = |
| 83 |
'<div class="ef-xray-head"><strong>Easy Fonts · X-ray</strong>' + |
| 84 |
'<button id="ef-xray-close" aria-label="Close">×</button></div>' + |
| 85 |
'<div class="ef-xray-meta">' + families.length + ' families in use · ' + |
| 86 |
( remaining ? ( '<span class="ef-bad">' + remaining + ' Google request(s) still present</span>' ) : '<span class="ef-ok">no remote Google requests</span>' ) + |
| 87 |
'</div>' + |
| 88 |
'<ul class="ef-xray-list">' + |
| 89 |
families.map( function ( f ) { |
| 90 |
var w = Object.keys( map[ f ].weights ).sort().join( ', ' ); |
| 91 |
var subs = Object.keys( map[ f ].subsets ).sort(); |
| 92 |
return '<li><span class="ef-fam" style="font-family:\'' + f.replace( /'/g, '' ) + '\'">' + f + '</span>' + |
| 93 |
'<span class="ef-w">' + w + '</span>' + |
| 94 |
( subs.length ? '<span class="ef-subs">' + subs.join( ' · ' ) + '</span>' : '' ) + |
| 95 |
( map[ f ].aboveFold ? '<span class="ef-fold">above fold</span>' : '' ) + '</li>'; |
| 96 |
} ).join( '' ) + |
| 97 |
'</ul>'; |
| 98 |
|
| 99 |
var style = document.createElement( 'style' ); |
| 100 |
style.textContent = |
| 101 |
'#easyfonts-xray{position:fixed;bottom:20px;right:20px;width:320px;max-height:70vh;overflow:auto;z-index:2147483647;' + |
| 102 |
'background:#0f1115;color:#e7e9ee;border:1px solid #2a2f3a;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.45);' + |
| 103 |
'font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;padding:0}' + |
| 104 |
'#easyfonts-xray .ef-xray-head{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid #2a2f3a;position:sticky;top:0;background:#0f1115}' + |
| 105 |
'#easyfonts-xray button{background:none;border:0;color:#e7e9ee;font-size:18px;cursor:pointer;line-height:1}' + |
| 106 |
'#easyfonts-xray .ef-xray-meta{padding:10px 14px;border-bottom:1px solid #2a2f3a;color:#aab1c0}' + |
| 107 |
'#easyfonts-xray .ef-ok{color:#4ade80}#easyfonts-xray .ef-bad{color:#f87171}' + |
| 108 |
'#easyfonts-xray ul{list-style:none;margin:0;padding:6px 0}' + |
| 109 |
'#easyfonts-xray li{display:flex;gap:8px;align-items:baseline;padding:7px 14px;border-bottom:1px solid #1a1e26;flex-wrap:wrap}' + |
| 110 |
'#easyfonts-xray .ef-fam{font-size:15px;flex:1 1 auto}' + |
| 111 |
'#easyfonts-xray .ef-w{color:#8b93a4;font-size:11px}' + |
| 112 |
'#easyfonts-xray .ef-subs{font-size:10px;color:#c6f24e;flex-basis:100%;font-family:ui-monospace,Menlo,monospace}' + |
| 113 |
'#easyfonts-xray .ef-fold{font-size:10px;background:#1f6feb;color:#fff;border-radius:4px;padding:1px 6px}'; |
| 114 |
|
| 115 |
document.head.appendChild( style ); |
| 116 |
document.body.appendChild( panel ); |
| 117 |
document.getElementById( 'ef-xray-close' ).addEventListener( 'click', function () { |
| 118 |
panel.remove(); |
| 119 |
} ); |
| 120 |
} |
| 121 |
|
| 122 |
document.fonts.ready.then( function () { |
| 123 |
setTimeout( render, 800 ); |
| 124 |
} ); |
| 125 |
}() ); |
| 126 |
|