1/** 2 * Sitewide-Ergaenzung fuer Bilder, die ohne die WordPress-Bildfunktionen 3 * ausgegeben werden (Pro). 4 * 5 * Die normale Kennzeichnung laeuft serverseitig ueber 6 * wp_get_attachment_image_html() bzw. den "wp-image-<ID>"-Klassenabgleich in 7 * "the_content" (siehe includes/frontend.php). Viele Theme-Builder-Widgets - 8 * allen voran das Beitragsbild- und Posts-Loop-Widget von Elementor - bauen 9 * ihr <img>-Markup aber selbst (z. B. per get_the_post_thumbnail_url()) und 10 * rufen wp_get_attachment_image() nie auf. Weder der Filter noch die 11 * "wp-image-"-Klasse greifen dann, das Bild bleibt unmarkiert - u. a. bei 12 * Beitragsbildern von Blogartikeln. 13 * 14 * Grundlage ist dieselbe Liste markierter Bild-URLs wie bei der 15 * WooCommerce-Ergaenzung (assets/js/frontend-woocommerce.js), hier aber fuer 16 * die gesamte Seite statt nur den Shop. Zusaetzlich wird jedes <img> anhand 17 * der von WordPress selbst vergebenen Klasse "wp-image-<ID>" direkt gegen 18 * die Liste markierter Anhang-IDs geprueft (robuster als der URL-Abgleich, 19 * u. a. bei Full-Page-Caching). Positioniert werden die Labels ueber die 20 * gemeinsame Schnittstelle window.kiimgPosition (frontend-position.js). 21 * Bereits vom Server gekennzeichnete Bilder werden nicht angefasst. 22 * 23 * NEBEN <img>-Elementen werden hier zusaetzlich CSS-Hintergrundbilder 24 * erkannt, die per Inline-style oder ueber ein Lazy-Load-Datenattribut 25 * gesetzt werden (z. B. bei Divi, WPBakery, Beaver Builder, Bricks oder 26 * individuellem Theme-Markup - ueberall dort, wo Elementor's eigene 27 * PHP-Erkennung in includes/elementor.php nicht greift, weil kein 28 * Elementor im Spiel ist). Rein per CSS-Klasse (externes Stylesheet ohne 29 * Inline-Angabe) gesetzte Hintergruende bleiben davon unberuehrt - das 30 * liesse sich nur durch das Durchsuchen der berechneten Stile jedes 31 * einzelnen Elements der Seite erkennen, was auf grossen Seiten spuerbar 32 * Leistung kosten wuerde. 33 */ 34( function () { 35 'use strict'; 36 37 if ( ! window.kiimgFallback ) { 38 return; 39 } 40 41 var data = window.kiimgFallback; 42 var AUTO_PREFIX = 'kiimg-fb-'; 43 var BG_PREFIX = 'kiimg-bgfb-'; 44 45 var autoCounter = 0; 46 var autoLabels = {}; 47 48 function refresh() { 49 if ( window.kiimgPosition && window.kiimgPosition.refresh ) { 50 window.kiimgPosition.refresh(); 51 } 52 } 53 54 function createLabel( instance ) { 55 var label = document.createElement( 'span' ); 56 label.className = 'kiimg-label'; 57 label.setAttribute( 'data-kiimg-for', instance ); 58 label.setAttribute( 'data-kiimg-position', data.position || 'bottom-right' ); 59 label.setAttribute( 'data-kiimg-margin', data.margin || '0' ); 60 label.setAttribute( 'style', data.labelStyle || '' ); 61 label.textContent = data.labelText || ''; 62 document.body.appendChild( label ); 63 return label; 64 } 65 66 /* ââ Abgleich der Bild-URLs (identisch zu frontend-woocommerce.js) ââââââââ */ 67 68 /* 69 * Die Dateiendung wird bewusst NICHT mitverglichen: wp_get_attachment_url() 70 * liefert immer das urspruenglich hochgeladene Format (z. B. .jpg/.png), 71 * viele Hosting-/CDN-/Optimierungs-Loesungen (z. B. automatische 72 * WebP/AVIF-Auslieferung) liefern dasselbe Bild im Frontend aber unter 73 * einer anderen Endung aus. Ohne diesen Schritt wuerde ein als KI-Bild 74 * markiertes Foto auf solchen Seiten nie erkannt. 75 * 76 * Manche Plugins haengen das neue Format zudem einfach an die 77 * bestehende Endung an (z. B. "foto.jpg.webp" statt "foto.webp"), 78 * deshalb werden alle bekannten Bild-Endungen vom Ende her entfernt, 79 * nicht nur die letzte einzelne. 80 */ 81 var IMAGE_EXTENSIONS = /\.(?:jpe?g|png|gif|webp|avif|bmp|tiff?|svg|ico|heic|jxl)$/i; 82 83 function stripSize( value ) { 84 var clean = String( value ).split( '?' )[ 0 ].split( '#' )[ 0 ]; 85 86 var next = clean.replace( IMAGE_EXTENSIONS, '' ); 87 while ( next !== clean ) { 88 clean = next; 89 next = clean.replace( IMAGE_EXTENSIONS, '' ); 90 } 91 92 // Falls eine seltene/unbekannte Endung uebrig bleibt, trotzdem einmal 93 // generisch entfernen, damit die Groessen-Angabe (z. B. "-700x401") 94 // direkt am Ende steht und erkannt wird. 95 clean = clean.replace( /\.[a-z0-9]+$/i, '' ); 96 97 return clean.replace( /-\d+x\d+$/i, '' ).toLowerCase(); 98 } 99 100 function pathKey( url ) { 101 if ( ! url ) { 102 return ''; 103 } 104 var clean = stripSize( url ); 105 return clean.replace( /^[a-z]+:\/\/[^/]+/i, '' ); 106 } 107 108 function fileKey( url ) { 109 var clean = pathKey( url ); 110 return clean.substring( clean.lastIndexOf( '/' ) + 1 ); 111 } 112 113 /* 114 * Manche Lazy-Load-Loesungen (aeltere Lazy-Load-Plugins, Cache-/ 115 * Performance-Plugins mit eigenem Lazy Load statt dem nativen 116 * loading="lazy") tragen die eigentliche Bild-URL erst in "data-src", 117 * "data-lazy-src" o. ae. ein und setzen "src" zunaechst auf einen 118 * Platzhalter (1x1-Pixel/Base64). Ohne diese Attribute zu pruefen, wuerde 119 * das Bild bis zum tatsaechlichen Nachladen (Scroll in die Naehe) nicht
120 * erkannt - bei kleinen Vorschaubildern in Blog-/Beitragslisten, die oft 121 * erst spaeter im Viewport erscheinen, faellt das besonders auf. 122 */ 123 var CANDIDATE_ATTRS = [ 'data-src', 'data-lazy-src', 'data-original', 'data-srcset', 'data-lazy-srcset', 'srcset' ]; 124 125 /* 126 * Zusaetzlich zum URL-Abgleich wird die Bild-ID direkt anhand der von 127 * WordPress selbst vergebenen Klasse "wp-image-<ID>" geprueft (dieselbe 128 * Klasse, die auch der "the_content"-Filter in includes/frontend.php 129 * serverseitig ausliest). Das ist zuverlaessiger als der URL-Abgleich, 130 * u. a. bei Elementor-Theme-Builder-Widgets (z. B. Beitragsbild), bei 131 * denen der serverseitige "wp_get_attachment_image_html"-Filter durch 132 * Full-Page-Caching (Bild wurde erst NACH dem Erzeugen der gecachten 133 * Seite als KI-Bild markiert) keine Wirkung mehr zeigt - die Bild-ID 134 * selbst bleibt davon unberuehrt. 135 */ 136 var WP_IMAGE_CLASS_REGEX = /\bwp-image-(\d+)\b/; 137 var markedIds = {}; 138 ( data.markedIds || [] ).forEach( function ( id ) { 139 markedIds[ id ] = true; 140 } ); 141 142 function getWpImageId( img ) { 143 var match = WP_IMAGE_CLASS_REGEX.exec( img.className || '' ); 144 return match ? match[ 1 ] : ''; 145 } 146 147 function isMarkedById( img ) { 148 var id = getWpImageId( img ); 149 return !! id && !! markedIds[ id ]; 150 } 151 152 function collectCandidateUrls( img ) { 153 var urls = []; 154 var direct = img.currentSrc || img.src; 155 if ( direct ) { 156 urls.push( direct ); 157 } 158 159 CANDIDATE_ATTRS.forEach( function ( attr ) { 160 var value = img.getAttribute( attr ); 161 if ( ! value ) { 162 return; 163 } 164 // srcset-artige Werte sind kommagetrennte "URL Breitenangabe"-Paare. 165 value.split( ',' ).forEach( function ( part ) { 166 var url = part.trim().split( /\s+/ )[ 0 ]; 167 if ( url ) { 168 urls.push( url ); 169 } 170 } ); 171 } ); 172 173 return urls; 174 } 175 176 function isMarkedImage( img ) { 177 if ( isMarkedById( img ) ) { 178 return true; 179 } 180 181 var urls = collectCandidateUrls( img ); 182 for ( var i = 0; i < urls.length; i++ ) { 183 if ( isMarkedUrl( urls[ i ] ) ) { 184 return true; 185 } 186 } 187 return false; 188 } 189 190 /* 191 * Generische Hintergrundbild-Erkennung (nicht Elementor-spezifisch, siehe 192 * includes/elementor.php fuer den PHP-seitigen Elementor-Weg). Deckt 193 * Inline-Styles ("style='background-image:url(...)'") sowie die 194 * gaengigsten Lazy-Load-Datenattribute fuer Hintergrundbilder ab. 195 */ 196 // Deckt sowohl "background-image:url(...)" als auch die Kurzschreibweise 197 // "background:url(...) no-repeat center/cover" ab (beide sind ueblich). 198 var BG_STYLE_REGEX = /background(?:-image)?\s*:[^;]*?url\(\s*(['"]?)([^'")]+)\1\s*\)/i; 199 var BG_ATTRS = [ 'data-bg', 'data-background', 'data-bg-image', 'data-background-image', 'data-bg-src', 'data-lazy-bg', 'data-lazy-background' ]; 200 var BG_SELECTOR = '[style*="background"], [data-bg], [data-background], [data-bg-image], [data-background-image], [data-bg-src], [data-lazy-bg], [data-lazy-background]'; 201 202 function collectBackgroundUrls( el ) { 203 var urls = []; 204 var style = el.getAttribute( 'style' ); 205 if ( style ) { 206 var match = BG_STYLE_REGEX.exec( style ); 207 if ( match && match[ 2 ] ) { 208 urls.push( match[ 2 ] ); 209 } 210 } 211 212 BG_ATTRS.forEach( function ( attr ) { 213 var value = el.getAttribute( attr ); 214 if ( value ) { 215 urls.push( value ); 216 } 217 } ); 218 219 return urls; 220 } 221 222 function isMarkedBackground( el ) { 223 var urls = collectBackgroundUrls( el ); 224 for ( var i = 0; i < urls.length; i++ ) { 225 if ( isMarkedUrl( urls[ i ] ) ) { 226 return true; 227 } 228 } 229 return false; 230 } 231 232 var markedPaths = {}; 233 var fileCounts = {}; 234 235 ( data.markedUrls || [] ).forEach( function ( url ) { 236 var path = pathKey( url ); 237 if ( ! path ) { 238 return; 239 } 240 markedPaths[ path ] = true; 241 var file = fileKey( url ); 242 fileCounts[ file ] = ( fileCounts[ file ] || 0 ) + 1; 243 } ); 244 245 var markedFiles = {}; 246 Object.keys( fileCounts ).forEach( function ( file ) { 247 if ( 1 === fileCounts[ file ] ) { 248 markedFiles[ file ] = true; 249 } 250 } ); 251 252 var hasMarked = Object.keys( markedPaths ).length > 0 || Object.keys( markedIds ).length > 0; 253 254 function isMarkedUrl( url ) { 255 if ( ! url ) { 256 return false;
257 } 258 return !! markedPaths[ pathKey( url ) ] || !! markedFiles[ fileKey( url ) ]; 259 } 260 261 /* ââ Bilder ohne Server-Label nachtragen âââââââââââââââââââââââââââââââââ */ 262 263 function removeAutoLabel( instance ) { 264 var label = autoLabels[ instance ]; 265 if ( label && label.parentNode ) { 266 label.parentNode.removeChild( label ); 267 } 268 delete autoLabels[ instance ]; 269 } 270 271 /** 272 * Gemeinsame Zuordnungslogik fuer <img>-Elemente und Hintergrundbild- 273 * Container: prueft eine Liste von Kandidaten-Elementen, vergibt/entfernt 274 * die "data-kiimg-instance"-Markierung samt Label. Der Praefix 275 * unterscheidet Bild- und Hintergrund-Auto-Labels voneinander (nur fuer 276 * Debugging/Lesbarkeit relevant, die Logik ist identisch). 277 */ 278 function syncElements( elements, prefix, isMarkedFn ) { 279 var changed = false; 280 281 for ( var i = 0; i < elements.length; i++ ) { 282 var el = elements[ i ]; 283 var instance = el.getAttribute( 'data-kiimg-instance' ); 284 var isAuto = !! instance && 0 === instance.indexOf( prefix ); 285 286 if ( instance && ! isAuto ) { 287 continue; 288 } 289 290 var marked = isMarkedFn( el ); 291 292 if ( marked && ! isAuto ) { 293 autoCounter++; 294 var newInstance = prefix + autoCounter; 295 el.setAttribute( 'data-kiimg-instance', newInstance ); 296 autoLabels[ newInstance ] = createLabel( newInstance ); 297 changed = true; 298 } else if ( ! marked && isAuto ) { 299 el.removeAttribute( 'data-kiimg-instance' ); 300 removeAutoLabel( instance ); 301 changed = true; 302 } 303 } 304 305 return changed; 306 } 307 308 /** 309 * Geht alle Bilder und Hintergrundbild-Container der Seite durch und 310 * ergaenzt bzw. entfernt Labels. Bereits vom Server oder von anderen 311 * Modulen (z. B. WooCommerce-Ergaenzung, Elementor-Hintergruende) 312 * gekennzeichnete Elemente bleiben unangetastet. 313 */ 314 function syncAll() { 315 if ( ! hasMarked ) { 316 return; 317 } 318 319 var changed = false; 320 321 changed = syncElements( document.images, AUTO_PREFIX, isMarkedImage ) || changed; 322 323 // Derselbe Schalter wie fuer Elementor-Hintergruende (Tab "Design"), 324 // damit ein Klick an einer Stelle beide Wege gleichermassen steuert. 325 if ( data.bgEnabled ) { 326 changed = syncElements( document.querySelectorAll( BG_SELECTOR ), BG_PREFIX, isMarkedBackground ) || changed; 327 } 328 329 Object.keys( autoLabels ).forEach( function ( instance ) { 330 if ( ! document.querySelector( '[data-kiimg-instance="' + instance + '"]' ) ) { 331 removeAutoLabel( instance ); 332 changed = true; 333 } 334 } ); 335 336 if ( changed ) { 337 refresh(); 338 } 339 } 340 341 /* ââ Laufende Beobachtung ââââââââââââââââââââââââââââââââââââââââââââââââ */ 342 343 /* 344 * Slider, Lazy Loading und clientseitig nachgeladene Blockinhalte 345 * (z. B. Elementor-Posts-Widget mit AJAX-Pagination) fuegen Bilder 346 * fortlaufend hinzu - ein einmaliger Durchlauf beim Laden reicht nicht. 347 */ 348 var tick = false; 349 function scheduleSync() { 350 if ( tick ) { 351 return; 352 } 353 tick = true; 354 requestAnimationFrame( function () { 355 tick = false; 356 syncAll(); 357 } ); 358 } 359 360 function bindObserver() { 361 if ( ! ( 'MutationObserver' in window ) ) { 362 return; 363 } 364 new MutationObserver( scheduleSync ).observe( document.body, { 365 childList: true, 366 subtree: true, 367 attributes: true, 368 attributeFilter: [ 'src', 'srcset', 'class', 'style', 'data-src', 'data-lazy-src', 'data-original', 'data-srcset', 'data-lazy-srcset', 'data-bg', 'data-background', 'data-bg-image', 'data-background-image', 'data-bg-src', 'data-lazy-bg', 'data-lazy-background' ] 369 } ); 370 } 371 372 function init() { 373 bindObserver(); 374 syncAll(); 375 } 376 377 if ( 'loading' === document.readyState ) { 378 document.addEventListener( 'DOMContentLoaded', init ); 379 } else { 380 init(); 381 } 382} )();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.