1/* global newspackNewzware */ 2( function ( $ ) { 3 'use strict'; 4 5 // Storage key constants 6 const KEYS = { 7 USER: 'np_newzware_user', 8 LAST_REFRESH: 'np_newzware_last_refresh', 9 FIRST_VISIT_IN_MONTH: 'np_newzware_first_visit_in_month', 10 ARTICLES_READ: 'np_newzware_articles_read', 11 }; 12 13 const COOKIE_NAME = 'np_newzware_hash'; 14 15 // Regex patterns to match reader subdomain iframes 16 const READER_IFRAME_PATTERNS = [ 17 /^https?:\/\/reader\./, // Matches URLs like: https://reader.example.com 18 ]; 19 20 const TIMEOUTS = { 21 LOGIN: 20000, 22 REFRESH_TOKEN: 15000, 23 }; 24 25 const ONE_DAY_MS = 24 * 60 * 60 * 1000; 26 27 const HASH_LOGIN_MODAL = '#newzware-login'; 28 29 let isModalOpen = false; 30 31 const NewspackNewzware = { 32 init() { 33 this.updateLoginState(); 34 this.bindEvents(); 35 const isAutoLogin = this.checkForAutoLogin(); 36 this.checkForLoginModal(); 37 38 // Only init paywall if we're not doing auto-login 39 if ( ! isAutoLogin ) { 40 this.initPaywall(); 41 } 42 43 this.checkTokenRefresh(); 44 }, 45 46 bindEvents() { 47 const self = this; 48 $( document ).on( 'click', '.newspack-newzware-modal-close', this.closeModal ); 49 $( document ).on( 'submit', '#newspack-newzware-login-form', function ( e ) { 50 self.handleLogin.call( self, e ); 51 } ); 52 $( document ).on( 'keydown', function ( e ) { 53 if ( e.key === 'Escape' && isModalOpen ) { 54 NewspackNewzware.closeModal(); 55 } 56 } ); 57 // Bind click events to any links with the login modal hash 58 $( document ).on( 'click', 'a[href$="#newzware-login"]', function ( e ) { 59 e.preventDefault(); 60 self.openModal(); 61 self.cleanupUrl(); 62 } ); 63 // Bind click events to logout elements 64 $( document ).on( 'click', '.newzware-logout', function ( e ) { 65 e.preventDefault(); 66 self.applyLoggedOutState(); 67 } ); 68 }, 69 70 checkForAutoLogin() { 71 const urlParams = new URLSearchParams( window.location.search ); 72 const loginId = urlParams.get( 'login_id' ); 73 const hash = urlParams.get( 'hash' ); 74 75 if ( loginId && hash ) { 76 // Use existing authentication method with hash instead of password 77 this.authenticate( loginId, hash, true ); 78 // Clean up URL parameters after processing 79 this.cleanupUrl(); 80 return true; 81 } 82 return false; 83 }, 84 85 checkForLoginModal() { 86 if ( HASH_LOGIN_MODAL === window.location.hash ) { 87 this.openModal(); 88 this.cleanupUrl(); 89 } 90 }, 91 92 cleanupUrl() { 93 const PARAMS_TO_CLEAN = [ 'login_id', 'hash' ]; 94 const HASHES_TO_CLEAN = [ HASH_LOGIN_MODAL ]; 95 const { history, location } = window; 96 const hasTargetParams = !! location.search && PARAMS_TO_CLEAN.some( p => location.search.includes( `${ p }=` ) ); 97 const hasTargetHash = HASHES_TO_CLEAN.includes( location.hash ); 98 if ( ! history || typeof history.replaceState !== 'function' || ( ! hasTargetParams && ! hasTargetHash ) ) { 99 return; 100 } 101 102 // Use the URL API for robust manipulation and fewer string ops. 103 const url = new URL( location.href ); 104 const params = url.searchParams; 105 let hasChanges = false; 106 107 PARAMS_TO_CLEAN.forEach( key => { 108 if ( params.has( key ) ) { 109 params.delete( key ); 110 hasChanges = true; 111 } 112 } ); 113 114 if ( HASHES_TO_CLEAN.includes( url.hash ) ) { 115 url.hash = ''; 116 hasChanges = true; 117 } 118 119 if ( hasChanges ) { 120 const query = params.toString(); 121 // Use a relative URL (same-origin) to avoid unnecessary origin repetition. 122 history.replaceState( null, '', url.pathname + ( query ? '?' + query : '' ) + url.hash ); 123 } 124 }, 125 126 openModal() { 127 this.closeOpenMenus(); 128 $( '#newspack-newzware-login-modal' ).show().attr( 'data-state', 'open' ); 129 $( 'body' ).addClass( 'newspack-newzware-modal-open' ); 130 isModalOpen = true; 131 $( '#newspack-newzware-email' ).focus(); 132 }, 133 134 closeModal() { 135 $( '#newspack-newzware-login-modal' ).attr( 'data-state', 'closed' ).hide(); 136 $( 'body' ).removeClass( 'newspack-newzware-modal-open' ); 137 isModalOpen = false; 138 $( '#newspack-newzware-login-message' ).empty(); 139 }, 140 141 closeOpenMenus() { 142 const $body = $( 'body' ); 143 [ 'desktop', 'mobile' ].forEach( type => { 144 if ( $body.hasClass( `${ type }-menu-opened` ) ) { 145 const $btn = $( `#${ type }-sidebar-fallback .${ type }-menu-toggle` ); 146 if ( $btn.length ) { 147 $btn.trigger( 'click' ); 148 } 149 } 150 } ); 151 }, 152 153 showLoginMessage( type, message ) { 154 $( '#newspack-newzware-login-message' ).removeClass( 'success error warning' ).addClass( type ).html( `<p>${ message }</p>` ); 155 }, 156 157 getNonce() { 158 return newspackNewzware.nonce || ( window.wpApiSettings && window.wpApiSettings.nonce ) || ''; 159 }, 160 161 handleLogin( e ) { 162 e.preventDefault(); 163 164 // Prevent double submissions 165 if ( $( e.currentTarget ).hasClass( 'submitting' ) ) { 166 return; 167 } 168 169 this.showLoginMessage( '', '' ); 170 171 const email = $( '#newspack-newzware-email' ).val().trim(); 172 const password = $( '#newspack-newzware-password' ).val(); 173 174 // Client-side validation 175 if ( ! email || ! password ) { 176 this.showLoginMessage( 'error', 'Please enter both email and password.' ); 177 return; 178 } 179 180 if ( ! this.isValidEmail( email ) ) { 181 this.showLoginMessage( 'error', 'Please enter a valid email address.' ); 182 return; 183 } 184 185 // Use the reusable authenticate method 186 this.authenticate( email, password, false ); 187 }, 188 189 authenticate( email, credential, isHashAuth = false ) { 190 const form = $( '#newspack-newzware-login-form' ); 191 const submitBtn = $( '#newspack-newzware-login-form button[type="submit"]' ); 192 193 // Set loading state only for manual login 194 if ( ! isHashAuth ) { 195 form.addClass( 'submitting' ); 196 submitBtn.prop( 'disabled', true ); 197 this.updateSubmitButton( submitBtn, 'loading' ); 198 } 199 200 // Get nonce using helper 201 const nonce = this.getNonce(); 202 203 const requestData = { 204 email, 205 }; 206
207 // Add password or hash depending on authentication type 208 requestData[ isHashAuth ? 'hash' : 'password' ] = credential; 209 210 // Add nonce to data if no header support 211 if ( nonce ) { 212 requestData._wpnonce = nonce; 213 } 214 215 $.ajax( { 216 url: newspackNewzware.rest_url + 'login', 217 method: 'POST', 218 data: requestData, 219 timeout: TIMEOUTS.LOGIN, 220 beforeSend( xhr ) { 221 if ( nonce ) { 222 xhr.setRequestHeader( 'X-WP-Nonce', nonce ); 223 } 224 }, 225 } ) 226 .done( response => { 227 if ( response.success && response.data ) { 228 this.applyLoggedInState( response.data ); 229 this.setStorageItem( KEYS.LAST_REFRESH, Date.now().toString() ); 230 231 if ( ! isHashAuth ) { 232 this.showLoginMessage( 'success', 'Login successful!' ); 233 this.updateSubmitButton( submitBtn, 'success' ); 234 setTimeout( () => { 235 this.closeModal(); 236 }, 1500 ); 237 } 238 } else { 239 // Login failed due to invalid credentials - reset to logged-out state 240 this.applyLoggedOutState(); 241 242 if ( ! isHashAuth ) { 243 this.showLoginMessage( 'error', 'Login failed. Please check your credentials.' ); 244 this.updateSubmitButton( submitBtn, 'error' ); 245 } else { 246 // Auto-login failed - show login modal as fallback 247 this.openModal(); 248 } 249 } 250 } ) 251 .fail( xhr => { 252 // Network/server error - reset to logged-out state 253 this.applyLoggedOutState(); 254 255 if ( ! isHashAuth ) { 256 let errorMessage = 'Login failed. Please try again.'; 257 if ( xhr.responseJSON && xhr.responseJSON.message ) { 258 errorMessage = xhr.responseJSON.message; 259 } 260 this.showLoginMessage( 'error', errorMessage ); 261 this.updateSubmitButton( submitBtn, 'error' ); 262 } else { 263 // eslint-disable-next-line no-console 264 console.warn( 'Auto-login with hash failed:', xhr.status ); 265 // Auto-login failed - show login modal as fallback 266 this.openModal(); 267 } 268 } ) 269 .always( () => { 270 if ( ! isHashAuth ) { 271 form.removeClass( 'submitting' ); 272 setTimeout( () => { 273 submitBtn.prop( 'disabled', false ); 274 this.updateSubmitButton( submitBtn, 'default' ); 275 }, 2000 ); 276 } 277 } ); 278 }, 279 280 refreshPaywallState() { 281 this.removePaywall(); 282 this.initPaywall(); 283 }, 284 285 isValidEmail( email ) { 286 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 287 return emailRegex.test( email ); 288 }, 289 290 updateSubmitButton( button, state ) { 291 const states = { 292 default: 'Login', 293 loading: 'Logging in...', 294 success: 'Success!', 295 error: 'Try Again', 296 }; 297 298 button.text( states[ state ] || states.default ); 299 button.removeClass( 'loading success error' ).addClass( state ); 300 }, 301 302 applyLoggedOutState() { 303 this.clearUserData(); 304 this.updateLoginState(); 305 this.refreshPaywallState(); 306 this.reloadReaderIframes(); 307 }, 308 309 applyLoggedInState( userData ) { 310 this.storeUserData( userData ); 311 this.updateLoginState(); 312 this.refreshPaywallState(); 313 this.reloadReaderIframes(); 314 }, 315 316 updateLoginState() { 317 $( 'body' ).toggleClass( 'newspack-newzware-logged-in', !! this.getUserData() ); 318 }, 319 320 clearUserData() { 321 this.removeStorageItem( KEYS.USER ); 322 this.removeStorageItem( KEYS.LAST_REFRESH ); 323 this.removeNewzwareHashCookie(); 324 }, 325 326 storeUserData( data ) { 327 const userData = { 328 login_id: data.login_id, 329 hash: data.hash, 330 subscription: data.subscription, 331 login_time: Date.now(), 332 }; 333 this.setStorageItem( KEYS.USER, JSON.stringify( userData ) ); 334 this.setNewzwareHashCookie( userData ); 335 }, 336 337 getUserData() { 338 const userData = this.getStorageItem( KEYS.USER ); 339 try { 340 return userData ? JSON.parse( userData ) : null; 341 } catch ( e ) { 342 // eslint-disable-next-line no-console 343 console.error( 'Error parsing user data from storage:', e ); 344 this.removeStorageItem( KEYS.USER ); 345 return null; 346 } 347 }, 348 349 // Storage methods with fallback handling 350 setStorageItem( key, value ) { 351 try { 352 if ( this.isLocalStorageAvailable() ) { 353 window.localStorage.setItem( key, value ); 354 } else { 355 // Fallback to sessionStorage 356 window.sessionStorage.setItem( key, value ); 357 } 358 } catch ( e ) { 359 // eslint-disable-next-line no-console 360 console.warn( 'Storage not available, using in-memory fallback:', e ); 361 this.memoryStorage = this.memoryStorage || {}; 362 this.memoryStorage[ key ] = value; 363 } 364 }, 365 366 getStorageItem( key ) { 367 try { 368 if ( this.isLocalStorageAvailable() ) { 369 return window.localStorage.getItem( key ); 370 } 371 return window.sessionStorage.getItem( key ); 372 } catch ( e ) { 373 // eslint-disable-next-line no-console 374 console.warn( 'Storage not available, using in-memory fallback:', e ); 375 return this.memoryStorage ? this.memoryStorage[ key ] : null; 376 } 377 }, 378 379 removeStorageItem( key ) { 380 try { 381 if ( this.isLocalStorageAvailable() ) { 382 window.localStorage.removeItem( key ); 383 } else { 384 window.sessionStorage.removeItem( key ); 385 } 386 } catch ( e ) { 387 if ( this.memoryStorage ) { 388 delete this.memoryStorage[ key ]; 389 } 390 } 391 }, 392 393 isLocalStorageAvailable() { 394 try { 395 const test = '__localStorage_test__'; 396 window.localStorage.setItem( test, test ); 397 window.localStorage.removeItem( test ); 398 return true; 399 } catch ( e ) { 400 return false;
401 } 402 }, 403 404 getRootDomain() { 405 const hostname = window.location.hostname; 406 const parts = hostname.split( '.' ); 407 408 // Get root domain for cookie (e.g., from www.example.com or example.com get .example.com) 409 return parts.length >= 2 ? '.' + parts.slice( -2 ).join( '.' ) : hostname; 410 }, 411 412 setNewzwareHashCookie( userData ) { 413 if ( ! userData ) { 414 return; 415 } 416 417 // Set cookie with 24 hour expiration (matches token refresh cycle) 418 const expirationDate = new Date(); 419 expirationDate.setTime( expirationDate.getTime() + ONE_DAY_MS ); 420 421 let serializedUserData = ''; 422 try { 423 serializedUserData = encodeURIComponent( JSON.stringify( userData ) ); 424 } catch ( e ) { 425 // eslint-disable-next-line no-console 426 console.warn( 'Unable to serialize user data for cookie:', e ); 427 return; 428 } 429 430 document.cookie = 431 COOKIE_NAME + 432 '=' + 433 serializedUserData + 434 '; expires=' + 435 expirationDate.toUTCString() + 436 '; domain=' + 437 this.getRootDomain() + 438 '; path=/'; 439 }, 440 441 removeNewzwareHashCookie() { 442 // Set expiration to past date to delete cookie 443 document.cookie = COOKIE_NAME + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=' + this.getRootDomain() + '; path=/;'; 444 }, 445 446 detectReaderIframes() { 447 const iframes = document.querySelectorAll( 'iframe' ); 448 const readerIframes = []; 449 450 iframes.forEach( iframe => { 451 const src = iframe.getAttribute( 'src' ); 452 if ( src && READER_IFRAME_PATTERNS.some( pattern => pattern.test( src ) ) ) { 453 readerIframes.push( iframe ); 454 } 455 } ); 456 457 return readerIframes; 458 }, 459 460 reloadReaderIframes() { 461 const readerIframes = this.detectReaderIframes(); 462 463 if ( readerIframes.length ) { 464 readerIframes.forEach( iframe => { 465 const currentSrc = iframe.getAttribute( 'src' ); 466 if ( currentSrc ) { 467 // Reload by reassigning the src (browser will fetch with updated cookies) 468 iframe.src = currentSrc; 469 } 470 } ); 471 } 472 }, 473 474 isUserLoggedIn() {
475 return !! this.getUserData()?.login_id; 476 }, 477 478 hasActiveSubscription() { 479 return 'active' === this.getUserData()?.subscription; 480 }, 481 482 isRegisteredUser() { 483 return 'none' === this.getUserData()?.subscription; 484 }, 485 486 checkTokenRefresh() { 487 if ( ! this.isUserLoggedIn() ) { 488 return; 489 } 490 491 const userData = this.getUserData(); 492 if ( ! userData ) { 493 return; 494 } 495 496 const lastRefresh = this.getStorageItem( KEYS.LAST_REFRESH ); 497 const now = Date.now(); 498 499 if ( ! lastRefresh || now - parseInt( lastRefresh ) > ONE_DAY_MS ) { 500 this.refreshToken( userData ); 501 } 502 }, 503 504 refreshToken( userData ) { 505 // Get nonce using helper 506 const nonce = this.getNonce(); 507 508 const requestData = { 509 login_id: userData.login_id, 510 old_hash: userData.hash, 511 }; 512 513 // Add nonce to data if no header support 514 if ( nonce ) { 515 requestData._wpnonce = nonce; 516 } 517 518 $.ajax( { 519 url: newspackNewzware.rest_url + 'refresh-token', 520 method: 'POST', 521 data: requestData, 522 timeout: TIMEOUTS.REFRESH_TOKEN, 523 beforeSend( xhr ) { 524 if ( nonce ) { 525 xhr.setRequestHeader( 'X-WP-Nonce', nonce ); 526 } 527 }, 528 } ) 529 .done( response => { 530 if ( response.success && response.data ) { 531 const currentUser = this.getUserData(); 532 this.storeUserData( { 533 login_id: response.data?.login_id, 534 hash: response.data?.hash, 535 subscription: currentUser?.subscription, 536 } ); 537 this.setStorageItem( KEYS.LAST_REFRESH, Date.now().toString() ); 538 } 539 } ) 540 .fail( ( xhr, status ) => { 541 // eslint-disable-next-line no-console 542 console.warn( 'Token refresh failed:', status ); 543 // Only clear user data if it's a definitive failure (not network issues) 544 if ( xhr.status === 401 || xhr.status === 403 ) { 545 this.applyLoggedOutState(); 546 } 547 } ); 548 }, 549 550 initPaywall() { 551 if ( newspackNewzware.is_staff || newspackNewzware.is_allowed_ip || ! newspackNewzware.is_premium_post ) { 552 return; 553 } 554 555 // Check if user has active subscription - they can read everything 556 if ( this.hasActiveSubscription() ) { 557 return; 558 } 559 560 // Handle monthly article count reset 561 const firstVisitInMonthTimestamp = parseInt( this.getStorageItem( KEYS.FIRST_VISIT_IN_MONTH ), 10 ); 562 563 if ( isNaN( firstVisitInMonthTimestamp ) ) { 564 this.setStorageItem( KEYS.FIRST_VISIT_IN_MONTH, Date.now().toString() ); 565 } else { 566 const firstVisitInMonthDate = new Date( firstVisitInMonthTimestamp ); 567 const nowDate = new Date(); 568 569 const isNewMonth = 570 nowDate.getFullYear() !== firstVisitInMonthDate.getFullYear() || nowDate.getMonth() !== firstVisitInMonthDate.getMonth(); 571 572 if ( isNewMonth ) { 573 this.removeStorageItem( KEYS.ARTICLES_READ ); 574 this.setStorageItem( KEYS.FIRST_VISIT_IN_MONTH, Date.now().toString() ); 575 } 576 } 577 578 const currentArticle = document.location.pathname; 579 let articlesRead = this.getStorageItem( KEYS.ARTICLES_READ ); 580 581 if ( ! articlesRead ) { 582 articlesRead = [ currentArticle ]; 583 } else { 584 try { 585 articlesRead = articlesRead.split( ',' ); 586 if ( ! articlesRead.includes( currentArticle ) ) { 587 articlesRead.push( currentArticle ); 588 } 589 } catch ( e ) { 590 // eslint-disable-next-line no-console 591 console.error( 'Error parsing articles read:', e ); 592 articlesRead = [ currentArticle ]; 593 } 594 } 595 596 this.setStorageItem( KEYS.ARTICLES_READ, articlesRead.join( ',' ) ); 597 598 // Determine limit based on user status 599 let freeViewsLimit = parseInt( newspackNewzware.num_free_views ) || 0; 600 if ( this.isRegisteredUser() ) { 601 freeViewsLimit += parseInt( newspackNewzware.num_reg_views ) || 0; 602 } 603 604 if ( articlesRead.length > freeViewsLimit ) { 605 this.showPaywall(); 606 } 607 }, 608 609 showPaywall() { 610 const articleContent = this.findArticleContent(); 611 if ( articleContent ) { 612 // Store original content before replacing it 613 if ( ! articleContent.dataset.originalContent ) { 614 articleContent.dataset.originalContent = articleContent.innerHTML; 615 } 616 617 // Headline area: the configured Paywall Page content overrides the 618 // Headline and Price Line settings. 619 let headline = ''; 620 if ( newspackNewzware.paywall_content ) { 621 headline = `<div class="newspack-newzware-paywall-headline">${ newspackNewzware.paywall_content }</div>`; 622 } else { 623 if ( newspackNewzware.heading_text ) { 624 headline += `<h3 class="newspack-newzware-paywall-heading">${ newspackNewzware.heading_text }</h3>`; 625 } 626 if ( newspackNewzware.price_text ) { 627 headline += `<p class="newspack-newzware-paywall-price">${ newspackNewzware.price_text }</p>`; 628 } 629 } 630 631 // Build the subscribe button (only when a subscribe URL is configured). 632 let subscribeRow = ''; 633 if ( newspackNewzware.subscribe_url ) { 634 // Replace {CURRENT_URL} placeholder with the current page URL. 635 const subscribeUrl = newspackNewzware.subscribe_url.replace( '{CURRENT_URL}', encodeURIComponent( window.location.href ) ); 636 const subscribeText = newspackNewzware.subscribe_btn_text || 'Subscribe';
637 subscribeRow = ` 638 <p class="newspack-newzware-paywall-cta"> 639 <a href="${ subscribeUrl }" class="newspack-newzware-paywall-button newspack-newzware-subscribe-link">${ subscribeText }</a> 640 </p> 641 `; 642 } 643 644 // Build the secondary line (only when its URL is configured). 645 let secondaryLine = ''; 646 if ( newspackNewzware.secondary_url ) { 647 secondaryLine = ` 648 <p class="newspack-newzware-paywall-secondary"> 649 ${ newspackNewzware.secondary_text } 650 <a href="${ newspackNewzware.secondary_url }" class="newspack-newzware-paywall-link">${ newspackNewzware.secondary_link_text }</a>. 651 </p> 652 `; 653 } 654 655 // Build the sign-in line. The button reuses the login-modal trigger class. 656 const signinLine = ` 657 <p class="newspack-newzware-paywall-signin"> 658 ${ newspackNewzware.signin_text } 659 <button type="button" class="newspack-newzware-paywall-link newspack-newzware-login-link">${ newspackNewzware.signin_link_text }</button> 660 </p> 661 `; 662 663 // Build disclaimers if provided. 664 let disclaimers = ''; 665 if ( newspackNewzware.disclaimers ) { 666 disclaimers = ` 667 <div class="newspack-newzware-paywall-disclaimers"> 668 <p>${ newspackNewzware.disclaimers }</p> 669 </div> 670 `; 671 } 672 673 const paywalledContent = ` 674 <div class="newspack-newzware-paywall-block"> 675 ${ headline } 676 ${ subscribeRow } 677 ${ secondaryLine } 678 ${ signinLine } 679 ${ disclaimers } 680 </div> 681 `; 682 articleContent.innerHTML = paywalledContent; 683 684 // Use event delegation to avoid multiple bindings 685 $( document ).off( 'click.newspack-newzware', '.newspack-newzware-login-link' ); 686 $( document ).on( 'click.newspack-newzware', '.newspack-newzware-login-link', e => { 687 e.preventDefault(); 688 this.openModal(); 689 } ); 690 } else { 691 // eslint-disable-next-line no-console 692 console.warn( 'Newspack Newzware: Could not find article content to replace with paywall' ); 693 } 694 }, 695 696 removePaywall() { 697 const articleContent = this.findArticleContent(); 698 if ( articleContent && articleContent.dataset.originalContent ) { 699 // Restore original content 700 articleContent.innerHTML = articleContent.dataset.originalContent; 701 // Clean up the stored data 702 delete articleContent.dataset.originalContent; 703 } else { 704 // Fallback: just remove paywall block 705 $( '.newspack-newzware-paywall-block' ).remove(); 706 } 707 }, 708 709 findArticleContent() { 710 // List of selectors in order of preference - more specific first 711 const selectors = [ 712 'article .entry-content', 713 '.hentry .entry-content', 714 '.post .entry-content', 715 '.single .entry-content', 716 'main .entry-content', 717 '.content-area .entry-content', 718 '.site-main .entry-content', 719 // Fallback selectors 720 '.entry-content', 721 'article .content', 722 '.post-content', 723 '.article-content', 724 '.single-content', 725 'main .content', 726 '.content', 727 // Last resort - very generic 728 'main article', 729 'main .post', 730 '.main-content', 731 'main', 732 ]; 733 734 for ( let i = 0; i < selectors.length; i++ ) { 735 const element = document.querySelector( selectors[ i ] ); 736 if ( element && element.innerHTML && element.innerHTML.trim().length > 0 ) { 737 return element; 738 } 739 } 740 741 // eslint-disable-next-line no-console 742 console.warn( 'Newspack Newzware: Could not find suitable article content element' ); 743 return null; 744 }, 745 }; 746 747 $( document ).ready( function () { 748 NewspackNewzware.init(); 749 } ); 750 751 window.NewspackNewzware = NewspackNewzware; 752} )( window.jQuery );
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.