/** * Internal dependencies */ import { domReady } from './dom-ready'; import { isAlternativeDistributionAllowed } from './is-alternative-distribution-allowed'; import { lcm } from './lcm'; import { addNabQueryArg } from '../url'; import type { PreloadQueryArgUrl, Session } from '../../types'; export const preloadQueryArgs = ( session: Session ): void => domReady( () => { doPreloadQueryArgs( session ); fixSelfHashUrls(); } ); // ======= // HELPERS // ======= export const doPreloadQueryArgs = ( session: Session ): void => { if ( ! session.preloadQueryArgUrls.length ) { return; } const site = document.location.protocol + '//' + document.location.hostname; const anchors = Array.from( document.querySelectorAll< HTMLAnchorElement >( 'a' ) ) .filter( ( a ) => a.href ) .filter( ( a ) => typeof a.href === 'string' ) .filter( ( a ) => 0 === a.href.indexOf( site ) ) .filter( ( a ) => -1 === a.href.indexOf( '#' ) ) .filter( ( a ) => -1 === a.href.indexOf( '/wp-content/' ) ); anchors.forEach( preloadQueryArg( session ) ); }; const fixSelfHashUrls = () => { const current = new URL( document.location.href ); if ( ! /\bnab=/.test( current.search ) ) { return; } current.hash = ''; current.searchParams.delete( 'nab' ); Array.from( document.querySelectorAll< HTMLAnchorElement >( 'a[href*="#"]' ) ) .filter( ( a ) => a.href ) .filter( ( a ) => typeof a.href === 'string' ) .filter( ( a ) => a.href.replace( /#.*$/, '' ) === current.href ) .forEach( ( a ) => { a.href = a.href.replace( /^[^#]*#/, '#' ); } ); }; function preloadQueryArg( session: Session ) { const config = [ ...session.preloadQueryArgUrls ].sort( ( x ) => x.type === 'alt-urls' ? -1 : 1 ); return ( a: HTMLAnchorElement ): void => { if ( ! a.href || typeof a.href !== 'string' ) { return; } if ( a.href.indexOf( 'nab=' ) !== -1 ) { return; } const scopes = getMatchingScopes( config, a.href ); if ( ! scopes.length ) { return; } // eslint-disable-next-line array-callback-return const altCounts = scopes.map( ( s ): number => { switch ( s.type ) { case 'alt-urls': return ( session.experiments.find( ( e ) => e.id === s.experimentId )?.alternatives.length ?? 1 ); case 'scope': return s.altCount; } } ); const altCount = altCounts.reduce( lcm, 1 ); const nab = isAlternativeDistributionAllowed( session ) ? session.alternative : session.alternative % altCount; const url = getAlternativeUrl( session, scopes ) || a.href; a.href = addNabQueryArg( url, nab, session.nabPosition ); }; } function getMatchingScopes( config: ReadonlyArray< PreloadQueryArgUrl >, link: string ): ReadonlyArray< PreloadQueryArgUrl > { link = link.toLowerCase(); return config.filter( ( s ) => { const scope = s.type === 'alt-urls' ? s.altUrls : s.scope; return scope.some( ( l ): boolean => { if ( isPartialUrl( l ) ) { return link.includes( getPart( l ) ); } if ( isPartialNotIncludedUrl( l ) ) { return ! link.includes( getPart( l ) ); } if ( isExclusionUrl( l ) ) { return l.substring( 1 ) !== link; } return link === l; } ); } ); } function getAlternativeUrl( session: Session, config: ReadonlyArray< PreloadQueryArgUrl > ) { const scope = config.find( ( s ) => s.type === 'alt-urls' ); if ( ! scope || scope.type !== 'alt-urls' ) { return undefined; } const experiment = session.experiments.find( ( e ) => e.id === scope.experimentId ); if ( ! experiment ) { return undefined; } return scope.altUrls[ experiment.alternative ]; } function isExclusionUrl( url: string ): boolean { return url.startsWith( '!' ) && ! url.startsWith( '!*' ); } function isPartialUrl( url: string ): boolean { return url.startsWith( '*' ) && url.endsWith( '*' ); } function isPartialNotIncludedUrl( url: string ): boolean { return url.startsWith( '!*' ) && url.endsWith( '*' ); } function getPart( partialUrl: string ): string { return partialUrl.substring( // We either remove '!*' or '*' at the beginning isPartialNotIncludedUrl( partialUrl ) ? 2 : 1, // And we remove the final '*' at the end partialUrl.length - 1 ); }