1/** 2 * Stellar 3 * Stellar is star rating system helps you rate and like articles on a website and keep track of results. 4 * Exclusively on https://1.envato.market/stellar 5 * 6 * @encoding UTF-8 7 * @version 2.1.4 8 * @copyright (C) 2018 - 2021 Merkulove ( https://merkulov.design/ ). All rights reserved. 9 * @license Envato License https://1.envato.market/KYbje 10 * @contributors Nemirovskiy Vitaliy ([email protected]), Dmitry Merkulov ([email protected]) 11 * @support [email protected] 12 **/ 13 14( function () { 15 16 "use strict"; 17 18 document.addEventListener( "DOMContentLoaded", function() { 19 20 /** If there is no Stellar on this page then exit. */ 21 if ( document.querySelectorAll( '.mdp-stellar-box' ).length < 1 ) { return; } 22 23 /** Get WordPress post ID */ 24 const id = document.querySelector('.mdp-stellar-box').id.replace('mdp-stellar-', '' ); // Page/Post id 25 26 /** Flag to identify users voting for the first time. */ 27 let fNew = 1; 28 29 /** Previous values of the user's stellar. */ 30 let stellarData = prepareStellarData(); 31 32 // noinspection JSUnresolvedVariable 33 /** Data from WP: ajax url and timestamp of last reset. */ 34 let wpData = window.mdpStellar; 35 36 /** Remove all stellarData if they was created before last reset. */ 37 // noinspection JSUnresolvedVariable 38 if ( wpData.reset_timestamp !== undefined) { 39 40 // noinspection JSUnresolvedVariable 41 if ( stellarData.timestamp < wpData.reset_timestamp ) { 42 43 /** Recreate stellarData, like for first visit. */ 44 stellarData = {}; 45 stellarData.guid = GetGUID(); // Mark User with GUID. 46 stellarData.timestamp = Math.floor( Date.now() / 1000 ); 47 48 /** Remember stellarData in Local Storage. */ 49 window.localStorage.setItem( 'mdpLocalStellarData', JSON.stringify( stellarData ) ); 50 window.sessionStorage.setItem( 'mdpSessionStellarData', JSON.stringify( stellarData ) ); 51 } 52 53 } 54 55 /** Find all Stellar boxes */ 56 const stellarBox = document.querySelectorAll( '.mdp-stellar-box' ); 57 58 /** Get data from the Local Storage and Session Storage */ 59 let stellarDataLS = JSON.parse( window.localStorage.getItem( "mdpLocalStellarData" ) ); 60 let stellarDataSS = JSON.parse( window.sessionStorage.getItem( "mdpSessionStellarData" ) ); 61 62 /** 63 * Pseudo GUID. We use unique user id to identify previously voted users. 64 * 65 * @returns {String} 66 **/ 67 function GetGUID() { 68 69 // noinspection SpellCheckingInspection 70 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 71 72 const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); // jshint ignore:line 73 return v.toString(16); 74 75 } ); 76 77 } 78 79 /** 80 * Prepare stellarData object 81 * @return {any} 82 */ 83 function prepareStellarData() { 84 85 /** Read stellarData from Local Storage. */ 86 let mdpLocalStellarData = JSON.parse( window.localStorage.getItem( 'mdpLocalStellarData' ) ); 87 88 /** Set LocalStorage and SessionStorage */ 89 if ( mdpLocalStellarData === null ) { 90 /** We generate a key once for the entire session. */ 91 mdpLocalStellarData = {}; 92 mdpLocalStellarData.guid = GetGUID(); 93 94 /** Remember stellarData in Local Storage. */ 95 window.localStorage.setItem('mdpLocalStellarData', JSON.stringify( mdpLocalStellarData ) ); 96 window.sessionStorage.setItem('mdpSessionStellarData', JSON.stringify( mdpLocalStellarData ) ); 97 } 98 99 return mdpLocalStellarData; 100 101 } 102 103 /** 104 * Hover handler for each <i> element 105 * 106 * @param event 107 */ 108 function stellarHover( event ) { 109 110 if ( event.target.closest( 'i' ) !== null ) { 111 112 const targetI = event.target.closest( 'i' ); //eventAR 113 const iconsBox = targetI.closest( '.mdp-box-icon' ); 114 const icons = iconsBox.getElementsByTagName( 'i' ); //eventLinks 115 const count = targetI.getAttribute( 'data-id' ); 116 117 // Add active class for each <i> element 118 if ( icons.length > 0 ) { 119 for( let i = 0; i < count; i++ ){ 120 icons[ i ].classList.add( 'mdp-box-list-hover' ); 121 } 122 } 123 124 // Add hover class for target <i> 125 targetI.classList.remove( 'mdp-stellar-hover' ); 126 127 // Update the rating valuation if it exist on the page 128 if ( iconsBox.querySelector( '.mdp-rating-res' ) !== null ) { 129 iconsBox.querySelector( '.mdp-rating-res' ).innerText = count; 130 } 131 132 // Add and place the rating valuation tooltip to the hovered element if exist 133 if ( iconsBox.querySelector( '.mdp-stellar-tooltip' ) !== null ) { 134 135 const ratingTooltip = iconsBox.querySelector( '.mdp-stellar-tooltip' ); 136 const targetRect = targetI.getBoundingClientRect(); 137 const parentRect = iconsBox.getBoundingClientRect(); 138 139 ratingTooltip.classList.add('mdp-stellar-tooltip-show'); 140 ratingTooltip.style.bottom = targetRect.top - parentRect.top + targetRect.height + 16 + 4 + 'px'; 141 ratingTooltip.style.left = targetRect.left - parentRect.left + ( targetRect.width - ratingTooltip.getBoundingClientRect().width ) / 2 + 'px'; 142 143 } 144 145 } 146 147 } 148 149 /** 150 * Ending Rating Bar Interaction Handler 151 * @param event - On mouseout event 152 * @return {boolean} 153 */ 154 function endInteraction( event ) { 155 156 const leaveTo = event.toElement || event.relatedTarget; 157 const target = event.target; 158 159 // Set <i> to default state if interaction is ended 160 if ( event.target.closest( 'i' ) !== null ) { 161 const targetI = event.target.closest( 'i' ); //e.target; 162 const iconsBox = targetI.closest( '.mdp-box-icon' ); 163 const icons = iconsBox.getElementsByTagName( 'i' ); 164 const count = targetI.getAttribute( 'data-id' ); 165 166 // Remove hover class for target <i> 167 targetI.classList.remove( 'mdp-stellar-hover' ); 168 169 // Remove active class for each <i> element 170 if ( icons.length > 0 ) { 171 for( let i = 0; i < icons.length; i++ ){ 172 icons[ i ].classList.remove( 'mdp-box-list-hover' ); 173 } 174 } 175 176 // Update the rating valuation if it exist on the page 177 if ( iconsBox.querySelector( '.mdp-rating-res' ) !== null ) { 178 179 if ( iconsBox.querySelector( '.mdp-stellar-tooltip' ) !== null ) { 180 // Sets the rating valuation data = data-id of leaved icon for tooltip 181 iconsBox.querySelector( '.mdp-rating-res' ).innerText = count; 182 } else { 183 // Sets 0 in other cases 184 iconsBox.querySelector( '.mdp-rating-res' ).innerText = 0; 185 } 186 187 } 188 189 } 190 191 // Hide tooltip is interaction is completely finished 192 if ( leaveTo !== null ) { //Right click 193 194 if ( leaveTo.querySelector( '.mdp-box-icon' ) !== null || target.classList.contains( 'mdp-stellar-tooltip' ) || target.tagName === 'SPAN' ) { 195 196 if ( document.querySelector( '.mdp-stellar-tooltip-show' ) !== null ) { 197 document.querySelector( '.mdp-stellar-tooltip-show' ).classList.remove('mdp-stellar-tooltip-show'); 198 } 199 200 } 201 202 } 203 204 } 205 206 /** 207 * Rating clicking handler 208 * Add scalable bubbles effect for clicked <i> 209 * 210 * @param target - Click Event target 211 */ 212 function ratingClickEffect( target ) { 213 214 // Create bubble svg 215 const clickBubbles = document.createElementNS("http://www.w3.org/2000/svg", 'svg'); 216 clickBubbles.classList.add( 'mdp-stellar-bubbles' ); 217 clickBubbles.innerHTML = '<g id="grp7" opacity="0" transform="translate(7 6)">\n ' + 218 '<circle class="oval1" cx="2" cy="6" r="6"/><circle class="oval2" cx="5" cy="2" r="6"/>\n' + 219 '</g>\n<g id="grp6" opacity="0" transform="translate(0 28)">\n ' + 220 '<circle class="oval1" cx="2" cy="7" r="5"/><circle class="oval2" cx="3" cy="2" r="7"/>\n' + 221 '</g>\n<g id="grp3" opacity="0" transform="translate(52 28)">\n ' + 222 '<circle class="oval2" cx="2" cy="7" r="6"/><circle class="oval1" cx="4" cy="2" r="6"/>\n' + 223 '</g>\n<g id="grp2" opacity="0" transform="translate(44 6)">\n ' + 224 '<circle class="oval2" cx="5" cy="6" r="5"/><circle class="oval1" cx="2" cy="2" r="7"/>\n' + 225 '</g>\n<g id="grp5" opacity="0" transform="translate(14 50)">\n ' + 226 '<circle class="oval1" cx="6" cy="5" r="6"/><circle class="oval2" cx="2" cy="2" r="6"/>\n' + 227 '</g>\n<g id="grp4" opacity="0" transform="translate(35 50)">\n ' + 228 '<circle class="oval1" cx="6" cy="5" r="5"/><circle class="oval2" cx="2" cy="2" r="7"/>\n' + 229 '</g>\n<g id="grp1" opacity="0" transform="translate(24 0)">\n ' + 230 '<circle class="oval1" cx="2" cy="6" r="5"/><circle class="oval2" cx="5" cy="2" r="7"/>\n' + 231 '</g>'; 232 233 // Set scale for click effect 234 if ( typeof getComputedStyle( target ) === 'object' ) { 235 if ( getComputedStyle( target )['font-size'].length > 0 ) { 236 clickBubbles.style.top = '-' + (getComputedStyle( target ).paddingLeft.replace('px','') - 1) + 'px'; 237 clickBubbles.style.transform = 'scale(' + target.offsetWidth / 60 + ')'; 238 } 239 } 240 241 // Append SVG bubble effects 242 target.appendChild( clickBubbles ); 243 244 // Display click effects 245 setTimeout( function () { 246 target.classList.add( 'mdp-stellar-rated' ); 247 clickBubbles.classList.add( 'mdp-stellar-click' ); 248 }, 50 ); 249 250 // Remove click effects 251 setTimeout( function () { 252 target.classList.remove( 'mdp-stellar-rated' ); 253 clickBubbles.remove(); 254 }, 1000 ); 255 256 } 257 258 /** 259 * Create AJAX call 260 * 261 * @param {object} data 262 * @return 263 **/ 264 function stellarRequest(data) { 265 266 /** AJAX call */ 267 const xhttp = new XMLHttpRequest(); 268 let stellarDataLS = JSON.parse( window.localStorage.getItem("mdpLocalStellarData") ); 269 270 xhttp.open('POST', wpData.url + '?action=process_stellar', true); 271 xhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 272 xhttp.send(encodeURI('stellar_id=' + data.stellar_id + '&rating=' + data.rating + '&length=' + data.length + '&guid=' + stellarDataLS.guid + '&new_stellar=' + data.new_stellar)); 273 274 xhttp.onload = function () { 275 276 // Request failed 277 if ( ! xhttp.status ) { 278 279 console.warn('Request failed! See below:'); 280 console.error( data ); 281 282 } 283 284 // Request error 285 else if ( xhttp.status !== 200 ) { 286 287 console.error('Request error. Returned status of ' + xhttp.status); 288 289 } 290 291 // Request success 292 else if ( xhttp.status === 200 ) { 293 294 fNew = 0; 295 let answer = JSON.parse( xhttp.response ); 296 297 /** Message about the result of sending data. */ 298 if ( answer.message !== undefined ) { 299 console.warn( answer.message ); 300 } 301 302 } 303 304 }; 305 306 } 307 308 /** 309 * Request page rank 310 * @param data 311 */ 312 function stellarPageRating(data) { 313 314 /** AJAX call */ 315 const xhttp = new XMLHttpRequest(); 316 xhttp.open('POST', wpData.url + '?action=stellar_page_rating', true); 317 xhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 318 xhttp.send(encodeURI('page_rating=1'+ '&stellar_id=' + data.stellar_id )); 319 320 xhttp.onload = function () { 321 322 // Request failed 323 if ( ! xhttp.status ) { 324 console.warn('Request failed! See below:'); 325 } 326 327 // Request error 328 else if ( xhttp.status !== 200 ) { 329 console.error('Request error. Returned status of ' + xhttp.status); 330 } 331 332 // Request success 333 else if ( xhttp.status === 200 ) { 334 335 let answer = JSON.parse( xhttp.response ); 336 337 /** After first like, one time update counter. */ 338 if ( document.getElementsByClassName( 'mdp-stellar-results' ).length > 0 ) { 339 340 const stellarResult = document.getElementsByClassName( 'mdp-stellar-results' ); 341 for ( let i = 0; i < stellarResult.length; i++ ) { 342 stellarResult[ i ].innerText = answer.page_rating; 343 } 344 345 } 346 347 } 348 }; 349 } 350 351 /** 352 * We call the function when you click on the icon. 353 * @param event 354 */ 355 function stellarClick( event ) { 356 357 if ( event.target.closest( 'i' ) !== null ) { 358 359 const targetI = event.target.closest('i'); //eventTarget 360 const iconsBox = targetI.closest('.mdp-box-icon'); 361 const icons = iconsBox.getElementsByTagName('i'); 362 const rating = targetI.getAttribute('data-id'); 363 const maxRating = iconsBox.getAttribute('data-rating'); //count_rating 364 365 // Send async request 366 const data = { 'stellar_id': id, 'rating': rating, 'length': wpData.length, 'new_stellar': fNew }; 367 stellarRequest( data ); 368 369 // Request Page Rank 370 setTimeout( () => { stellarPageRating(data); },200 ); 371 372 // Save data to local storage 373 stellarData['rating_' + id] = rating; 374 stellarData.timestamp = Math.floor( Date.now() / 1000 ); 375 stellarData.best_rating = maxRating; 376 window.localStorage.setItem( 'mdpLocalStellarData', JSON.stringify( stellarData ) ); 377 window.sessionStorage.setItem( 'mdpSessionStellarData', JSON.stringify( stellarData ) ); 378 379 // Remove and add mdp-stellar-active class for <i>. 380 if ( icons.length > 0 ) { 381 382 // Remove old voting active class 383 for( let i = 0; i < icons.length; i++ ){ 384 icons[ i ].classList.remove( 'mdp-stellar-active' ); 385 icons[ i ].querySelector( 'div' ).style.width = '0'; 386 } 387 388 // Add new voting active class 389 for( let i = 0; i < rating; i++ ){ 390 icons[ i ].classList.add( 'mdp-stellar-active' ); 391 } 392 393 } 394 395 // Update 'Your page rank' data 396 iconsBox.parentNode.querySelector( '.mdp-stellar-your-rating span' ).innerHTML = rating; 397 398 // Click effect 399 ratingClickEffect( targetI ); 400 401 // Update the rating valuation if it exist on the page 402 if ( iconsBox.querySelector( '.mdp-rating-res' ) !== null ) { 403 iconsBox.querySelector( '.mdp-rating-res' ).innerText = rating; 404 } 405 406
406 } 407 408 } 409 410 /** 411 * Display results for already voted users. 412 * 413 * @param $wrapper - Stellar wrapper 414 */ 415 function stellarResults( $wrapper ) { 416 417 if ( JSON.parse( window.localStorage.getItem( "mdpLocalStellarData" ) ) === null ){ return; } 418 419 let stellarDataLS = JSON.parse( window.localStorage.getItem( "mdpLocalStellarData" ) ); 420 const icons = $wrapper.getElementsByTagName('i'); 421 const maxRating = icons.length; 422 let rating = 0; 423 424 // Recalculate and determine the number of painted icons 425 if ( stellarData['rating_' + id] !== undefined ) { 426 427 if ( maxRating === stellarDataLS.best_rating ) { 428 rating = stellarData['rating_' + id]; 429 } else { 430 rating = Math.round((stellarData['rating_' + id] / stellarDataLS.best_rating) * maxRating); 431 } 432 433 } 434 435 // Set rating data-attribute to User rating of 436 if ( rating !== 0 && $wrapper.getAttribute( 'data-secrecy' ) === 'off' ) { 437 $wrapper.setAttribute( 'data-rating', rating ); 438 } 439 440 // Fill the icons 441 stellarRating( $wrapper ); 442 443 // Update the rating valuation if it exist on the page 444 if ( $wrapper.querySelector( '.mdp-rating-res' ) !== null ) { 445 $wrapper.querySelector( '.mdp-rating-res' ).innerText = rating; 446 } 447 448 449 } 450 451 /** 452 * Run all Stellar events 453 * 454 * @param $wrapper - Stellar wrapper container 455 */ 456 function stellarEvents( $wrapper ) { 457 458 const box = $wrapper.querySelector( '.mdp-box-icon' ); 459 460 /** UnBind and Bind the End of interaction with the icons bar */ 461 box.removeEventListener( 'mouseout', endInteraction ); 462 box.addEventListener( 'mouseout', endInteraction ); 463 464 /** Find all Stellar Icons in single Stellar Box */ 465 const stellarIcon = box.getElementsByTagName( 'i' ); 466 if ( stellarIcon.length > 0 ) { 467 468 for( let i = 0; i < stellarIcon.length; i++ ) { 469 470 /** UnBind and Bind the hover event listener for each <i> */ 471 stellarIcon[i].removeEventListener( 'mouseover', stellarHover ); 472 stellarIcon[i].addEventListener( 'mouseover', stellarHover ); 473 474 /** UnBind and Bind the click event listener for each <i> */ 475 stellarIcon[i].removeEventListener( 'click', stellarClick ); 476 stellarIcon[i].addEventListener( 'click', stellarClick ); 477 478 } 479 480 } 481 482 } 483 484 /** 485 * Display and hide message for unable voting 486 * 487 * @param event - Click event 488 */ 489 function stellarMessage( event ) { 490 491 const box = event.target.closest( '.mdp-box-icon' ); 492 const newMessage = document.createElement( 'span' ); 493 494 /** Add message */ 495 newMessage.classList.add( 'mdp-stellar-message' ); 496 newMessage.innerText = wpData.limitText; 497 box.appendChild( newMessage ); 498 499 /** Hide message */ 500 setTimeout( function () { 501 newMessage.remove(); 502 }, 2000 ); 503 504 } 505 506 /** 507 * Returns rating value in rounded format 508 * 509 * @param $wrapper - Stellar parent element 510 * @return {number|*} 511 */ 512 function getRoundRating( $wrapper ) { 513 514 const round = $wrapper.getAttribute( 'data-round' ); 515 const rating = $wrapper.getAttribute( 'data-rating' ); 516 517 if ( 'up' === round ) { return Math.ceil( rating ); } 518 if ( 'no' === round ) { return rating; } 519 if ( 'down' === round ) { return Math.floor( rating ); } 520 521 } 522 523 /** 524 * Fill the icons 525 * 526 * @param $wrapper - Stellar parent element 527 */ 528 function stellarRating( $wrapper ) { 529 530 if ( $wrapper.getElementsByTagName( 'i' ).length < 1 ) { return; } 531 532 const secrecy = $wrapper.getAttribute( 'data-secrecy' ) !== 'on'; // true = hide voting results 533 const voted = stellarData['rating_' + id] !== undefined; 534 535 const icons = $wrapper.getElementsByTagName( 'i' ); 536 for( let i = 0; i < icons.length; i++ ){ 537 538 const activeSVG = icons[ i ].querySelector( 'div' ); 539 const decimals = getRoundRating( $wrapper ) - Math.floor( $wrapper.getAttribute( 'data-rating' ) ); 540 541 // Fill or empty stars 542 activeSVG.style.width = i < getRoundRating( $wrapper ) ? '100%' : '0'; 543 544 // Half stars 545 if ( decimals !== 0 && i === Math.floor( $wrapper.getAttribute( 'data-rating' ) ) ) { activeSVG.style.width = 100 * decimals + '%'; } 546 547 // Hide Empty stars id secrecy activated and no rating value 548 if ( secrecy && !voted ) { 549 activeSVG.style.width = '0'; 550 } 551 552 } 553 554 } 555 556 /**
557 * Displays 'Your rating is' text message and rating 558 * 559 * @param $wrapper - Stellar parent element 560 */ 561 function yourRatingMessage( $wrapper ) { 562 563 if ( $wrapper.getAttribute( 'data-your-rating' ) !== 'text' ) { return; } // Exit if message set to hide 564 if ( stellarDataLS['rating_' + id] === undefined ) { return; } // Exit if no votes in Local Storage 565 566 $wrapper.querySelector( '.mdp-stellar-your-rating' ).classList.remove( 'mdp-hidden' ); 567 $wrapper.querySelector( '.mdp-stellar-your-rating span' ).innerHTML += ' ' + stellarDataLS[ 'rating_' + id ]; 568 569 } 570 571 /** 572 * Behavior of Stellar regarding to Local Storage and Session Storage 573 * 574 * @param $wrapper - Stellar parent element 575 */ 576 function stellarBehavior( $wrapper ) { 577 578 const stellarIconBoxes = $wrapper.querySelector( '.mdp-box-icon' ); 579 580 /** Already has record in Local Storage */ 581 if( stellarDataLS['rating_' + id] !== undefined ) { 582 583 if ( stellarDataSS === null ) { 584 585 /** Add class and message for unable rating */ 586 stellarIconBoxes.classList.add( 'mdp-stellar-unable' ); 587 stellarIconBoxes.addEventListener( 'click', stellarMessage ); 588 589 } else { 590 591 /** Voted in this session */ 592 fNew = 0; 593 stellarEvents( $wrapper ); 594 595 } 596 597 } else { 598 599 /** No record in Local Storage */ 600 stellarEvents( $wrapper ); 601 602 } 603 604 } 605 606 /** Run for all Stellar Boxes */ 607 for ( let i = 0; i < stellarBox.length; i++ ) { 608 609 const $stellarWrapper = stellarBox[ i ]; // Stellar Wrapper 610 611 if ( $stellarWrapper.getAttribute( 'data-your-rating' ) !== 'stars' ) { // Hide user voting result 'hide' and 'text' 612 613 stellarRating( $stellarWrapper ); 614 yourRatingMessage( $stellarWrapper ); 615 if ( wpData.session === 'on' ) { stellarBehavior( $stellarWrapper ); } else { stellarEvents( $stellarWrapper ); } 616 617 } else { // Show user voting result as stars 618 619 stellarResults( $stellarWrapper ); 620 if ( wpData.session === 'on' ) { stellarBehavior( $stellarWrapper ); } else { stellarEvents( $stellarWrapper ); } 621 622 } 623 624 } 625 626 /** When you reload the page, the likes are not erased with the mouse. */ 627 sessionStorage.setItem( "is_reloaded", true ); 628 629 /** Check multiple browser tabs are open */ 630 const watchTabs = function( event ) { 631 632 /** Identify the event that occurred. */ 633 if ( event.key === 'mdpLocalStellarData' ) { 634 635 /** Get data from the Local Storage. */ 636 let stellarDataLS = JSON.parse( window.localStorage.getItem( "mdpLocalStellarData" ) ); 637 638 /** We remove the asset class from the icons. */ 639 for ( let i = 0; i < stellarDataLS.best_rating; i++ ) { 640 stellarBox[ 0 ].getElementsByTagName( 'i' )[i].classList.remove( 'mdp-stellar-active' ); 641 } 642 643 /** Add an asset class to the icons that have been selected. */ 644 for ( let i = 0; i < parseInt( stellarDataLS['rating_' + id] ); i++ ) { 645 /** Set the flag of the current session. */ 646 fNew = 0; 647 648 stellarBox[ 0 ].getElementsByTagName( 'i' )[i].classList.add( 'mdp-stellar-active' ); 649 } 650 651 const data = { 652 'stellar_id': id, 653 'rating': stellarDataLS['rating_' + id], 654 'length': wpData.length, 655 'new_stellar': fNew 656 }; 657 658 /** Request Page Rank. */ 659 setTimeout( () => { 660 stellarPageRating( data ); 661 }, 200 ); 662 663 } 664 665 }; 666 window.addEventListener( 'storage', watchTabs ); 667 668 } ); 669 670} () );
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.