PageSourceSearch

https://www.concordmonitor.com/wp-content/plugins/newspack-newzware/assets/js/newspack-newzware.js?ver=1.5.0

js concordmonitor.com collected 2026-09-24 08:37:22 UTC 21,746 bytes, 752 lines download raw bytes

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.