PageSourceSearch

https://fwfuer.de/wp-content/plugins/aim-transparency/assets/js/badge.js?ver=1790102479

js fwfuer.de collected 2026-10-04 20:30:56 UTC 24,025 bytes, 659 lines download raw bytes

1/* AIM Transparency — front-end badge injector.
2 * Renders a visible badge into the nearest positioned ancestor for every
3 * element marked server-side with data-aicl. <img> elements cannot host a
4 * pseudo-element, so they are wrapped in a positioned span. Background-image
5 * containers receive the badge directly. Re-runs on AJAX/DOM changes so
6 * Enfold "load more" thumbnails are covered too. */
7( function () {
8	'use strict';
9
10	const POS = ( window.AICL && window.AICL.position ) || 'top-left';
11	const POS_CLASS = { 'top-left': 'aicl-tl', 'top-right': 'aicl-tr', 'bottom-left': 'aicl-bl', 'bottom-right': 'aicl-br' };
12	const MAP = ( window.AICL && window.AICL.scanAll && window.AICL.map ) || null;
13	const STYLE = ( window.AICL && window.AICL.style ) || 'eu';
14	const EU_BADGE = {
15		basicAI: '',
16		trainedAlgorithmicMedia: 'Generated',
17		compositeWithTrainedAlgorithmicMedia: 'Modified'
18	};
19	const ICON_BASE = ( window.AICL && window.AICL.iconBase ) || '';
20	const ICON_VARIANT = ( window.AICL && window.AICL.iconVariant ) === 'white' ? 'white' : 'black';
21	const EU_ICON = {
22		basicAI: 'basic',
23		trainedAlgorithmicMedia: 'generated',
24		compositeWithTrainedAlgorithmicMedia: 'modified'
25	};
26
27	/**
28	 * Resolve the official EU icon file for a type and style.
29	 *
30	 * @param {string} type  - The AI content type token.
31	 * @param {string} style - The badge style ('eu' full, or 'eu_basic' compact).
32	 * @return {string} The icon filename within ICON_BASE, or '' when none applies.
33	 */
34	function euIconFile( type, style ) {
35		const name = ( style === 'eu_basic' ) ? 'basic' : ( EU_ICON[ type ] || '' );
36
37		return name ? ( name + '-' + ICON_VARIANT + '.png' ) : '';
38	}
39
40	const SHAPE = ( window.AICL && window.AICL.shape ) || 'pill';
41	const MODAL = !!( window.AICL && window.AICL.modal );
42	const MODAL_TITLE = ( window.AICL && window.AICL.modalTitle ) || '';
43	const MODAL_TEXT = ( window.AICL && window.AICL.modalText ) || '';
44	// Custom logo. Applies to the built-in styles only ('aim' / 'custom'); never to the
45	// official EU icon ('eu' / 'eu_basic'), whose mark is fixed by the disclosure standard.
46	const LOGO = ( window.AICL && window.AICL.logo ) || '';
47	// Translated on the server (badge_config); the literals below are fallbacks.
48	const TYPE_HEADING = ( window.AICL && window.AICL.typeHeadings ) || {
49		trainedAlgorithmicMedia: 'AI-generated image',
50		compositeWithTrainedAlgorithmicMedia: 'AI-edited image',
51		basicAI: 'AI content',
52		algorithmicMedia: 'Algorithmically generated image',
53		digitalCapture: 'Camera photograph',
54		digitalCreation: 'Human-created image',
55		composite: 'Composite image'
56	};
57	const TYPE_BODY = ( window.AICL && window.AICL.typeBodies ) || {
58		trainedAlgorithmicMedia: 'This image was generated by an artificial-intelligence model.',
59		compositeWithTrainedAlgorithmicMedia: 'This is a real image that was modified using artificial intelligence.',
60		basicAI: 'Artificial intelligence was involved in creating this image.',
61		algorithmicMedia: 'This image was produced by an algorithm (not AI).',
62		digitalCapture: 'This photograph was captured from real life with a camera. No AI was involved.',
63		digitalCreation: 'This image was created by a human using digital tools, without generative AI.',
64		composite: 'This image is a composite of non-AI elements, such as photographs and 3D renders.'
65	};
66	const ARIA_AI = ( window.AICL && window.AICL.ariaAi ) || 'artificially generated';
67	const ARIA_NON_AI = ( window.AICL && window.AICL.ariaNonAi ) || 'content disclosure';
68	const CLOSE_LABEL = ( window.AICL && window.AICL.closeLabel ) || 'Close';
69	const MODAL_FOOTER = ( window.AICL && window.AICL.modalFooter ) || 'Disclosed under the EU AI Act (Article 50), labelled by the site owner.';
70	const NON_AI = { algorithmicMedia: true, digitalCapture: true, digitalCreation: true, composite: true };
71	let lastFocus = null;
72
73	/**
74	 * Extracts the type token from a MAP entry.
75	 *
76	 * @param {(string|Array)} e - A MAP entry, either a legacy string or a [token, label] tuple.
77	 * @return {string} The type token, or an empty string for legacy string entries.
78	 */
79	function mapType( e ) { return ( typeof e === 'string' ) ? '' : e[ 0 ]; }
80
81	/**
82	 * Extracts the label from a MAP entry.
83	 *
84	 * @param {(string|Array)} e - A MAP entry, either a legacy string or a [token, label] tuple.
85	 * @return {string} The label text.
86	 */
87	function mapLabel( e ) { return ( typeof e === 'string' ) ? e : e[ 1 ]; }
88
89	/**
90	 * Normalizes an image URL to its base filename (strip query, hash and -WxH size suffix).
91	 *
92	 * @param {string} url - The image URL to normalize.
93	 * @return {string} The base filename without query string, hash or size suffix.
94	 */
95	function baseName( url ) {
96		if ( !url ) { return ''; }
97
98		url = url.split( '?' )[ 0 ].split( '#' )[ 0 ];
99		const f = url.substring( url.lastIndexOf( '/' ) + 1 );
100
101		return f.replace( /-\d+x\d+(\.[a-z0-9]+)$/i, '$1' );
102	}
103
104	/**
105	 * Looks a URL up in the map, preferring the most specific key the URL can supply.
106	 *
107	 * The map is keyed by the path relative to the uploads directory, which is what distinguishes
108	 * two images that share a filename in different month folders. Hand-written markup does not
109	 * always carry a resolvable path, so the bare filename is tried second, and the server drops any
110	 * filename claimed by more than one attachment rather than pick between them.
111	 *
112	 * @param {string} url - The image URL from the markup.
113	 * @return {?(string|Array)} The MAP entry, or null when nothing matches.
114	 */
115	function mapLookup( url ) {
116		if ( !MAP || !url ) { return null; }
117
118		const clean = url.split( '?' )[ 0 ].split( '#' )[ 0 ];
119		const full  = clean.replace( /-\d+x\d+(\.[a-z0-9]+)$/i, '$1' );
120		const at    = full.indexOf( '/uploads/' );
121
122		if ( at !== -1 ) {
123			const rel = full.substring( at + 9 );
124
125			if ( MAP[ rel ] ) { return MAP[ rel ]; }
126		}
127
128		return MAP[ baseName( url ) ] || null;
129	}
130
131	/**
132	 * Builds a badge element for the given content type and label.
133	 *
134	 * The official EU icons are AI-only, so non-AI (algorithmic/procedural) types
135	 * yield no badge; 'eu_basic' forces the compact mark on every AI image.
136	 *
137	 * @param {string} type  - The AI content type token used to pick the EU badge word.
138	 * @param {string} label - The fallback label text used for the non-EU badge style.
139	 * @return {?HTMLSpanElement} The badge element, or null when the type warrants no badge.
140	 */
141	function makeBadge( type, label ) {
142		let badge;
143
144		if ( STYLE === 'eu' || STYLE === 'eu_basic' || STYLE === 'aim' ) {
145			if ( !EU_BADGE.hasOwnProperty( type ) ) { return null; }
146
147			// 'aim' = built-in CSS badge (word is customisable per type via the label, minus a
148			// leading "AI "); 'eu'/'eu_basic' = the official PNG icon, whose text is fixed.
149			const word = ( STYLE === 'eu_basic' )
150				? ''
151				: ( STYLE === 'aim' )
152					? ( type === 'basicAI' ? '' : ( ( label || '' ).replace( /^ai\s+/i, '' ).trim() || EU_BADGE[ type ] ) )
153					: EU_BADGE[ type ];
154			const file = ( STYLE === 'aim' ) ? '' : euIconFile( type, STYLE );
155			badge = document.createElement( 'span' );
156			badge.className = 'aicl-badge aicl-eu-svg ' + ( POS_CLASS[ POS ] || 'aicl-tl' );
157			badge.setAttribute( 'role', 'note' );
158			badge.setAttribute( 'aria-label', word ? ( 'AI ' + word.toLowerCase() ) : ( 'AI (' + ARIA_AI + ')' ) );
159
160			if ( ICON_BASE && file ) {
161				const img = document.createElement( 'img' );
162				img.src = ICON_BASE + file;
163				img.alt = word ? ( 'AI ' + word ) : 'AI';
164				img.className = 'aicl-eu-img';
165				badge.appendChild( img );
166			} else {
167				// Fallback to the built-in mark if the icon asset is unavailable.
168				badge.className = 'aicl-badge aicl-eu ' + ( POS_CLASS[ POS ] || 'aicl-tl' ) + ( word ? '' : ' aicl-eu-basic' );
169
170				const circle = document.createElement( 'span' );
171				circle.className = 'aicl-eu-ai';
172				circle.textContent = 'AI';
173				badge.appendChild( circle );
174
175				if ( word ) {
176					const w = document.createElement( 'span' );
177					w.className = 'aicl-eu-word';
178					w.textContent = word;
179					badge.appendChild( w );
180				}
181			}
182		} else {
183			badge = document.createElement( 'span' );
184			badge.className = 'aicl-badge aicl-shape-' + SHAPE + ' ' + ( POS_CLASS[ POS ] || 'aicl-tl' );
185			badge.setAttribute( 'role', 'note' );
186			badge.textContent = label || 'AI';
187			badge.setAttribute( 'aria-label', ( label || 'AI' ) + ' (' + ( NON_AI[ type ] ? ARIA_NON_AI : ARIA_AI ) + ')' );
188		}
189
190		// Prepend the deployer's custom logo on the built-in styles (never the official EU icon).
191		if ( LOGO && STYLE !== 'eu' && STYLE !== 'eu_basic' ) {
192			const logo = document.createElement( 'img' );
193			logo.src = LOGO;
194			logo.alt = '';
195			logo.loading = 'lazy';
196			logo.className = 'aicl-badge-logo';
197			// The badge draws a small dot as its mark. A logo is a mark, so showing both puts two
198			// of them side by side, which reads as clutter rather than as a label. The class lets
199			// the stylesheet drop the dot without needing :has(), which keeps older browsers right.
200			badge.classList.add( 'aicl-has-logo' );
201			badge.insertBefore( logo, badge.firstChild );
202		}
203
204		return decorateBadge( badge, type, label );
205	}
206
207	/**
208	 * Store disclosure data on a badge and, when the click-to-disclose modal is enabled,
209	 * make it an accessible button that opens the disclosure.
210	 *
211	 * @param {HTMLSpanElement} badge - The badge element.
212	 * @param {string} type  - The source-type token.
213	 * @param {string} label - The disclosure label.
214	 * @return {HTMLSpanElement} The decorated badge.
215	 */
216	function decorateBadge( badge, type, label ) {
217		badge.setAttribute( 'data-aicl-type', type || '' );
218		badge.setAttribute( 'data-aicl-label', label || '' );
219		badge.setAttribute( 'title', label || 'AI' );
220
221		if ( MODAL ) {
222			badge.classList.add( 'aicl-clickable' );
223			badge.setAttribute( 'role', 'button' );
224			badge.setAttribute( 'tabindex', '0' );
225			badge.setAttribute( 'aria-haspopup', 'dialog' );
226			badge.addEventListener( 'click', onBadgeActivate );
227			badge.addEventListener( 'keydown', onBadgeKey );
228		}
229
230		return badge;
231	}
232
233	/**
234	 * Open the disclosure modal for the activated badge.
235	 *
236	 * @param {Event} e - The triggering event.
237	 * @return {void}
238	 */
239	function onBadgeActivate( e ) {
240		if ( e ) { e.preventDefault(); e.stopPropagation(); }
241
242		openModal( this.getAttribute( 'data-aicl-type' ) || '', this.getAttribute( 'data-aicl-label' ) || 'AI' );
243	}
244
245	/**
246	 * Activate the badge on Enter or Space for keyboard users.
247	 *
248	 * @param {KeyboardEvent} e - The key event.
249	 * @return {void}
250	 */
251	function onBadgeKey( e ) {
252		if ( 'Enter' === e.key || ' ' === e.key ) {
253			onBadgeActivate.call( this, e );
254		}
255	}
256
257	/**
258	 * Render the accessible click-to-disclose modal for an image's AI provenance.
259	 *
260	 * @param {string} type  - The source-type token.
261	 * @param {string} label - The disclosure label.
262	 * @return {void}
263	 */
264	function openModal( type, label ) {
265		closeModal();
266
267		lastFocus = document.activeElement;
268
269		const overlay = document.createElement( 'div' );
270		overlay.className = 'aicl-modal-overlay';
271		overlay.id = 'aicl-modal';
272
273		const dialog = document.createElement( 'div' );
274		dialog.className = 'aicl-modal';
275		dialog.setAttribute( 'role', 'dialog' );
276		dialog.setAttribute( 'aria-modal', 'true' );
277		dialog.setAttribute( 'aria-labelledby', 'aicl-modal-title' );
278
279		const close = document.createElement( 'button' );
280		close.type = 'button';
281		close.className = 'aicl-modal-x';
282		close.setAttribute( 'aria-label', CLOSE_LABEL );
283		close.textContent = '×';
284
285		const mark = document.createElement( 'div' );
286		mark.className = 'aicl-modal-mark';
287		mark.textContent = 'AI';
288
289		const title = document.createElement( 'h2' );
290		title.className = 'aicl-modal-title';
291		title.id = 'aicl-modal-title';
292		title.textContent = MODAL_TITLE || TYPE_HEADING[ type ] || 'AI content';
293
294		const body = document.createElement( 'div' );
295		body.className = 'aicl-modal-body';
296
297		if ( MODAL_TEXT ) {
298			// Server-sanitised with wp_kses (tight allowlist), safe to render as HTML.
299			body.innerHTML = MODAL_TEXT;
300		} else {
301			body.textContent = TYPE_BODY[ type ] || ( ( label || 'AI' ) + ' — created or altered with artificial intelligence.' );
302		}
303
304		const foot = document.createElement( 'p' );
305		foot.className = 'aicl-modal-foot';
306		foot.textContent = MODAL_FOOTER;
307
308		dialog.appendChild( close );
309		dialog.appendChild( mark );
310		dialog.appendChild( title );
311		dialog.appendChild( body );
312		dialog.appendChild( foot );
313		overlay.appendChild( dialog );
314		document.body.appendChild( overlay );
315
316		overlay.addEventListener( 'click', function ( e ) { if ( e.target === overlay ) { closeModal(); } } );
317		close.addEventListener( 'click', closeModal );
318		document.addEventListener( 'keydown', onModalKey );
319
320		close.focus();
321	}
322
323	/**
324	 * Close the disclosure modal on Escape.
325	 *
326	 * @param {KeyboardEvent} e - The key event.
327	 * @return {void}
328	 */
329	function onModalKey( e ) {
330		if ( 'Escape' === e.key ) { closeModal(); }
331	}
332
333	/**
334	 * Remove the disclosure modal and restore focus to the badge.
335	 *
336	 * @return {void}
337	 */
338	function closeModal() {
339		const m = document.getElementById( 'aicl-modal' );
340
341		if ( m ) { m.parentNode.removeChild( m ); }
342
343		document.removeEventListener( 'keydown', onModalKey );
344
345		if ( lastFocus && lastFocus.focus ) {
346			lastFocus.focus();
347			lastFocus = null;
348		}
349	}
350
351	/**
352	 * Wraps an <img> in a positioned span and appends its badge.
353	 *
354	 * @param {HTMLImageElement} img - The image element to decorate.
355	 * @return {void}
356	 */
357	function decorateImg( img ) {
358		if ( img.getAttribute( 'data-aicl-done' ) ) { return; }
359		img.setAttribute( 'data-aicl-done', '1' );
360
361		rememberOriginal( img );
362
363		const badge = makeBadge( img.getAttribute( 'data-aicl-type' ) || '', img.getAttribute( 'data-aicl-label' ) || 'AI' );
364		if ( !badge ) { return; }
365
366		img.aiclBadge = badge;
367
368		const parent = img.parentNode;
369
370		if ( parent && parent.classList && parent.classList.contains( 'aicl-wrap' ) ) {
371			parent.appendChild( badge );
372			return;
373		}
374
375		// object-fit images are sized/cropped by their container (aspect-ratio boxes,
376		// cover blocks, cropped thumbnails). Re-parenting them into a wrapper detaches
377		// them from that sizing and blows them up, so anchor the badge to the container.
378		const fit = window.getComputedStyle( img ).objectFit;
379		if ( ( fit === 'cover' || fit === 'contain' ) && parent && 1 === parent.nodeType ) {
380			if ( 'static' === window.getComputedStyle( parent ).position ) {
381				parent.style.position = 'relative';
382			}
383			parent.appendChild( badge );
384			return;
385		}
386
387		const wrap = document.createElement( 'span' );
388		wrap.className = 'aicl-wrap';
389		if ( parent ) { parent.insertBefore( wrap, img ); }
390		wrap.appendChild( img );
391		wrap.appendChild( badge );
392	}
393
394	/**
395	 * Records the verdict an image was first rendered with, so clearing a
396	 * WooCommerce variation can put it back.
397	 *
398	 * Called for every decorated image and, separately, for a variable product's
399	 * gallery image even when that image carries no disclosure at all. An empty
400	 * recorded type is meaningful: it restores "no badge".
401	 *
402	 * @param {HTMLImageElement} img - The image to remember.
403	 * @return {void}
404	 */
405	function rememberOriginal( img ) {
406		if ( img.hasAttribute( 'data-aicl-otype' ) ) { return; }
407
408		img.setAttribute( 'data-aicl-otype', img.getAttribute( 'data-aicl-type' ) || '' );
409		img.setAttribute( 'data-aicl-olabel', img.getAttribute( 'data-aicl-label' ) || '' );
410	}
411
412	/**
413	 * The gallery image WooCommerce swaps when a variation is chosen.
414	 *
415	 * Scoped to the product the form belongs to, so a page showing more than one
416	 * variable product cannot resolve to a neighbour's picture.
417	 *
418	 * @param {HTMLFormElement} form - A WooCommerce variations form.
419	 * @return {?HTMLImageElement} The main gallery image, or null.
420	 */
421	function galleryImage( form ) {
422		const scope = ( form.closest && form.closest( '.product' ) ) || document;
423		const wrap = scope.querySelector( '.woocommerce-product-gallery__image, .woocommerce-product-gallery__image--placeholder' );
424
425		return wrap ? wrap.querySelector( '.wp-post-image' ) : null;
426	}
427
428	/**
429	 * Replaces an image's disclosure with a new verdict, badge and all.
430	 *
431	 * @param {HTMLImageElement} img     - The image to re-decide.
432	 * @param {?Object}          verdict - {type, label} to apply, or a falsy value for none.
433	 * @return {void}
434	 */
435	function applyVerdict( img, verdict ) {
436		if ( !img ) { return; }
437
438		const existing = img.aiclBadge || ( img.parentNode && img.parentNode.querySelector( '.aicl-badge' ) );
439
440		if ( existing && existing.parentNode ) { existing.parentNode.removeChild( existing ); }
441
442		img.aiclBadge = null;
443		img.removeAttribute( 'data-aicl-done' );
444		img.removeAttribute( 'data-aicl' );
445		img.removeAttribute( 'data-aicl-type' );
446		img.removeAttribute( 'data-aicl-label' );
447
448		if ( !verdict || !verdict.type ) { return; }
449
450		img.setAttribute( 'data-aicl', '1' );
451		img.setAttribute( 'data-aicl-type', verdict.type );
452		img.setAttribute( 'data-aicl-label', verdict.label || 'AI' );
453		decorateImg( img );
454	}
455
456	/**
457	 * Keeps the disclosure honest across WooCommerce variation changes.
458	 *
459	 * Choosing a variation swaps the gallery picture by rewriting src on the
460	 * existing element, so the markers the server wrote for the original
461	 * attachment would otherwise stay attached to a different photograph: a real
462	 * photo wearing an "AI Generated" badge, or an AI variation wearing none.
463	 *
464	 * This listens to WooCommerce's own `found_variation` and `reset_data`
465	 * events rather than watching src. Those events carry the chosen variation,
466	 * including the verdict AICL_Frontend::variation_disclosure() attached to it,
467	 * and they fire in AJAX mode too, so products above the variation threshold
468	 * (where WooCommerce prints no inline JSON) are covered as well. Watching src
469	 * would also misfire on lazy-loading plugins, which swap a placeholder for
470	 * the real file exactly the same way.
471	 *
472	 * @return {void}
473	 */
474	function watchVariations() {
475		const $ = window.jQuery;
476		if ( !$ ) { return; }
477
478		$( 'form.variations_form' ).each( function () {
479			const form = this;
480			if ( form.getAttribute( 'data-aicl-bound' ) ) { return; }
481			form.setAttribute( 'data-aicl-bound', '1' );
482
483			const first = galleryImage( form );
484			if ( first ) { rememberOriginal( first ); }
485
486			$( form ).on( 'found_variation', function ( event, variation ) {
487				const img = galleryImage( form );
488				window.requestAnimationFrame( function () {
489					applyVerdict( img, variation && variation.aicl );
490				} );
491			} );
492
493			$( form ).on( 'reset_data', function () {
494				const img = galleryImage( form );
495				window.requestAnimationFrame( function () {
496					applyVerdict( img, img && { type: img.getAttribute( 'data-aicl-otype' ), label: img.getAttribute( 'data-aicl-olabel' ) } );
497				} );
498			} );
499		} );
500	}
501
502	/**
503	 * Marks a background-image container and appends its badge.
504	 *
505	 * @param {HTMLElement} el - The background-image container element to decorate.
506	 * @return {void}
507	 */
508	function decorateBg( el ) {
509		if ( el.getAttribute( 'data-aicl-done' ) ) { return; }
510		el.setAttribute( 'data-aicl-done', '1' );
511
512		const badge = makeBadge( el.getAttribute( 'data-aicl-type' ) || '', el.getAttribute( 'data-aicl-label' ) || 'AI' );
513		if ( !badge ) { return; }
514
515		const host = bgHost( el );
516
517		host.classList.add( 'aicl-bg' );
518		host.appendChild( badge );
519
520		if ( host !== el ) { host.setAttribute( 'data-aicl-done', '1' ); }
521	}
522
523	/**
524	 * Picks the element that should host a background badge.
525	 *
526	 * Enfold parallax sections paint the background in a transformed, oversized
527	 * layer (.av-parallax-inner) that forms its own stacking context beneath the
528	 * section colour overlay, so a badge placed there is pushed off-screen and
529	 * hidden behind the overlay. Anchor the badge to the visible section instead.
530	 *
531	 * @param {HTMLElement} el - The element carrying the background image.
532	 * @return {HTMLElement} The element the badge should be appended to.
533	 */
534	function bgHost( el ) {
535		if ( el.closest( '.av-parallax' ) ) {
536			const section = el.closest( '.avia-section' );
537			if ( section ) { return section; }
538		}
539
540		return el;
541	}
542
543	const BG_SEL = 'div,section,a,article,aside,header,figure,li';
544	let lastBgScan = 0;
545
546	/**
547	 * Cheap pass: badge any <img> whose file matches a flagged attachment.
548	 *
549	 * @param {(Document|HTMLElement)} root - The subtree to scan for matching images.
550	 * @return {void}
551	 */
552	function scanImgs( root ) {
553		const imgs = root.querySelectorAll( 'img:not([data-aicl-done]):not([data-aicl])' );
554
555		for ( let i = 0; i < imgs.length; i++ ) {
556			const entry = mapLookup( imgs[ i ].getAttribute( 'src' ) );
557			if ( entry ) {
558				imgs[ i ].setAttribute( 'data-aicl-type', mapType( entry ) );
559				imgs[ i ].setAttribute( 'data-aicl-label', mapLabel( entry ) );
560				decorateImg( imgs[ i ] );
561			}
562		}
563	}
564
565	/**
566	 * Heavier pass: read the computed background-image of candidate containers and
567	 * badge those whose file matches a flagged attachment. Reading the computed
568	 * style catches CSS-class / <style>-block backgrounds, not just inline ones;
569	 * the read pass runs before any DOM writes to avoid layout thrash.
570	 *
571	 * @param {(Document|HTMLElement)} root - The subtree to scan for matching backgrounds.
572	 * @return {void}
573	 */
574	function scanBackgrounds( root ) {
575		const els = root.querySelectorAll( BG_SEL + ':not([data-aicl-done]):not([data-aicl])' );
576		const hits = [];
577		const max = Math.min( els.length, 4000 );
578
579		for ( let j = 0; j < max; j++ ) {
580			const el = els[ j ];
581			let bg = el.style.backgroundImage;
582			if ( !bg || bg.indexOf( 'url(' ) === -1 ) {
583				const cs = window.getComputedStyle( el );
584				if ( cs.display === 'none' ) { continue; }
585				bg = cs.backgroundImage;
586			}
587			if ( !bg || bg.indexOf( 'url(' ) === -1 ) { continue; }
588
589			const m = bg.match( /url\((['"]?)([^'")]+)\1\)/ );
590			if ( !m ) { continue; }
591
592			const bentry = mapLookup( m[ 2 ] );
593			if ( bentry ) { hits.push( [ el, bentry ] ); }
594		}
595
596		for ( let k = 0; k < hits.length; k++ ) {
597			hits[ k ][ 0 ].setAttribute( 'data-aicl-type', mapType( hits[ k ][ 1 ] ) );
598			hits[ k ][ 0 ].setAttribute( 'data-aicl-label', mapLabel( hits[ k ][ 1 ] ) );
599			decorateBg( hits[ k ][ 0 ] );
600		}
601	}
602
603	/**
604	 * Universal scan: badge any <img>/background whose file matches a flagged attachment.
605	 *
606	 * @param {(Document|HTMLElement)} root - The subtree to scan.
607	 * @return {void}
608	 */
609	function scanAll( root ) {
610		if ( !MAP ) { return; }
611
612		scanImgs( root );
613
614		const now = Date.now();
615		if ( now - lastBgScan > 800 ) {
616			lastBgScan = now;
617			scanBackgrounds( root );
618		}
619	}
620
621	/**
622	 * Decorates all server-flagged images and backgrounds in a subtree, then runs the universal scan.
623	 *
624	 * @param {(Document|HTMLElement)} [root] - The subtree to process; defaults to document.
625	 * @return {void}
626	 */
627	function run( root ) {
628		root = root || document;
629
630		const imgs = root.querySelectorAll( 'img[data-aicl="1"]:not([data-aicl-done])' );
631		for ( let i = 0; i < imgs.length; i++ ) { decorateImg( imgs[ i ] ); }
632
633		const bgs = root.querySelectorAll( '[data-aicl="1"]:not(img):not([data-aicl-done])' );
634		for ( let j = 0; j < bgs.length; j++ ) { decorateBg( bgs[ j ] ); }
635
636		scanAll( root );
637	}
638
639	if ( document.readyState === 'loading' ) {
640		document.addEventListener( 'DOMContentLoaded', function () { run(); watchVariations(); } );
641	} else {
642		run();
643		watchVariations();
644	}
645	/* Bound again on load because jQuery, which WooCommerce's variation form
646	   needs, may not have been present the first time. watchVariations() marks
647	   each form it has bound, so this cannot double-subscribe. */
648	window.addEventListener( 'load', function () { run(); watchVariations(); } );
649
650	if ( 'MutationObserver' in window ) {
651		let pending = false;
652		const mo = new MutationObserver( function () {
653			if ( pending ) { return; }
654			pending = true;
655			window.requestAnimationFrame( function () { pending = false; run(); } );
656		} );
657		mo.observe( document.body || document.documentElement, { childList: true, subtree: true } );
658	}
659}() );

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.